- 시험: 무늬는 박스 안(형태 다섯 · 견본 서른) · 확정본 kind development · 3.55 폭 표기 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf
293 lines
12 KiB
Python
293 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""B07 전개도(요구 241-2 · 3 · PLAN 377-1) — 측점 표기 · 묶음 · 거리 · 면적 · 계 · 그림 · DXF.
|
||
|
||
견본(그림 241 콘크리트포장): No.22+00~No.26+05 = 85 m × B 3.5 = 297.5 ㎡ (측점 간격 20 m).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
from B05_Profile.B05_Profile_Structures_Repository import structures_file_path
|
||
from B07_DesignDetail import B07_DesignDetail_Engine_Cad_Development as dev
|
||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Development_Draw import (
|
||
FILL_LAYER_ID,
|
||
build_development_drawing,
|
||
fill_entities,
|
||
)
|
||
from B07_DesignDetail.B07_DesignDetail_Engine_Frame_Export import drawing_to_dxf
|
||
|
||
POINTS = [
|
||
{ # 관 Φ800 — 유입 찰 H2.0(전 3 · 후 3) · 유출 메 높이 빈칸(관경 최소 1.3) · 추가 흙막이 1 · 2
|
||
"chainage_m": 88.0,
|
||
"options": {
|
||
"pipe_diameter_mm": 800,
|
||
"inlet_type": "기슭막이",
|
||
"inlet_revet_form": "돌쌓기(찰)",
|
||
"inlet_revet_height_m": 2.0,
|
||
"inlet_revet_before_m": 3,
|
||
"inlet_revet_after_m": 3,
|
||
"outlet_type": "기슭막이",
|
||
"outlet_revet_form": "돌쌓기(메)",
|
||
"outlet_revet_length_m": 8, # 전 · 후 빈칸 → 반씩
|
||
"soil_add_1_form": "돌쌓기(메)",
|
||
"soil_add_1_height": 1.5,
|
||
"soil_add_1_before": 2,
|
||
"soil_add_1_after": 4.5,
|
||
"soil_add_2_form": "돌쌓기(메)",
|
||
"soil_add_2_height": 1.5,
|
||
},
|
||
},
|
||
{ # 박스 — 유출 흙막이(높이 빈칸 = 박스 높이 2.5) · 유입 날개벽
|
||
"chainage_m": 300.0,
|
||
"facility": "box_culvert",
|
||
"options": {
|
||
"body_height_m": 2.5,
|
||
"wing_in": "날개벽",
|
||
"wing_out": "흙막이",
|
||
"outlet_revet_form": "돌쌓기(메)",
|
||
"outlet_revet_before_m": 3,
|
||
"outlet_revet_after_m": 3,
|
||
},
|
||
},
|
||
]
|
||
STRUCTURES = [
|
||
{ # 콘크리트 포장 No.22+00 ~ No.26+05 · 폭 3.5
|
||
"type_id": "pavement_concrete",
|
||
"placement": "interval",
|
||
"start_m": 440.0,
|
||
"end_m": 525.0,
|
||
"options": {"width_m": 3.5},
|
||
},
|
||
{ # 폭 빈칸 = 횡단 노폭 구간 평균
|
||
"type_id": "pavement_concrete",
|
||
"placement": "interval",
|
||
"start_m": 720.0,
|
||
"end_m": 760.0,
|
||
"options": {},
|
||
},
|
||
{ # 흙막이 메 H2.0 + 추가 단 하나(찰 H1.0 = 별도 박스 · 찰 장)
|
||
"type_id": "soil_guard",
|
||
"placement": "interval",
|
||
"start_m": 80.0,
|
||
"end_m": 88.0,
|
||
"options": {
|
||
"form": "돌쌓기(메)",
|
||
"height_m": 2.0,
|
||
"tier_1_form": "돌쌓기(찰)",
|
||
"tier_1_height": 1.0,
|
||
},
|
||
},
|
||
{ # 숨김 벽 종류(돌쌓기(메)) = 흙막이 묶음 · 높이 빈칸
|
||
"type_id": "masonry_dry",
|
||
"placement": "interval",
|
||
"start_m": 100.0,
|
||
"end_m": 110.0,
|
||
"options": {"height_m": None},
|
||
},
|
||
{ # 옹벽 — 형태 = wall_form(form 은 형식)
|
||
"type_id": "retaining_wall",
|
||
"placement": "interval",
|
||
"start_m": 200.0,
|
||
"end_m": 210.0,
|
||
"options": {"form": "중력식", "wall_form": "콘크리트", "height_m": 3.0},
|
||
},
|
||
]
|
||
DESIGNS = {
|
||
88.0: {"extra_walls": [{"side": "outlet"}]}, # 유출 다단 1 → 추가 흙막이 2 는 안 섬
|
||
720.0: {"carriageway_width_m": 4.0},
|
||
740.0: {"carriageway_width_m": 5.0},
|
||
760.0: {"carriageway_width_m": 4.5},
|
||
}
|
||
|
||
|
||
@pytest.fixture
|
||
def root(tmp_path: Path) -> Path:
|
||
found = tmp_path / "proj"
|
||
edits = found / "B04_PreProcess/drainage/edits"
|
||
edits.mkdir(parents=True)
|
||
(edits / "pipe_points.json").write_text(
|
||
json.dumps({"points": [{**p, "source": "user"} for p in POINTS]}, ensure_ascii=False),
|
||
encoding="utf-8",
|
||
)
|
||
path = Path(structures_file_path(str(found)))
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
path.write_text(
|
||
json.dumps({"revision": 1, "structures": STRUCTURES}, ensure_ascii=False), encoding="utf-8"
|
||
)
|
||
return found
|
||
|
||
|
||
def _sheet(sheets, sheet_id):
|
||
return next(s for s in sheets if s["id"] == sheet_id)
|
||
|
||
|
||
def test_측점_표기():
|
||
assert dev.station_text(440.0, 20) == "No.22+00"
|
||
assert dev.station_text(525.0, 20) == "No.26+05"
|
||
assert dev.station_text(88.0, 20) == "No.04+08"
|
||
assert dev.station_text(445.5, 20) == "No.22+05.5"
|
||
assert dev.station_text(459.98, 20) == "No.23+00"
|
||
|
||
|
||
def test_그림_예_거리_면적_계():
|
||
rows = [
|
||
dev._row("concrete", "", 440.0, 525.0, 3.5, "a"),
|
||
dev._row("concrete", "", 720.0, 860.0, 3.5, "b"),
|
||
]
|
||
lines = dev.table_rows(rows, 20.0)
|
||
assert lines[0] == {
|
||
"station": "No.22+00~No.26+05",
|
||
"distance": 85.0,
|
||
"height": 3.5,
|
||
"area": 297.5,
|
||
}
|
||
assert lines[1]["area"] == 490.0
|
||
drawing = build_development_drawing(
|
||
"development_concrete",
|
||
{
|
||
"title": "콘크리트포장 전개도",
|
||
"group": "concrete",
|
||
"form": "",
|
||
"rows": rows,
|
||
"missing": 0,
|
||
},
|
||
20.0,
|
||
)
|
||
table = next(e for e in drawing["entities"] if e["type"] == "Table")
|
||
cells = table["shapeData"]["cells"]
|
||
assert cells[0][2]["text"] == "폭"
|
||
assert [c["text"] for c in cells[2]] == ["No.22+00~No.26+05", "85.0", "3.5", "297.5"]
|
||
assert [c["text"] for c in cells[-1]] == ["계", "225.0", "", "787.5"]
|
||
|
||
|
||
def test_묶음_거리_높이(root):
|
||
sheets = dev.development_sheets(dev.development_rows(root, DESIGNS))
|
||
ids = [s["id"] for s in sheets]
|
||
# 차례 = 관보호공 → BOX → 흙막이 → 옹벽 → 콘크리트포장 · 형태마다 한 장
|
||
assert ids == [
|
||
"development_pipe0",
|
||
"development_pipe1",
|
||
"development_box1",
|
||
"development_soil0",
|
||
"development_soil1",
|
||
"development_wall2",
|
||
"development_concrete",
|
||
]
|
||
pipe = _sheet(sheets, "development_pipe0")
|
||
assert pipe["title"] == "관보호공 돌쌓기(찰) 전개도"
|
||
assert [(r["start_m"], r["end_m"], r["height_m"]) for r in pipe["rows"]] == [(85.0, 91.0, 2.0)]
|
||
meh = _sheet(sheets, "development_pipe1")["rows"]
|
||
# 유출 길이 8 반씩(84~92 · 높이 빈칸 = 관경 최소 1.3) · 추가 흙막이 1(86~92.5) · 2 는 다단 밖
|
||
assert [(r["start_m"], r["end_m"], r["height_m"]) for r in meh] == [
|
||
(84.0, 92.0, 1.3),
|
||
(86.0, 92.5, 1.5),
|
||
]
|
||
box = _sheet(sheets, "development_box1")
|
||
assert box["title"] == "BOX암거 흙막이 돌쌓기(메) 전개도"
|
||
assert [(r["start_m"], r["end_m"], r["height_m"]) for r in box["rows"]] == [(297.0, 303.0, 2.5)]
|
||
# 추가 단 = 별도 박스(자기 형태 장) · 숨김 돌쌓기(메) = 흙막이 묶음 · 높이 빈칸
|
||
assert [(r["height_m"]) for r in _sheet(sheets, "development_soil0")["rows"]] == [1.0]
|
||
soil = _sheet(sheets, "development_soil1")["rows"]
|
||
assert [(r["start_m"], r["height_m"]) for r in soil] == [(80.0, 2.0), (100.0, None)]
|
||
wall = _sheet(sheets, "development_wall2")
|
||
assert wall["title"] == "옹벽 콘크리트 전개도" and wall["rows"][0]["height_m"] == 3.0
|
||
pave = _sheet(sheets, "development_concrete")
|
||
assert pave["title"] == "콘크리트포장 전개도"
|
||
assert [r["height_m"] for r in pave["rows"]] == [3.5, 4.5] # 빈 폭 = (4 + 5 + 4.5) / 3
|
||
# 표 · 끝 글 = 면적이 쓴 폭 그대로(3.55 를 3.5 로 줄여 보이지 않음)
|
||
odd = {**pave, "rows": [{**pave["rows"][0], "height_m": 3.55}]}
|
||
drawing = build_development_drawing(odd["id"], odd, 20.0)
|
||
cells = next(e for e in drawing["entities"] if e["type"] == "Table")["shapeData"]["cells"]
|
||
assert [c["text"] for c in cells[2]] == ["No.22+00~No.26+05", "85.0", "3.55", "301.8"]
|
||
labels = [e["shapeData"]["label"] for e in drawing["entities"] if e["type"] == "Text"]
|
||
assert "B=3.55" in labels
|
||
|
||
|
||
def test_설계_없으면_단수_안_거름_목록(root):
|
||
rows = dev.development_rows(root)
|
||
meh = [r for r in rows if r["group"] == "pipe" and r["form"] == "돌쌓기(메)"]
|
||
assert len(meh) == 3 # 추가 흙막이 2 도(단 수 모름) · 2 는 길이 빈칸
|
||
sheet = _sheet(dev.development_sheets(rows), "development_pipe1")
|
||
assert sheet["missing"] == 1 and len(sheet["rows"]) == 2
|
||
items = dev.development_list(root, {})
|
||
assert {i.kind for i in items} == {"development"}
|
||
assert (items[0].plan_kind, items[0].plan_kind_name) == ("development_pipe", "관보호공")
|
||
assert items[0].label == "관보호공 돌쌓기(찰) 전개도"
|
||
|
||
|
||
def test_넘치면_2장():
|
||
rows = [dev._row("soil", "돌망태", i * 20.0, i * 20.0 + 6, 2.0, str(i)) for i in range(35)]
|
||
sheets = dev.development_sheets(rows)
|
||
assert [s["id"] for s in sheets] == ["development_soil3", "development_soil3_2"]
|
||
assert [len(s["rows"]) for s in sheets] == [30, 5]
|
||
assert sheets[1]["title"] == "흙막이 돌망태 전개도 (2)"
|
||
|
||
|
||
def test_높이_빈칸_박스와_미입력_글_무늬_DXF(root):
|
||
sheets = dev.development_sheets(dev.development_rows(root, DESIGNS))
|
||
sheet = _sheet(sheets, "development_soil1")
|
||
drawing = build_development_drawing(sheet["id"], sheet, 20.0)
|
||
texts = [e["shapeData"]["label"] for e in drawing["entities"] if e["type"] == "Text"]
|
||
assert "흙막이 돌쌓기(메) 전개도" in texts
|
||
assert "No.04+00" in texts and "No.05+10" in texts
|
||
assert "H=2.0" in texts and "H=미입력" in texts and "L=8.0m" in texts
|
||
fills = [e for e in drawing["entities"] if e["layerId"] == FILL_LAYER_ID]
|
||
assert len(fills) > 10 # 돌 무늬
|
||
table = next(e for e in drawing["entities"] if e["type"] == "Table")
|
||
assert [c["text"] for c in table["shapeData"]["cells"][-1]] == ["계", "18.0", "", "16.0"]
|
||
# 같은 자료 = 같은 그림(씨앗 고정)
|
||
assert build_development_drawing(sheet["id"], sheet, 20.0) == drawing
|
||
dxf, _skipped = drawing_to_dxf(drawing)
|
||
text = dxf.decode("utf-8", "ignore")
|
||
assert "b07-dev-fill" in text and "No.04+00" in text and "297.5" not in text
|
||
assert text.count("LINE\r\n") > 50 # 돌 무늬 = 선(DXF 에도 남음)
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"form", ["돌쌓기(찰)", "돌쌓기(메)", "돌망태", "통나무·목재틀", "콘크리트"]
|
||
)
|
||
def test_무늬는_박스_안(form):
|
||
for seed in range(30):
|
||
width, height = 10 + seed * 9.7, 8 + seed % 7 * 5.0
|
||
for entity in fill_entities(f"s{seed}", form, "soil", (0.0, 0.0, width, height)):
|
||
for one in entity.get("children") or [entity]:
|
||
shape = one["shapeData"]
|
||
if "center" in shape:
|
||
r, (x, y) = shape["radius"], (shape["center"]["x"], shape["center"]["y"])
|
||
points = [(x - r, y - r), (x + r, y + r)]
|
||
else:
|
||
points = [(p["x"], p["y"]) for p in (shape["startPoint"], shape["endPoint"])]
|
||
for x, y in points:
|
||
assert -1e-6 <= x <= width + 1e-6 and -1e-6 <= y <= height + 1e-6, (seed, x, y)
|
||
|
||
|
||
def test_목록_전_장_없으면_빈_장(root, tmp_path):
|
||
long_path = tmp_path / "long.json"
|
||
long_path.write_text(json.dumps({"stations": []}), encoding="utf-8")
|
||
kind, label, drawing, confirmed, _ = dev.read_development(
|
||
root, long_path, "development_pipe1_3", DESIGNS
|
||
)
|
||
assert kind == "development" and label == "관보호공 돌쌓기(메) 전개도" and not confirmed
|
||
with pytest.raises(ValueError):
|
||
dev.read_development(root, long_path, "development_x-1", DESIGNS)
|
||
|
||
|
||
def test_확정본은_전개도_kind_로_읽음(root, tmp_path):
|
||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import DRAWING_FORMAT
|
||
from B07_DesignDetail.B07_DesignDetail_Router_Support import _design_root, _read_drawing
|
||
|
||
stage = _design_root(root)
|
||
(stage / "drawings").mkdir(parents=True)
|
||
saved = {"format": DRAWING_FORMAT, "entities": [], "layers": []}
|
||
(stage / "drawings" / "development_pipe0.json").write_text(json.dumps(saved), "utf-8")
|
||
manifest = {"drawings": {"development_pipe0": {"confirmed": True, "label": "관보호공"}}}
|
||
(stage / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False), "utf-8")
|
||
kind, label, drawing, confirmed, table = _read_drawing(
|
||
root, tmp_path / "long.json", "development_pipe0"
|
||
)
|
||
assert (kind, label, confirmed, table) == ("development", "관보호공", True, None)
|