190 lines
9.0 KiB
Python
190 lines
9.0 KiB
Python
"""구조물도 **상단 그림** — 돌쌓기 단면 (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` 한 벌**을 읽는다 — 횡단도가 쓰는 그 상수다.
|
||
⚠ 뒷길이를 안 정한 장은 **그리지 않고 까닭을 적는다**(브레인 판정 — 빈칸 대신 까닭).
|
||
|
||
⚠ **치수가 없는 것은 그리되 치수를 적지 않는다** — 막자갈(뒷채움)은 우리 식이 「입적에서
|
||
몸통·고임돌을 뺀 것」이라 폭·높이로 정의된 도형이 아니다. 자리만 보이고 값은 표를 가리킨다.
|
||
"""
|
||
|
||
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_KEYS,
|
||
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]) -> float | None:
|
||
"""장의 뒷길이(㎝). 안 정했으면 `None` — 그때는 그리지 않는다."""
|
||
options = sheet.get("options") or {}
|
||
raw = next(
|
||
(options[key] for key in BACK_LENGTH_KEYS if options.get(key) not in (None, "")), None
|
||
)
|
||
try:
|
||
return float(raw) if raw is not None else None
|
||
except (TypeError, ValueError):
|
||
return None
|
||
|
||
|
||
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 "그림 없음 — 높이가 없습니다(제원에서 높이를 넣으면 그림이 섭니다)"
|
||
if back_length_cm(sheet) is None:
|
||
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 = back_length_cm(sheet) or 0.0
|
||
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:g}㎝", (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,
|
||
),
|
||
]
|
||
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)
|