Files
Aislo/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardFigure.py
T
eomsangdonandClaude Opus 5 918e7c12da fix(B07): 표준도가 단면유형을 안 넘겨 기울기 판정이 통째로 꺼져 있던 것
표준도가 `build_table` 을 직접 부르면서 `section_modes` 를 안 넘겨, 성절토를 가를 근거가
없다며 **전 구조물이 종전값 1:0.3** 으로 섰음. 값이 없는 게 아니라 **안 넘긴 것**이었음.

`section_modes_from_designs` 한 벌을 그대로 씀(부르는 쪽마다 다시 짜면 B08 과 갈림).
노선은 `get_workflow_route_context` 로 찾고, 못 찾으면 빈 표 — 성토로 눅이지 않음.

⚠ 고치고 나니 **제목·그림이 표와 갈렸음** — 표는 판정된 1:0.35, 제목은 다시 판정하며
근거를 못 받아 1:0.3. 실측으로 잡았음. 그래서 **표를 만든 그 판정을 시트에 실어**
제목·그림이 같은 값을 쓰게 함(`face`·`face_reason`). 자리를 고르는 것도 B08 이 쓰는
`_section_mode_at` 과 판정 한 벌 `structure_face_role` 을 그대로 부름.

실측 (프로젝트 936be972) — 제목과 표가 **같은 값**
```
큰돌쌓기  H=2.5  제목 1:0.3   표 0.3    면적 2.6101
돌쌓기(메) H=2.0  제목 1:0.35  표 0.35   면적 2.1190   ← 메쌓기가 갈림
돌쌓기(찰) H=2.5  제목 1:0.3   표 0.3    면적 2.6101
옹벽      H=2.0  기울기 없음(판정 대상 아님)
```
표 산출근거에 판정 경로가 그대로 뜸 — 「품셈 13-4-4 [주]⑪ 표준경사 · 메쌓기 성토 ·
직고 2m ≤3m → 1:0.35 · right_cut · 자동(성토 쪽) → 성토면」.

앞서 `face_slope_ratio({}, wet=…)` 로 확인한 「근거 없음」은 `face` 를 안 준 호출이라
**정상 동작**이었음 — 함수가 성토로 안 눅이게 짠 결과.

자체검증 — 회귀 580 통과 · 0 실패. 세 라우터 모두 700줄 안(588 · 190 · 692).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 21:01:46 +09:00

230 lines
8.7 KiB
Python

"""표준도 **위쪽 그림** — 돌쌓기 단면(2단계, 2026-09-09).
실무 원본 탭이 「위 치수조서 + 아래 수량산출서」인데, 아래 표는 1단계에서 섰고 여기가 위다.
⚠ **기울기를 값으로 받는다 — 0.3 을 박지 않는다.**
판정은 **B08 `face_slope_ratio()` 한 벌**을 그대로 부른다(품셈 13-4-4 [주]⑪ 표준경사 —
직고·메찰·성절토). 사용자가 정했으면 그 값이 이긴다(확정 ⑨). 그림은 **수량이 쓴 바로
그 값**으로 기울고, 근거 문구도 같이 받아 그림에 적는다 — 판정을 두 벌로 짜면 그림과
수량이 갈린다.
⚠ **수량이 쓰는 그 상수로 그린다** — `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, 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 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_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)
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}", (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,
)
)
# 기울기 근거는 **그림 밑에** 한 줄 — 「사용자 지정」인지 「품셈 표준경사」인지 보여야 한다.
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