Files
Aislo/B08_Quantity/B08_Quantity_Engine_StructureFigure.py
T
eomsangdonandClaude Opus 5 a9ddcd5b1b feat(b08): 구조물도 그림 — 뒷길이 미정 장도 표가 선 기본값으로 그림 · 표 사유와 같은 말
- 뒷길이를 안 고른 돌쌓기 장: 표가 쓴 그 뒷길이(양식 장은 양식이 푼 값, 전개 장은 기본 45㎝)로 그림
- 「⚠ 뒷길이를 안 골라 기본 N㎝ 로 섰습니다」 한 줄을 한 곳에 두고 표 사유·그림이 같이 씀(골막이 문구도 같은 함수)
- 돌쌓기 표 사유에 그 한 줄이 새로 붙음 — 전에는 기본값인 줄 표에 안 드러났음
- 그림 까닭은 정말 못 그리는 장(큰돌쌓기 직경 축·옹벽)만

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
2026-09-14 04:42:28 +09:00

196 lines
9.3 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.
"""구조물도 **상단 그림** — 돌쌓기 단면 (PLAN 12장 · 3장 한 장의 짜임 「상단 그림」).
⚠ **2026-09-14 B07 에서 이관** — 옛 자리 `B07_DesignDetail_Engine_Cad_StandardFigure.py`.
CAD 엔티티 대신 **모양 목록(선·글자, m 단위, 위가 +y)** 을 냄 — 화면이 SVG 로 그리고(읽기 전용),
CAD 는 뒤 일감이 같은 목록을 받아 씀(B07 `StandardSheet` 가 이미 옮겨 적음).
⚠ **기울기를 값으로 받는다 — 0.3 을 박지 않는다.**
판정은 B08 `slope_of()` 한 벌(품셈 13-4-4 [주]⑪ 표준경사 — 직고·메찰·성절토). 사용자가 정했으면
그 값이 이긴다(확정 ⑨). 그림은 **수량이 쓴 바로 그 값**으로 기울고 근거 문구도 같이 적는다.
⚠ **두께는 수량표가 쓰는 그 함수**(`UnitQuantity.wall_thickness`) — 사용자가 넣은 상부·하부 두께가
식을 이긴다. 옛 B07 그림은 식을 따로 적어 **두께를 넣어도 그림만 옛 두께**로 섰음(이관 때 잡음).
상부 두께 = 뒷길이 + 0.30 하부 두께 = 상부 + 0.30 × (H 1.0)
터파기 폭 = 평균두께 + 0.2 기초 0.5×0.9 (기초유) · 0.1×0.7 (기초버림)
⚠ 터파기 치수는 **`common_util_excavation` 한 벌**을 읽는다 — 횡단도가 쓰는 그 상수다.
⚠ 뒷길이를 안 정한 장은 **표가 선 그 기본값으로 그리고 표 사유와 같은 말을 적는다**
(2026-09-14 브레인 판정 — 표가 서면 그림도 섬). 까닭은 정말 못 그리는 장(큰돌쌓기 등)만.
⚠ **치수가 없는 것은 그리되 치수를 적지 않는다** — 막자갈(뒷채움)은 우리 식이 「입적에서
몸통·고임돌을 뺀 것」이라 폭·높이로 정의된 도형이 아니다. 자리만 보이고 값은 표를 가리킨다.
"""
from __future__ import annotations
from typing import Any
from B08_Quantity.B08_Quantity_Engine_StructureSheet import slope_of
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import (
_back_length,
back_length_default_note,
wall_thickness,
weep_hole_spec,
)
from common_util.common_util_excavation import (
WALL_BLINDING_DEPTH_M,
WALL_BLINDING_WIDTH_M,
WALL_FOUNDATION_DEPTH_M,
WALL_FOUNDATION_WIDTH_M,
WALL_TRENCH_CLEARANCE_M,
)
#: 그림 축척 — 1/25(1m = 40㎜). CAD 로 옮길 때 글자 크기(㎜)를 m 로 바꾸는 데도 씀.
SCALE_MM_PER_M = 40.0
#: 이 그림을 그리는 종류. 옹벽·집수정처럼 단면이 다른 것은 아직 그리지 않는다.
#: ⚠ **큰돌쌓기(`boulder_masonry`)를 뺐다**(2026-09-09). 그림이 두께를 **뒷길이**에서 내는데
#: 큰돌쌓기는 규격 축이 **직경**(`stone_cm`)이라 **영영 못 그린다** — 품셈 13-6(직경) ↔
#: 13-4(뒷길이). 「직경에서 두께를 내는 식」은 도메인 판단이라 사용자 몫.
FIGURE_TYPE_IDS: frozenset[str] = frozenset({"masonry_wet", "masonry_dry"})
#: 그림이 못 서는 장에 **까닭을 적는다** — 빈 자리를 그냥 두면 사용자가 「고장」으로 읽는다
#: (2026-09-09 사용자 지시). 「무엇을 받아야 서는지」까지 적는다.
_NO_FIGURE_REASONS: dict[str, str] = {
"boulder_masonry": (
"그림 없음 — 큰돌쌓기는 규격이 **직경**이라 두께를 낼 식이 아직 없습니다"
"(뒷길이로 내는 돌쌓기 식과 축이 다름 · 사용자 확정 대기)"
),
"retaining_wall": "그림 없음 — 옹벽은 단면이 달라 아직 그리지 않습니다",
}
_NO_FIGURE_DEFAULT = "그림 없음 — 이 형식은 아직 단면을 그리지 않습니다"
#: 글자 크기(m) — 종이 ㎜ 를 축척으로 나눈 값(옛 CAD 그림과 같은 크기).
_LABEL_SIZE = 4.0 / SCALE_MM_PER_M
_DIM_SIZE = 3.4 / SCALE_MM_PER_M
def back_length_cm(sheet: dict[str, Any]) -> tuple[int, str | None]:
"""(표가 쓴 뒷길이 ㎝, 기본으로 섰으면 표 사유와 같은 한 줄).
양식 장은 양식이 푼 값(`formula_sheet.vars.L3`), 전개 장은 `_back_length` — 표와 한 벌.
"""
options = sheet.get("options") or {}
solved = ((sheet.get("formula_sheet") or {}).get("vars") or {}).get("L3")
back = int(float(solved)) if isinstance(solved, (int, float)) else _back_length(options)
return back, back_length_default_note(options, back)
def figure_reason(sheet: dict[str, Any]) -> str | None:
"""그림이 **안 서는 까닭** 한 줄. 서는 장이면 `None`."""
type_id = str(sheet.get("type_id") or "")
if type_id not in FIGURE_TYPE_IDS:
return _NO_FIGURE_REASONS.get(type_id, _NO_FIGURE_DEFAULT)
if float(sheet.get("height_m") or 0.0) <= 0:
return "그림 없음 — 높이가 없습니다(제원에서 높이를 넣으면 그림이 섭니다)"
return None
def _path(points: list[tuple[float, float]], role: str, *, dash: bool = False) -> dict[str, Any]:
return {"kind": "path", "points": [list(p) for p in points], "role": role, "dash": dash}
def _text(
text: str, at: tuple[float, float], align: str, size: float = _DIM_SIZE
) -> dict[str, Any]:
return {"kind": "text", "text": text, "at": list(at), "align": align, "size": size}
def build_figure(sheet: dict[str, Any]) -> list[dict[str, Any]] | None:
"""장 하나의 그림 — 모양 목록(밑면 앞끝이 원점). 못 그리면 `None`(까닭은 `figure_reason`)."""
if figure_reason(sheet) is not None:
return None
options = sheet.get("options") or {}
height_m = float(sheet.get("height_m") or 0.0)
back_cm, default_note = back_length_cm(sheet)
slope, slope_note = slope_of(sheet)
top_t, bottom_t, _basis = wall_thickness(options, back_cm=back_cm, height_m=height_m)
lean = slope * height_m
average_t = (top_t + bottom_t) / 2.0
dig_width = average_t + WALL_TRENCH_CLEARANCE_M
# 기초 몫 — 「기초유 / 기초버림」이 폭·깊이를 가른다(정본 탭 제목). 안 정한 장은 안 그린다.
foundation = str(options.get("foundation") or "")
base_w, base_d = {
"기초유": (WALL_FOUNDATION_WIDTH_M, WALL_FOUNDATION_DEPTH_M),
"기초버림": (WALL_BLINDING_WIDTH_M, WALL_BLINDING_DEPTH_M),
}.get(foundation, (0.0, 0.0))
shapes = [
_path(
[(0.0, 0.0), (lean, height_m), (lean + top_t, height_m), (bottom_t, 0.0), (0.0, 0.0)],
"wall",
),
# 터파기 — 수량식 H × (평균두께 + 0.2) 와 **넓이가 같은** 평행사변형(앞면 따라 기움).
_path(
[
(0.0, 0.0),
(lean, height_m),
(lean + dig_width, height_m),
(dig_width, 0.0),
(0.0, 0.0),
],
"guide",
dash=True,
),
]
if base_d > 0:
shapes.append(
_path([(0.0, 0.0), (0.0, -base_d), (base_w, -base_d), (base_w, 0.0)], "guide")
)
# 물구멍 — 벽을 가로지르는 짧은 선. 값은 **그 장의 제원**에서(수량표와 같은 함수).
weep_area_m2, weep_diameter_mm, _weep_basis = weep_hole_spec(options)
weep_y = height_m * 0.5
shapes.append(_path([(slope * weep_y, weep_y), (slope * weep_y + top_t, weep_y)], "guide"))
right = max(lean + dig_width, base_w) + 0.16
shapes += [
_text(f"H = {height_m:g} m", (-0.28, height_m / 2.0), "right"),
_text(f"상부 {top_t:.2f} m", (lean + top_t / 2.0, height_m + 0.14), "center"),
_text(f"하부 {bottom_t:.2f} m", (bottom_t / 2.0, -0.30 - base_d), "center"),
_text(f"1 : {slope:g}", (lean / 2.0 - 0.30, height_m * 0.72), "right"),
_text(
f"터파기 폭 {dig_width:.2f} m "
f"(평균두께 {average_t:.2f} + {WALL_TRENCH_CLEARANCE_M:g})",
(right, height_m * 0.92),
"left",
),
_text(
f"뒷길이 ℓ₃ = {back_cm}{' (기본)' if default_note else ''}",
(right, height_m * 0.72),
"left",
),
_text("막자갈(뒷채움) — 수량은 아래 표", (right, height_m * 0.55), "left"),
_text(
f"물구멍 Ø{weep_diameter_mm}{weep_area_m2:g}㎡당 1개소",
(right, weep_y - 0.2),
"left",
),
_text(
f"기초 {base_w:g} × {base_d:g} m ({foundation})"
if base_d > 0
else "기초 — 안 정함(기초유/기초버림)",
(right, -base_d / 2.0 if base_d else 0.0),
"left",
),
# 기울기 근거는 그림 밑에 한 줄 — 「사용자 지정」인지 「품셈 표준경사」인지 보여야 한다.
_text(f"전면 기울기 — {slope_note}", (0.0, -0.62 - base_d), "left"),
_text(
f"{sheet.get('title') or '돌쌓기'} (축척 1/{int(1000 / SCALE_MM_PER_M)})",
(0.0, height_m + 0.52),
"left",
_LABEL_SIZE,
),
]
if default_note:
# 「이 값이 어디서 왔나」 — 표 사유 줄과 **글자까지 같게**(브레인 판정).
shapes.append(_text(default_note, (0.0, -0.80 - base_d), "left"))
return shapes
def attach_figures(payload: dict[str, Any]) -> None:
"""장마다 `figure`(모양 목록 또는 `None`)와 `figure_reason` 을 실음 — 화면은 둘 중 하나."""
for sheet in payload.get("sheets") or []:
sheet["figure_reason"] = figure_reason(sheet)
sheet["figure"] = build_figure(sheet)