Files
Aislo/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardFigure.py
T
eomsangdonandClaude Opus 5 8cd0b003e5 feat(B07): 표준도 2단계 — 돌쌓기 단면 그림 + 제원 조합마다 한 장
실무 탭이 「위 치수조서 + 아래 수량산출서」인데 1단계에서 아래 표가 섰고 이번이 위 그림.
그림이 붙으면서 한 면에 넉 장을 쌓을 수 없게 돼 **장 나눔(㉢)도 같이** 했음.

장 나눔 — 계획평면도가 쓰는 본 그대로
  `standard_sheet` / `standard_sheet_2` … 제원 조합 하나가 한 장.
  좌측 목록은 `idPrefix` 묶음(계획평면도와 같은 짜임)이고 kind 는 새로 `standard`.
  구조물이 없어도 한 장은 남김 — 단추가 사라지면 「없어진 것」처럼 보이고 사유를 읽을
  자리도 없어짐.

그림 — `B07_DesignDetail_Engine_Cad_StandardFigure.py`
  ⚠ **기울기를 값으로 받음. 0.3 을 그림에 박지 않았음.** `face_slope_ratio` 가 오면 그
  값으로 기울고, 비어 있으면 교본 7-3 기본 0.3 으로 그리되 **「자동 판정 전」이라고 그림에
  적음**. 자동 판정이 붙는 대로 그림이 따라감.
  ⚠ 치수는 수량이 쓰는 그 상수(`STONE_MASONRY`)를 직접 읽음 — 여기서 다시 적으면 그림과
  표가 갈림. 상부 0.45+0.10H · 하부 0.45+0.40H · 터파기 폭 평균두께+0.2 · 되메우기 0.2.
  ⓘ 그 상수는 **뒷면이 수직인 1:0.3 벽**을 뜻함(하부−상부 = 0.30H = 전면이 기우는 양).
    기울기가 0.3 이 아니면 그 차이만큼 뒷면이 기욺 — 상수에서 따라 나오는 결과라 그대로 그림.
  ⚠ 치수가 없는 것은 그리되 치수를 안 적음 — 막자갈은 「입적 − 몸통 − 고임돌」이라 폭·높이로
    정의된 도형이 아님. 자리만 보이고 값은 표를 가리킴.

죽은 길 걷어냄 — `BLANK_DRAWINGS` 가 「표준도」 하나뿐이었는데 그것이 내용 있는 장이 되어
빈 도각 경로가 통째로 안 쓰이게 됨. 상수·목록 루프·분기·import 를 지움(700줄 제한도 걸렸음).

자체검증 — 회귀 573 통과 · 0 실패, `tsc --noEmit` 0.
⚠ 병합 뒤 깨진 시험 둘을 고침(내 폴더 `tmp/tests`, 랩탑 메인 `11394d40` 의 여파)
  · `stone_supply='채집'` 기본값을 확정 ② 근거와 함께 허용 목록에 등록
  · 바닥막이 「모든 옵션이 b05」 단정 → 조달이 detail 로 들어와 B05 셋만 보게 좁힘

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

204 lines
7.7 KiB
Python

"""표준도 **위쪽 그림** — 돌쌓기 단면(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