- 부품 상자 common_util_structure_parts.json(갈래마다 부품 · 읽는 칸 · 자리꼴 · 정본 · 42종 처음값) · 표 읽기 common_util_structure_catalog.ts / .py 짝(정의 반영 ⊕ 처음값 ⊕ 부품 끔) · 거울 시험 test_322_1_catalog_mirror - 목록: pickGroups 군 차례 = 구조물군 열쇠 · 페이지 거르기 listPick(B05 both · B06 both + b06) · HIDDEN_PICK_GROUPS · HIDDEN_PICK_TYPES · pickable 걷음(옛 대피 셋 asRoad 그대로) - 측점: structureStationMarks · pipeStationMarks · is_station_planting_type · resolve_extra_stations = 표 횡단도(anchor · span3 · none) - 카드 이름: cardTitle / card_title 한 공식(빈칸 = 그 종류 정의 기본값) · 표시 이름이 정의 관종도 앎 - 형상: 벽 = shape wall(형태 = 형상칸.form · 물빼기 = 형상칸.weep) · 포장 = shape pavement(「사용 중」 거르기 없앰) · 맹암거 · 사토 · 관 지점 시설 세트 = shape - 3D 몸체 = 그림 자료 주인 종류의 표 3D 부품 · 알약 띠 = 표 자리꼴 구간(관 지점 시설은 알약만) - ★1 노출형 · 개거 횡단도 none · ★2 쇄석 화면에서도 포장층 · ★3 기슭막이 표 자리꼴 interval(정의 파일 그대로) · ★4 코드 숨김 10종 list hidden · ★5 관 빈칸 D800 → 정의 기본 D1000 - 옛 시험: 도움 파일 로더 27개가 표 모듈 공용 한 벌(helper_322_catalog_module) 씀 · 기대값 고침(272_1 · b05_extra_stations · roundtrip · 314 · 52_7 · walls 거울 · wall_drain_pipe) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
135 lines
5.8 KiB
Python
135 lines
5.8 KiB
Python
"""옹벽 물빼기 파이프 단면(73-2) — 횡단도에서 옹벽을 가르는 보기용 형상.
|
|
|
|
규칙(10-7 사용자 · 브레인):
|
|
① 옹벽(`retaining_wall`)만 — 흙막이(찰쌓기 · 콘크리트 등)에는 없음.
|
|
② 개수 = 반올림(벽 높이 / 1.5) · 최소 1 — 높이를 그 개수로 고르게 나눈 띠마다 가운데 하나.
|
|
1.5 m = 0.75 m 에 1개 · 3 m = 0.75 · 2.25 m 에 2개.
|
|
③ 공용 벽 무늬(`appendWallHatch`) 안 — B06 카드 · B07 횡단면도 · 서버 Node 가 한 벌.
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|
B06 = PROJECT_ROOT / "B06_Section"
|
|
TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
|
|
|
|
_RUNNER = """
|
|
import { writeFileSync } from "node:fs";
|
|
const make = (tag) => ({
|
|
tag, attrs: {}, children: [], text: "",
|
|
setAttribute(k, v) { this.attrs[k] = String(v); },
|
|
removeAttribute(k) { delete this.attrs[k]; },
|
|
getAttribute(k) { return this.attrs[k] ?? null; },
|
|
append(...nodes) { this.children.push(...nodes); },
|
|
set textContent(v) { this.text = v; },
|
|
});
|
|
globalThis.document = { createElementNS: (_ns, tag) => make(tag) };
|
|
const { buildRevetWallGeometry } = await import("./B06_Section_UI_Cross_Wall.js");
|
|
const { appendWallHatch } = await import("./B06_Section_UI_Cross_Wall_Hatch.js");
|
|
const walk = (node, out = []) => {
|
|
if (node.tag === "g" && node.attrs.class === "b06-chart__wall-drain") out.push(node);
|
|
for (const child of node.children ?? []) walk(child, out);
|
|
return out;
|
|
};
|
|
const drains = (heightM, form, retaining, outward = 1) => {
|
|
const wall = buildRevetWallGeometry({
|
|
anchor: { offset: 5, elevation: 100 }, outward, height: heightM - 0.5,
|
|
material: "concrete", role: "outlet", form,
|
|
});
|
|
if (retaining) wall.retaining = true;
|
|
const layer = make("svg");
|
|
appendWallHatch(layer, wall, (o) => o, (e) => -e, "t");
|
|
const bottom = wall.bottomBack.elevation;
|
|
const xs = wall.points.map((p) => p.offset);
|
|
const [lo, hi] = [Math.min(...xs) - 1e-6, Math.max(...xs) + 1e-6];
|
|
return walk(layer).map((pipe) => {
|
|
const lines = pipe.children.filter((c) => c.tag === "line");
|
|
const mid = lines.map((l) => -(Number(l.attrs.y1) + Number(l.attrs.y2)) / 2);
|
|
const drop = lines.map((l) => Number(l.attrs.y2) - Number(l.attrs.y1)); // x1 = 배면
|
|
return {
|
|
lines: lines.length,
|
|
center: (mid[0] + mid[1]) / 2 - bottom,
|
|
frontLower: drop.every((d) => d > 0),
|
|
inside: lines.every((l) => [l.attrs.x1, l.attrs.x2].every((v) => lo <= +v && +v <= hi)),
|
|
title: pipe.children.find((c) => c.tag === "title")?.text,
|
|
cls: lines.map((l) => l.attrs.class ?? null),
|
|
};
|
|
});
|
|
};
|
|
writeFileSync(process.argv[2], JSON.stringify({
|
|
h15: drains(1.5, "콘크리트", true),
|
|
h30: drains(3.0, "콘크리트", true),
|
|
h30right: drains(3.0, "중력식", true, -1),
|
|
low: drains(0.6, "콘크리트", true),
|
|
wet: drains(3.0, "돌쌓기(찰)", false),
|
|
concrete: drains(3.0, "콘크리트", false),
|
|
}));
|
|
"""
|
|
|
|
|
|
@pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치")
|
|
def test_옹벽만_높이_15m_띠마다_파이프_하나(tmp_path: Path) -> None:
|
|
out = tmp_path / "js"
|
|
subprocess.run( # noqa: S603 — 고정 실행 파일
|
|
[
|
|
"node",
|
|
str(TSC),
|
|
str(B06 / "B06_Section_UI_Cross_Wall.ts"),
|
|
"--outDir",
|
|
str(out),
|
|
"--module",
|
|
"esnext",
|
|
"--target",
|
|
"es2022",
|
|
"--moduleResolution",
|
|
"bundler",
|
|
"--skipLibCheck",
|
|
"--ignoreConfig",
|
|
],
|
|
cwd=str(PROJECT_ROOT),
|
|
capture_output=True,
|
|
)
|
|
for emitted in out.rglob("*.js"):
|
|
emitted.write_text(
|
|
re.sub(r'(from\s+"\./[^"]+)"', r'\1.js"', emitted.read_text(encoding="utf-8")),
|
|
encoding="utf-8",
|
|
)
|
|
base = next(path.parent for path in out.rglob("B06_Section_UI_Cross_Wall.js"))
|
|
(base / "runner.mjs").write_text(_RUNNER, encoding="utf-8")
|
|
result = tmp_path / "out.json"
|
|
subprocess.run( # noqa: S603
|
|
["node", str(base / "runner.mjs"), str(result)], check=True, capture_output=True
|
|
)
|
|
got = json.loads(result.read_text(encoding="utf-8"))
|
|
|
|
assert [p["center"] for p in got["h15"]] == pytest.approx([0.75], abs=0.01)
|
|
assert [p["center"] for p in got["h30"]] == pytest.approx([0.75, 2.25], abs=0.01)
|
|
assert [p["center"] for p in got["h30right"]] == pytest.approx([0.75, 2.25], abs=0.01)
|
|
assert len(got["low"]) == 1, "낮은 옹벽도 최소 1개"
|
|
assert got["wet"] == [] and got["concrete"] == [], "흙막이(찰쌓기 · 콘크리트)에 파이프가 섬"
|
|
for pipe in got["h15"] + got["h30"] + got["h30right"]:
|
|
assert pipe["lines"] == 2, "두 줄 평행선이 아님"
|
|
assert pipe["frontLower"], "앞쪽이 낮지 않음"
|
|
assert pipe["inside"], "파이프가 벽 밖으로 나감(B07 수확은 clip 이 없음)"
|
|
assert pipe["title"] == "물빼기(보기용)"
|
|
assert pipe["cls"] == [None, None], "선 색은 묶음 토큰이 정함"
|
|
|
|
|
|
def test_옹벽_표식은_구조물_종류로() -> None:
|
|
source = (B06 / "B06_Section_UI_Cross_Revetment.ts").read_text(encoding="utf-8")
|
|
# 물빼기 표식 = 구조물 표 형상칸.weep(322-1) — 처음값은 옹벽만
|
|
assert "catalogRow(spec.type_id)?.shapeOpts.weep" in source
|
|
start = json.loads(
|
|
(PROJECT_ROOT / "common_util" / "common_util_structure_parts.json").read_text("utf-8")
|
|
)
|
|
assert [t for t, v in start["처음값"].items() if v.get("형상칸", {}).get("weep")] == [
|
|
"retaining_wall"
|
|
]
|
|
css = (B06 / "B06_Section_UI_Style_Cross_Areas.css").read_text(encoding="utf-8")
|
|
assert re.search(r"\.b06-chart__wall-drain line \{[^}]*var\(--color-", css), "색 토큰 없음"
|