"""표준도 **위쪽 그림** — 돌쌓기 단면(2단계, 2026-09-09). 실무 원본 탭이 「위 치수조서 + 아래 수량산출서」인데, 아래 표는 1단계에서 섰고 여기가 위다. ⚠ **기울기를 값으로 받는다 — 0.3 을 박지 않는다.** 판정은 **B08 `face_slope_ratio()` 한 벌**을 그대로 부른다(품셈 13-4-4 [주]⑪ 표준경사 — 직고·메찰·성절토). 사용자가 정했으면 그 값이 이긴다(확정 ⑨). 그림은 **수량이 쓴 바로 그 값**으로 기울고, 근거 문구도 같이 받아 그림에 적는다 — 판정을 두 벌로 짜면 그림과 수량이 갈린다. ⚠ **수량이 쓰는 그 상수로 그린다** — `B08_Quantity_Engine_UnitQuantity.STONE_MASONRY` 를 직접 읽는다. 치수를 여기서 다시 적으면 **그림과 표가 갈린다**(CLAUDE.md 5장). 상부 두께 = 뒷길이 + 0.30 하부 두께 = 상부 + 0.30 × (H − 1.0) 터파기 폭 = 평균두께 + 0.2 되메우기 두께 = 0.2 ⚠ **뒷길이가 두께에 들어간다**(확정 2차 ②, 실무 구조물도 식). 뒷길이가 다르면 벽이 두꺼워지고 그림도 그만큼 넓어진다 — 예전 식(0.45+0.10H / 0.45+0.40H)은 뒷길이를 아예 안 봐서 35㎝ 든 45㎝ 든 같은 그림이 나왔다. ⚠ 뒷길이를 안 정한 장은 두께를 못 낸다 — 그때는 **그리지 않는다**(0 으로 때우면 거짓 그림이 된다). ⚠ **치수가 없는 것은 그리되 치수를 적지 않는다** — 막자갈(뒷채움)은 우리 식이 「입적에서 몸통·고임돌을 뺀 것」이라 폭·높이로 정의된 도형이 아니다. 자리만 보이고 값은 표를 가리킨다. 없는 치수를 그림에 적으면 **표와 다른 두 번째 정본**이 생긴다. """ 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, face_slope_ratio #: 그림 축척 — 1/25(1m = 40㎜). 표(줄 높이 9㎜) 위에 얹어도 한 면에 드는 크기다. SCALE_MM_PER_M = 40.0 #: 이 그림을 그리는 종류. 옹벽·집수정처럼 단면이 다른 것은 아직 그리지 않는다. FIGURE_TYPE_IDS: frozenset[str] = frozenset({"masonry_wet", "masonry_dry", "boulder_masonry"}) _LABEL_FONT = 4.0 _DIM_FONT = 3.4 def slope_of(sheet: dict[str, Any]) -> tuple[float, str]: """장 하나의 전면 기울기와 근거 문구 — **판정은 B08 한 벌**을 그대로 쓴다. 메/찰은 종류에서 온다. 큰돌쌓기는 `bond` 칸(메쌓기/찰쌓기)이 그것이고, 안 고르면 찰쌓기로 본다 — 품셈 13-6 은 둘 다 「1:0.3 **이상**」이라 그림 기울기가 갈리지 않는다. """ options = sheet.get("options") or {} type_id = str(sheet.get("type_id") or "") if type_id == "masonry_dry": wet = False elif type_id == "boulder_masonry": wet = options.get("bond") != "메쌓기" else: wet = True return face_slope_ratio( options, wet=wet, height_m=float(sheet.get("height_m") or 0.0), # ⚠ 표를 만든 그 판정을 그대로 넘긴다 — 여기서 다시 가르면 근거를 못 받아 # 종전값으로 떨어지고 **표와 갈린다**(2026-09-09 실측). face=sheet.get("face"), face_reason=str(sheet.get("face_reason") or ""), ) def wall_thickness(height_m: float, back_cm: float) -> tuple[float, float]: """(상부, 하부) 두께 — **수량이 쓰는 그 식**(`stone_masonry`)과 같은 상수를 읽는다.""" top_t = back_cm / 100.0 + STONE_MASONRY["thickness_top_add_m"] bottom_t = top_t + STONE_MASONRY["thickness_slope_per_m"] * max( height_m - STONE_MASONRY["thickness_height_base_m"], 0.0 ) return top_t, bottom_t def back_length_cm(sheet: dict[str, Any]) -> float | None: """장의 뒷길이(㎝). 안 정했으면 `None` — 그때는 두께를 못 내므로 그리지 않는다.""" options = sheet.get("options") or {} raw = options.get("back_len_cm") or options.get("stone_back_length_cm") try: return float(raw) if raw not in (None, "") else None except (TypeError, ValueError): return None def section_points( height_m: float, slope_ratio: float, back_cm: float ) -> list[tuple[float, float]]: """벽 단면 네 점(m 단위, 밑면 앞끝이 원점). 앞면이 뒤로 `n·H` 기운다.""" top_t, bottom_t = wall_thickness(height_m, back_cm) 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 back_cm = back_length_cm(sheet) if back_cm is None: # 뒷길이가 없으면 두께를 못 낸다 — 0 으로 때우지 않고 그리지 않는다. return [], 0.0 slope, slope_note = slope_of(sheet) 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, back_cm) top_t, bottom_t = wall_thickness(height_m, back_cm) 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}", (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"), ] labels.append((f"뒷길이 ℓ₃ = {int(back_cm)}㎝", (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, ) ) # 기울기 근거는 **그림 밑에** 한 줄 — 「사용자 지정」인지 「품셈 표준경사」인지 보여야 한다. note_x, note_y = mm((0.0, -0.62)) entities.append( _text_entity( f"{drawing_id}:fig:slopenote", f"전면 기울기 — {slope_note}", note_x, note_y, layer_id, _DIM_FONT, label_color, "left", ) ) 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