"""표준도 **위쪽 그림** — 돌쌓기 단면(2단계, 2026-09-09). 실무 원본 탭이 「위 치수조서 + 아래 수량산출서」인데, 아래 표는 1단계에서 섰고 여기가 위다. ⚠ **기울기를 값으로 받는다 — 0.3 을 박지 않는다.** 품셈 표준경사 표 자동 판정이 다른 창에서 붙는 중이고, 붙으면 이 그림이 **그 값으로** 기울어져야 한다. `face_slope_ratio` 가 비면(=자동 미구현·미입력) 교본 7-3 기본 0.3 으로 그리되 **그 사실을 그림에 적는다**. ⚠ **수량이 쓰는 그 상수로 그린다** — `B08_Quantity_Engine_UnitQuantity.STONE_MASONRY` 를 직접 읽는다. 치수를 여기서 다시 적으면 **그림과 표가 갈린다**(CLAUDE.md 5장). 상부 두께 = 0.45 + 0.10·H 하부 두께 = 0.45 + 0.40·H 터파기 폭 = 평균두께 + 0.2 되메우기 두께 = 0.2 ⓘ 이 상수는 **뒷면이 수직인 1:0.3 벽**을 뜻한다 — 하부−상부 = 0.30·H 이고 전면이 뒤로 기우는 양도 0.3·H 라 딱 맞는다. 기울기가 0.3 이 아니면 그 차이만큼 **뒷면이 기운다**. 지어낸 것이 아니라 상수에서 따라 나오는 결과라 그대로 그린다. ⚠ **치수가 없는 것은 그리되 치수를 적지 않는다** — 막자갈(뒷채움)은 우리 식이 「입적에서 몸통·고임돌을 뺀 것」이라 폭·높이로 정의된 도형이 아니다. 자리만 보이고 값은 표를 가리킨다. 없는 치수를 그림에 적으면 **표와 다른 두 번째 정본**이 생긴다. """ from __future__ import annotations from typing import Any from B07_DesignDetail.B07_DesignDetail_Engine_Cad import ( _line_entity, _text_entity, polyline_entity, ) from B08_Quantity.B08_Quantity_Engine_UnitQuantity import STONE_MASONRY #: 그림 축척 — 1/25(1m = 40㎜). 표(줄 높이 9㎜) 위에 얹어도 한 면에 드는 크기다. SCALE_MM_PER_M = 40.0 #: 이 그림을 그리는 종류. 옹벽·집수정처럼 단면이 다른 것은 아직 그리지 않는다. FIGURE_TYPE_IDS: frozenset[str] = frozenset({"masonry_wet", "masonry_dry", "boulder_masonry"}) #: 기울기를 못 받았을 때 쓰는 값 — 교본 7-3 「돌 찰쌓기 3.0m 이하 1:0.3」. DEFAULT_SLOPE_RATIO = 0.3 _LABEL_FONT = 4.0 _DIM_FONT = 3.4 def slope_ratio_of(options: dict[str, Any] | None) -> tuple[float, bool]: """전면 기울기와 **그것이 입력된 값인지**. 비어 있으면 `(0.3, False)`.""" raw = (options or {}).get("face_slope_ratio") if isinstance(raw, (int, float)) and float(raw) > 0: return float(raw), True return DEFAULT_SLOPE_RATIO, False def section_points(height_m: float, slope_ratio: float) -> list[tuple[float, float]]: """벽 단면 네 점(m 단위, 밑면 앞끝이 원점). 앞면이 뒤로 `n·H` 기운다.""" top_t = STONE_MASONRY["thickness_base_m"] + STONE_MASONRY["thickness_top_coeff"] * height_m bottom_t = ( STONE_MASONRY["thickness_base_m"] + STONE_MASONRY["thickness_bottom_coeff"] * height_m ) lean = slope_ratio * height_m return [ (0.0, 0.0), (lean, height_m), (lean + top_t, height_m), (bottom_t, 0.0), (0.0, 0.0), ] def build_figure( drawing_id: str, sheet: dict[str, Any], layer_id: str, origin: tuple[float, float], line_color: str, label_color: str, guide_color: str, ) -> tuple[list[dict[str, Any]], float]: """장 하나의 그림. `(엔티티, 그림이 차지한 높이 ㎜)` — 못 그리면 `([], 0)`.""" if str(sheet.get("type_id") or "") not in FIGURE_TYPE_IDS: return [], 0.0 height_m = float(sheet.get("height_m") or 0.0) if height_m <= 0: return [], 0.0 options = sheet.get("options") or {} slope, slope_given = slope_ratio_of(options) scale = SCALE_MM_PER_M ox, oy = origin def mm(point: tuple[float, float]) -> tuple[float, float]: return (ox + point[0] * scale, oy + point[1] * scale) points = section_points(height_m, slope) top_t = STONE_MASONRY["thickness_base_m"] + STONE_MASONRY["thickness_top_coeff"] * height_m bottom_t = ( STONE_MASONRY["thickness_base_m"] + STONE_MASONRY["thickness_bottom_coeff"] * height_m ) average_t = (top_t + bottom_t) / 2.0 dig_width = average_t + STONE_MASONRY["excavation_extra_m"] backfill_t = STONE_MASONRY["backfill_thickness_m"] entities: list[dict[str, Any]] = [] wall = polyline_entity( f"{drawing_id}:fig:wall", [mm(p) for p in points], layer_id, line_color, width=2 ) if wall is not None: entities.append(wall) # 터파기 — 우리 수량식(평균두께+0.2) × 높이 를 **그대로** 그린다. 점선. dig = polyline_entity( f"{drawing_id}:fig:dig", [mm(p) for p in ((0.0, 0.0), (0.0, height_m), (dig_width, height_m), (dig_width, 0.0))], layer_id, guide_color, dash=[4, 3], ) if dig is not None: entities.append(dig) # 되메우기 두께 선 — 밑면에서 0.2m 위. 값이 어디서 오는지 눈에 보이게. entities.append( _line_entity( f"{drawing_id}:fig:backfill", mm((0.0, backfill_t)), mm((dig_width, backfill_t)), layer_id, guide_color, ) ) # 물구멍 — 벽을 가로지르는 짧은 선 하나. 개소 간격은 글자로 적는다(면적당이라 그림에 못 씀). weep_y = height_m * 0.5 entities.append( _line_entity( f"{drawing_id}:fig:weep", mm((slope * weep_y, weep_y)), mm((slope * weep_y + top_t, weep_y)), layer_id, guide_color, ) ) labels: list[tuple[str, tuple[float, float], str]] = [ (f"H = {height_m:g} m", (-0.28, height_m / 2.0), "right"), (f"상부 {top_t:.2f} m", (slope * height_m + top_t / 2.0, height_m + 0.14), "center"), (f"하부 {bottom_t:.2f} m", (bottom_t / 2.0, -0.30), "center"), ( f"1 : {slope:g}" + ("" if slope_given else " (자동 판정 전 — 교본 기본값)"), (slope * height_m / 2.0 - 0.30, height_m * 0.72), "right", ), ( f"터파기 폭 {dig_width:.2f} m (평균두께 {average_t:.2f} + 0.20)", (dig_width + 0.16, height_m * 0.92), "left", ), (f"되메우기 {backfill_t:g} m", (dig_width + 0.16, backfill_t), "left"), ( f"물구멍 — {STONE_MASONRY['weep_hole_area_m2']:g}㎡당 1개소", (slope * weep_y + top_t + 0.16, weep_y), "left", ), ("막자갈(뒷채움) — 수량은 아래 표", (dig_width + 0.16, height_m * 0.55), "left"), ] back = options.get("back_len_cm") or options.get("stone_back_length_cm") if back: labels.append((f"뒷길이 ℓ₃ = {int(back)}㎝", (dig_width + 0.16, height_m * 0.72), "left")) for index, (text, point, align) in enumerate(labels): x, y = mm(point) entities.append( _text_entity( f"{drawing_id}:fig:label:{index}", text, x, y, layer_id, _DIM_FONT, label_color, align, ) ) title_x, title_y = mm((0.0, height_m + 0.52)) entities.append( _text_entity( f"{drawing_id}:fig:title", f"{sheet.get('title') or '돌쌓기'} (축척 1/{int(1000 / SCALE_MM_PER_M)})", title_x, title_y, layer_id, _LABEL_FONT, label_color, "left", ) ) used_mm = (height_m + 0.95) * scale return entities, used_mm