"""구조물도 상단 그림 — 그림 치수가 **수량표와 같은 한 벌**인가 (PLAN 12장). ⚠ 옛 B07 그림은 두께 식을 따로 적어, 상부·하부 두께를 넣어도 **그림만 옛 두께**였음. """ from __future__ import annotations import math import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[2] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from B08_Quantity.B08_Quantity_Engine_ObservedUnit import ( # noqa: E402 expand_observed, load_observed_table, ) from B08_Quantity.B08_Quantity_Engine_StructureFigure import ( # noqa: E402 attach_figures, build_figure, figure_reason, ) from B08_Quantity.B08_Quantity_Engine_UnitQuantity import ( # noqa: E402 back_length_default_note, stone_masonry, ) def _sheet(**options) -> dict: return { "title": "돌쌓기(메)", "type_id": "masonry_dry", "height_m": 2.0, "options": {"height_m": 2.0, **options}, } def _paths(shapes: list[dict]) -> list[dict]: return [shape for shape in shapes if shape["kind"] == "path"] def _area(points: list[list[float]]) -> float: return abs(sum(x0 * y1 - x1 * y0 for (x0, y0), (x1, y1) in zip(points, points[1:]))) / 2 def test_벽_두께는_사용자가_넣은_값을_따른다() -> None: wall = _paths(build_figure(_sheet(back_len_cm=35, face_slope_ratio=0.3)))[0]["points"] # 식: 상부 0.35+0.30=0.65 · 하부 0.65+0.30×(2−1)=0.95 · 기울기 0.3 → 앞면 위 끝 0.6 assert wall[:4] == [[0.0, 0.0], [0.6, 2.0], [1.25, 2.0], [0.95, 0.0]] given = _paths( build_figure( _sheet( back_len_cm=35, face_slope_ratio=0.3, thickness_top_m=0.5, thickness_bottom_m=1.2 ) ) )[0]["points"] assert given[2] == [1.1, 2.0] and given[3] == [1.2, 0.0] def test_터파기_넓이는_수량식과_같다() -> None: dig = _paths(build_figure(_sheet(back_len_cm=35, face_slope_ratio=0.3)))[1] assert dig["dash"] assert abs(_area(dig["points"]) - 2.0 * ((0.65 + 0.95) / 2 + 0.2)) < 1e-9 def test_못_그리는_장은_그림_대신_까닭이_실린다() -> None: payload = { "sheets": [ _sheet(), {**_sheet(stone_cm="60~80"), "type_id": "boulder_masonry"}, _sheet(back_len_cm=45), ] } attach_figures(payload) no_back, boulder, drawn = payload["sheets"] # 뒷길이 미정은 기본값이 있어 **그림이 섬**(2026-09-14 브레인 판정) — 까닭은 직경 축만. assert no_back["figure_reason"] is None and no_back["figure"] assert boulder["figure"] is None and "직경" in boulder["figure_reason"] assert drawn["figure_reason"] is None and drawn["figure"] def _texts(shapes: list[dict]) -> list[str]: return [shape["text"] for shape in shapes if shape["kind"] == "text"] def test_기본값으로_그린_장은_표_사유와_같은_말을_적는다() -> None: options = {"height_m": 2.0} _rows, table_notes = stone_masonry(2.0, 1.0, options, wet=False) table_default = [note for note in table_notes if "뒷길이를 안 골라" in note] assert table_default == [back_length_default_note(options, 45)] texts = _texts(build_figure(_sheet())) assert table_default[0] in texts assert "뒷길이 ℓ₃ = 45㎝ (기본)" in texts # 두께도 기본 45 로 — 상부 0.45+0.30 assert "상부 0.75 m" in texts chosen = _texts(build_figure(_sheet(back_len_cm=45))) assert "뒷길이 ℓ₃ = 45㎝" in chosen and not any("안 골라" in text for text in chosen) assert back_length_default_note({"back_len_cm": 45}, 45) is None def _wall_sheet(**options) -> dict: return { "title": "옹벽 H=2", "type_id": "retaining_wall", "height_m": 2.0, "options": {"height_m": 2.0, "form": "반중력식", **options}, } def test_옹벽_단면_치수가_표의_관측_수량을_그대로_낸다() -> None: """표 = 그림 — 그림이 쓰는 단면으로 표 수량(라이브러리 반올림)이 다시 나와야 함.""" entry = load_observed_table().find("retaining_wall", {"form": "반중력식", "height_m": 2.0}) section = entry["section"] amounts = {item["name"]: item["amount"] for item in entry["components"]} footing, wall, key, blinding = (section[k] for k in ("footing", "wall", "key", "blinding")) bottom = wall["top_m"] + wall["front_batter_m"] + wall["back_batter_m"] assert abs(bottom - 0.45) < 1e-9 concrete = ( footing["width_m"] * footing["thickness_m"] + key["width_m"] * key["depth_m"] + (wall["top_m"] + bottom) / 2 * wall["height_m"] ) blind = blinding["thickness_m"] * ( footing["width_m"] + 2 * blinding["overhang_m"] - key["width_m"] ) form = math.hypot(wall["height_m"], wall["back_batter_m"]) + math.hypot( wall["height_m"], wall["front_batter_m"] ) weep = section["weep"] pipe = wall["height_m"] / weep["area_m2"] * weep["pipe_length_m"] assert round(concrete, 2) == amounts["콘크리트"] # 1.345 assert round(blind, 2) == amounts["버림콘크리트"] # 0.145 assert round(form, 1) == amounts["유로폼"] # 3.205 assert round(pipe, 2) == amounts["물구멍관"] assert footing["thickness_m"] + wall["height_m"] == 2.0 def test_옹벽_그림은_기초잡석_두께를_산출_조건에서_받고_사유는_표와_같다() -> None: shapes = build_figure(_wall_sheet(), rubble_thickness_m=0.3) texts = _texts(shapes) assert "기초잡석 T=0.3 m — 산출 조건 · 폭은 버림과 같음" in texts assert "버림 T=0.10 · 폭 1.80 − 키 0.35 = 1.45 m" in texts wall = _paths(shapes)[0]["points"] assert [0.95, 0.4] in wall and [1.068, 2.0] in wall and [1.368, 2.0] in wall # 표 사유와 같은 말 — 터파기 줄이 없다는 것 _rows, table_notes = expand_observed( "retaining_wall", {"form": "반중력식", "height_m": 2.0}, _wall_sheet() | {"options": {"length_m": 10}}, ) missing = [note for note in table_notes if "터파기" in note] assert missing and missing[0] in texts assert not any(text.startswith("기초잡석") for text in _texts(build_figure(_wall_sheet(), 0))) def test_옹벽_못_그리는_장은_표와_같은_까닭() -> None: assert "형식" in figure_reason({**_wall_sheet(), "options": {"height_m": 2.0}}) other = figure_reason(_wall_sheet(height_m=1.6) | {"height_m": 1.6}) assert other.startswith("그림 없음 — ") and "자료에 없습니다" in other note = [t for t in _texts(build_figure(_wall_sheet(weep_hole_area_m2=3))) if "안 닿음" in t] assert note and "Ø50" in note[0] def test_양식_장은_양식이_푼_뒷길이로_그린다() -> None: sheet = {**_sheet(), "formula_sheet": {"vars": {"L3": 55}}} texts = _texts(build_figure(sheet)) assert "뒷길이 ℓ₃ = 55㎝ (기본)" in texts and "상부 0.85 m" in texts