- 짝 `common_util_structure_attach.ts` attachShapes ↔ `B06_Section_Engine_Attach.py` attach_shapes — 열쇠 걷고 관 세트 → 벽 → 포장 → 맹암거 → 빈칸 경고(측점 계획 자리 · 「옹벽 높이」 · 「배관 관경」 · 「맹암거 암거 높이」 · 「물넘이포장 월류 높이」) - 서버 `_read_section_detail` · 재생성 응답 = attach_saved_shapes 하나 · 화면 `withDraftShapes`(현황 = 초안 ?? 저장분 · 초안 없어지면 저장분으로 한 번) — decorate 관 세트 · withDraftPipeKinds · 맹암거 · 포장 · 벽 블록 걷음 · 구조물 초안 없을 때 미리 뜬 횡단에 포장 · 벽 · 맹암거 빠지던 것 · 반폭 넓힘 뒤 날 상세(초안 빠짐) 고침 - 파이썬 얹기 = 목록 함수(culvert_sets_from · attach_culvert_specs · wall_specs_from · attach_wall_specs · underdrains_from · attach_underdrain_specs) + 옛 파일 함수는 감쌈 - 3D 지문 = SHAPE_KEYS(`common_util_structure_shape_keys.ts`) + 조작값 통째(손 해시 걷음 · 벽 기초 · 단 · 포장 · 맹암거 빠짐) · C군 벽 = 기준 측점과 가장 가까운 측점에서만 시작 ~ 끝 스윕(`wallSweepAt`) - 소단 = 표 형상 berm(BERM_TYPE_ID 걷음 · 화면 isBermType · 서버 catalog_row) - 시험: test_323_4_attach_mirror(42종 + 빈칸 · 서버 = 화면 · 옛 자리 걷힘 · 벽 한 번 · 지문) · 전수 표 ★ 둘(물넘이포장 걸친 격자 100 = 서버와 같음 · STAGE 4) · 옛 도움 파일 = 측점 → 형상 차례 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
179 lines
6.3 KiB
Python
179 lines
6.3 KiB
Python
"""PLAN 323-4 형상 얹기 한 벌 — 파이썬 `attach_shapes` = TS `attachShapes`(거울) · 저장 전 그림 = 저장 뒤 그림.
|
|
|
|
- 42종 견본(관 지점 · 구조물 · 정의 기본값) + 빈칸 견본 → 측점마다 그림 자료(관 세트 · 벽 · 포장 · 맹암거) ·
|
|
빈칸 경고 `blank`(「종류 칸」 · 측점 계획 자리)가 서버 = 화면.
|
|
- 옛 열쇠는 걷고 다시(물넘이포장을 옮기면 옛 자리 파임이 빠짐 · 걸친 측점에 새로).
|
|
- 3D — C군 벽은 덮는 측점 중 기준 측점에서만 시작 ~ 끝 스윕 · 지문 = 그림 자료 통째(손 해시가 빠뜨린 칸도).
|
|
TS 쪽 = `helper_323_4_attach.cjs`(Node).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import json
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance, load_definition
|
|
from B06_Section.B06_Section_Engine_Attach import attach_shapes
|
|
from common_util.common_util_drainage_pipes import parse_pipe_points
|
|
from common_util.common_util_structure_catalog import catalog
|
|
from M02_MasterTemplete.StructOptions.M02_SO_Options import with_visible
|
|
|
|
HERE = Path(__file__).parent
|
|
ROOT = HERE.parent.parent
|
|
|
|
pytestmark = pytest.mark.skipif(
|
|
not (ROOT / "config" / "node_modules").is_dir(), reason="프론트엔드 의존성 미설치"
|
|
)
|
|
|
|
TYPES = with_visible().model_dump()["types"]
|
|
|
|
|
|
def _defaults(type_id: str) -> dict:
|
|
"""정의 기본값(빈 값 뺌) — 그림이 서는 견본."""
|
|
options = next(t for t in TYPES if t["type_id"] == type_id)["options"]
|
|
return {o["key"]: o["default"] for o in options if o["default"] not in (None, "")}
|
|
|
|
|
|
def _samples() -> tuple[list[dict], list[dict]]:
|
|
"""42종 하나씩(30 m 간격 · 구간형 = 기준 ± 5) + 빈칸 셋(관경 · 옹벽 높이 · 맹암거 높이)."""
|
|
pipes: list[dict] = []
|
|
structures: list[dict] = []
|
|
rows = sorted(catalog().values(), key=lambda row: row["typeId"])
|
|
for index, row in enumerate(rows):
|
|
at = 15.0 + 30.0 * index
|
|
options = _defaults(row["typeId"])
|
|
if row["owner"] == "pipe_points":
|
|
pipes.append({"chainage_m": at, "facility": row["typeId"], "options": options})
|
|
continue
|
|
span = {"start_m": at - 5, "end_m": at + 5} if row["placement"] == "interval" else {}
|
|
structures.append(
|
|
{
|
|
"structure_id": f"s{index}",
|
|
"type_id": row["typeId"],
|
|
"placement": row["placement"],
|
|
"chainage_m": at,
|
|
"options": options,
|
|
**span,
|
|
}
|
|
)
|
|
end = 15.0 + 30.0 * len(rows)
|
|
pipes.append({"chainage_m": end, "facility": "pipe", "options": {"pipe_diameter_mm": ""}})
|
|
structures.append(
|
|
{
|
|
"structure_id": "blank-wall",
|
|
"type_id": "retaining_wall",
|
|
"placement": "interval",
|
|
"start_m": end + 25,
|
|
"chainage_m": end + 30,
|
|
"end_m": end + 35,
|
|
"options": {},
|
|
}
|
|
)
|
|
structures.append(
|
|
{
|
|
"structure_id": "blank-drain",
|
|
"type_id": "cross_underdrain",
|
|
"placement": "point",
|
|
"chainage_m": end + 60,
|
|
"options": {},
|
|
}
|
|
)
|
|
return pipes, structures
|
|
|
|
|
|
def _sections(until: float) -> list[dict]:
|
|
"""5 m 마다 측점(모든 기준 · 시작 · 끝 자리) — 옛 열쇠를 미리 얹어 걷힘도 봄."""
|
|
out = []
|
|
for step in range(int(until // 5) + 2):
|
|
section = {"chainage_m": float(step * 5)}
|
|
if step == 1:
|
|
section["ford_pavement"] = {"type": "ford_pavement", "chainage_m": 5.0} # 옛 자리
|
|
out.append(section)
|
|
return out
|
|
|
|
|
|
PIPES, STRUCTURES = _samples()
|
|
UNTIL = 15.0 + 30.0 * len(catalog()) + 60
|
|
CASES = {
|
|
"all": {"sections": _sections(UNTIL), "pipes": PIPES, "structures": STRUCTURES},
|
|
"empty": {"sections": _sections(30), "pipes": [], "structures": []},
|
|
}
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def ts() -> dict:
|
|
payload = {"definition": load_definition(), "types": TYPES, "cases": CASES}
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
path = Path(folder) / "input.json"
|
|
path.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
|
|
out = subprocess.run(
|
|
["node", str(HERE / "helper_323_4_attach.cjs"), str(path)],
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
timeout=120,
|
|
check=True,
|
|
)
|
|
return json.loads(out.stdout)
|
|
|
|
|
|
def _py(case: dict) -> list[dict]:
|
|
sections = copy.deepcopy(case["sections"])
|
|
pipes = parse_pipe_points(case["pipes"])
|
|
structures = [StructureInstance.model_validate(s) for s in case["structures"]]
|
|
attach_shapes(sections, pipes, structures)
|
|
return json.loads(json.dumps(sections))
|
|
|
|
|
|
def test_mirror_python_equals_ts(ts: dict) -> None:
|
|
for name, case in CASES.items():
|
|
py, js = _py(case), ts["cases"][name]
|
|
assert len(py) == len(js), name
|
|
for left, right in zip(py, js):
|
|
assert left == right, (name, left["chainage_m"])
|
|
|
|
|
|
def test_every_shape_lands(ts: dict) -> None:
|
|
keys = {key for section in ts["cases"]["all"] for key in section if key != "chainage_m"}
|
|
assert {
|
|
"culvert",
|
|
"ford",
|
|
"box",
|
|
"ford_pavement",
|
|
"revetment",
|
|
"pavement",
|
|
"underdrain",
|
|
} <= keys
|
|
old = ts["cases"]["all"][1] # 옛 물넘이 자리(5 m) — 걷힘
|
|
assert "ford_pavement" not in old
|
|
assert all(len(section) == 1 for section in ts["cases"]["empty"])
|
|
|
|
|
|
def test_blank_chips(ts: dict) -> None:
|
|
end = 15.0 + 30.0 * len(catalog())
|
|
by_at = {s["chainage_m"]: s.get("blank") for s in ts["cases"]["all"]}
|
|
assert by_at[end] == ["배관 관경"]
|
|
assert by_at[end + 25] == by_at[end + 30] == by_at[end + 35] == ["옹벽 높이"]
|
|
assert by_at[end + 60] == ["맹암거 암거 높이"]
|
|
assert by_at[end + 5] is None # 계획 자리만
|
|
|
|
|
|
def test_wall_sweeps_once(ts: dict) -> None:
|
|
assert ts["sweep"] == [None, None, {"beforeM": 8, "afterM": 12}, None]
|
|
|
|
|
|
def test_fingerprint_covers_drawing(ts: dict) -> None:
|
|
assert ts["fingerprint"] == {
|
|
"same": True,
|
|
"foundation": True,
|
|
"pavement": True,
|
|
"underdrain": True,
|
|
"adjust": True,
|
|
"plain": True,
|
|
}
|