Files
Aislo/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardFigure.py
T
eomsangdonandClaude Opus 5 e128f651fc feat(B07): 표준도 제원에 「기초」 칸 + 그림을 새 두께 식에 맞춤
**기초 칸**(`foundation` — 기초유 / 기초버림, 정본 xls 탭 제목 그대로)
터파기 기초 몫이 0.5×(0.7+0.2)=0.45 대 0.1×(0.7+0.0)=0.07 ㎥/m 로 갈리고, 횡단도
터파기 선도 같은 칸을 봄. 고를 자리가 어디에도 없어 비어 있던 값임.
정본에 없는 갈래를 넣으면 **막지 않고 알림**(기존 네 칸과 같은 결). 비우면 키를 지움.

**그림을 새 두께 식에 맞춤** — 데스크탑 메인이 `STONE_MASONRY` 를 갈면서
`thickness_base_m`·`thickness_top_coeff`·`thickness_bottom_coeff` 가 사라져
그림이 `KeyError` 로 죽었음(도면이 「읽지 못했습니다」로 뜸). 실화면에서 잡았음.
  상부 = 뒷길이 + 0.30 · 하부 = 상부 + 0.30 × (H − 1.0)
  H=1.0 ℓ45 → 0.75/0.75 · H=1.5 ℓ35 → 0.65/0.80 · H=2.0 ℓ35 → 0.65/0.95 ·
  H=2.0 ℓ45 → 0.75/1.05 · H=3.0 ℓ45 → 0.75/1.35   ← 실무 원본과 전부 일치

⚠ **뒷길이를 안 정한 장은 두께를 못 내므로 그리지 않음** — 0 으로 때우면 거짓 그림이 됨.

⚠ 배운 것 — 「그림이 상수를 직접 읽으니 저절로 따라온다」가 **키 이름이 바뀌면 깨진다**는
   것으로 드러남. 값은 한 벌이 맞으나 **이름이 바뀌는 것은 예고가 필요함.**

실화면 확인 — 「표준도 3장」에서 기초 「기초유」 저장 → 「2개소에 반영했습니다」,
정본 실측 `[기초유, null, 기초유, null, null]`(그 장의 두 개소에만).

자체검증 — 새 시험 3건 + 회귀 583 통과 · 0 실패, `tsc --noEmit` 0.

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

248 lines
9.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""표준도 **위쪽 그림** — 돌쌓기 단면(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