- 밑폭 2.2(기초 1.6 + 여유 0.3×2) · 윗폭 = 밑폭 + 0.5 × 판 깊이 × 2 = 2.9 · 깊이 0.7 → m당 터파기 1.54 → 1.785 · 되메우기 0.36 → 0.605 · 잔토 1.18 그대로 - 검증 프로젝트 내역 본체 124,202,681 → 124,424,547원(+221,866 = 구조물터파기 0~1m 15.4→17.85㎥ +213,375 · 되메우기 24.6→27.05㎥ +8,491) - 그림 터파기 점선을 사다리꼴로 · 「밑폭 2.20 · 윗폭 2.90 × 깊이 0.70」 · 표 사유와 같은 말 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
385 lines
17 KiB
Python
385 lines
17 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` 한 벌**을 읽는다 — 횡단도가 쓰는 그 상수다.
|
||
⚠ 뒷길이를 안 정한 장은 **표가 선 그 기본값으로 그리고 표 사유와 같은 말을 적는다**
|
||
(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(뒷길이). 「직경에서 두께를 내는 식」은 도메인 판단이라 사용자 몫.
|
||
#: ⓘ 옹벽(2026-09-14)은 **관측 원단위 줄에 실린 단면 치수**로 그림 — 표 수량이 나온 그 단면.
|
||
MASONRY_TYPE_IDS: frozenset[str] = frozenset({"masonry_wet", "masonry_dry"})
|
||
FIGURE_TYPE_IDS: frozenset[str] = MASONRY_TYPE_IDS | {"retaining_wall"}
|
||
|
||
#: 그림이 못 서는 장에 **까닭을 적는다** — 빈 자리를 그냥 두면 사용자가 「고장」으로 읽는다
|
||
#: (2026-09-09 사용자 지시). 「무엇을 받아야 서는지」까지 적는다.
|
||
_NO_FIGURE_REASONS: dict[str, str] = {
|
||
"boulder_masonry": (
|
||
"그림 없음 — 큰돌쌓기는 규격이 **직경**이라 두께를 낼 식이 아직 없습니다"
|
||
"(뒷길이로 내는 돌쌓기 식과 축이 다름 · 사용자 확정 대기)"
|
||
),
|
||
}
|
||
_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 type_id == "retaining_wall":
|
||
return _observed_section(sheet)[1]
|
||
if float(sheet.get("height_m") or 0.0) <= 0:
|
||
return "그림 없음 — 높이가 없습니다(제원에서 높이를 넣으면 그림이 섭니다)"
|
||
return None
|
||
|
||
|
||
def _observed_section(sheet: dict[str, Any]) -> tuple[dict[str, Any] | None, str | None]:
|
||
"""(관측 원단위 줄, 못 그리는 까닭) — 표가 줄을 꺼낸 **그 조회**(`OBSERVED_SPEC_KEYS`)를 씀.
|
||
|
||
표가 안 선 장은 표 사유와 같은 말 앞에 「그림 없음」만 붙임.
|
||
"""
|
||
from B08_Quantity.B08_Quantity_Engine_ObservedUnit import load_observed_table
|
||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import OBSERVED_SPEC_KEYS
|
||
from B08_Quantity.B08_Quantity_Wording import option_missing, spec_missing
|
||
|
||
type_id = str(sheet.get("type_id") or "")
|
||
keys = OBSERVED_SPEC_KEYS.get(type_id) or ()
|
||
options = sheet.get("options") or {}
|
||
spec = {key: options[key] for key in keys if options.get(key) is not None}
|
||
table = load_observed_table()
|
||
if not spec:
|
||
return None, f"그림 없음 — {option_missing(keys[0], type_id)}"
|
||
entry = table.find(type_id, spec)
|
||
if entry is None:
|
||
return None, f"그림 없음 — {spec_missing(type_id, table.specs_for(type_id))}"
|
||
if not entry.get("section"):
|
||
return None, "그림 없음 — 이 규격은 자료에 수량만 있고 단면 치수가 없습니다"
|
||
return entry, 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], rubble_thickness_m: float | None = None
|
||
) -> list[dict[str, Any]] | None:
|
||
"""장 하나의 그림 — 모양 목록(밑면 앞끝이 원점). 못 그리면 `None`(까닭은 `figure_reason`).
|
||
|
||
`rubble_thickness_m` — 산출 조건 기초잡석 두께(표의 기초잡석 줄과 같은 값 · 비면 기본).
|
||
"""
|
||
if figure_reason(sheet) is not None:
|
||
return None
|
||
if sheet.get("type_id") == "retaining_wall":
|
||
entry, _reason = _observed_section(sheet)
|
||
return _wall_figure(sheet, entry or {}, rubble_thickness_m)
|
||
return _masonry_figure(sheet)
|
||
|
||
|
||
def _masonry_figure(sheet: dict[str, Any]) -> list[dict[str, Any]]:
|
||
"""돌쌓기 단면 — 두께는 수량표 함수 한 벌."""
|
||
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 _wall_figure(
|
||
sheet: dict[str, Any], entry: dict[str, Any], rubble_thickness_m: float | None
|
||
) -> list[dict[str, Any]]:
|
||
"""옹벽 단면 — 관측 원단위 줄의 `section`(표 수량이 나온 그 단면)으로 그림.
|
||
|
||
x 는 앞굽 끝이 0(앞면 왼쪽), y 는 기초 밑면이 0. 버림·기초잡석은 기초 밑, 전단키가 뚫고 내려감.
|
||
⚠ 기초잡석 두께는 표의 기초잡석 줄과 같은 산출 조건 값(비면 기본) — 폭은 버림과 같음.
|
||
"""
|
||
from B08_Quantity.B08_Quantity_Engine_ObservedUnit import earthwork_missing_note
|
||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import RUBBLE_BASE_THICKNESS_M
|
||
|
||
section = entry["section"]
|
||
footing, wall, key = section["footing"], section["wall"], section["key"]
|
||
blinding, weep = section["blinding"], section["weep"]
|
||
f_w, f_t, toe = footing["width_m"], footing["thickness_m"], footing["toe_m"]
|
||
w_h, w_top = wall["height_m"], wall["top_m"]
|
||
front, back = wall["front_batter_m"], wall["back_batter_m"]
|
||
w_bottom = w_top + front + back
|
||
heel = f_w - toe - w_bottom
|
||
k_w, k_d = key["width_m"], key["depth_m"]
|
||
k_left = f_w - key["from_heel_m"] - k_w
|
||
b_t, over = blinding["thickness_m"], blinding["overhang_m"]
|
||
rubble = RUBBLE_BASE_THICKNESS_M if rubble_thickness_m is None else float(rubble_thickness_m)
|
||
top = f_t + w_h
|
||
|
||
shapes = [
|
||
_path(
|
||
[
|
||
(0.0, 0.0),
|
||
(0.0, f_t),
|
||
(toe, f_t),
|
||
(toe + front, top),
|
||
(toe + front + w_top, top),
|
||
(toe + w_bottom, f_t),
|
||
(f_w, f_t),
|
||
(f_w, 0.0),
|
||
(k_left + k_w, 0.0),
|
||
(k_left + k_w, -k_d),
|
||
(k_left, -k_d),
|
||
(k_left, 0.0),
|
||
(0.0, 0.0),
|
||
],
|
||
"wall",
|
||
),
|
||
]
|
||
# 버림(키 자리는 비움) · 그 밑 기초잡석(같은 폭).
|
||
layers = [(0.0, b_t, False)] + ([(b_t, rubble, True)] if rubble > 0 else [])
|
||
for depth, thickness, dash in layers:
|
||
for x0, x1 in ((-over, k_left), (k_left + k_w, f_w + over)):
|
||
shapes.append(
|
||
_path(
|
||
[
|
||
(x0, -depth),
|
||
(x0, -depth - thickness),
|
||
(x1, -depth - thickness),
|
||
(x1, -depth),
|
||
],
|
||
"guide",
|
||
dash=dash,
|
||
)
|
||
)
|
||
weep_y = f_t + w_h * 0.5
|
||
ratio = (weep_y - f_t) / w_h
|
||
shapes.append(
|
||
_path([(toe + front * ratio, weep_y), (toe + w_bottom - back * ratio, weep_y)], "guide")
|
||
)
|
||
# 터파기 — 표 줄과 같은 식(기초 폭 + 양쪽 여유 · 수직 · 기초 윗면에서 기초잡석 밑까지). 점선.
|
||
trench = section.get("trench")
|
||
clearance = trench["clearance_m"] if trench else 0.0
|
||
side = (trench or {}).get("side_slope", 0.0) * (f_t + b_t + rubble)
|
||
dig_bottom = -(b_t + rubble)
|
||
if trench:
|
||
shapes.append(
|
||
_path(
|
||
[
|
||
(-clearance - side, f_t),
|
||
(-clearance, dig_bottom),
|
||
(f_w + clearance, dig_bottom),
|
||
(f_w + clearance + side, f_t),
|
||
],
|
||
"guide",
|
||
dash=True,
|
||
)
|
||
)
|
||
|
||
right = max(f_w + over, f_w + clearance + side) + 0.2
|
||
weep_area, weep_dia, _weep_basis = weep_hole_spec(sheet.get("options") or {})
|
||
blinding_width = f_w + 2 * over - k_w
|
||
shapes += [
|
||
_text(
|
||
f"{sheet.get('title') or '옹벽'} {sheet.get('options', {}).get('form') or ''}"
|
||
f" (축척 1/{int(1000 / SCALE_MM_PER_M)})",
|
||
(0.0, top + 0.52),
|
||
"left",
|
||
_LABEL_SIZE,
|
||
),
|
||
_text(f"H = {top:g} m", (-0.28, top / 2.0), "right"),
|
||
_text(f"상단 {w_top:.2f} m", (toe + front + w_top / 2.0, top + 0.14), "center"),
|
||
_text(
|
||
f"벽체 높이 {w_h:.2f} · 하단 {w_bottom:.2f} m (앞면 {front:g} · 뒷면 {back:g} 기움)",
|
||
(right, top * 0.9),
|
||
"left",
|
||
),
|
||
_text(
|
||
f"물구멍 Ø{weep_dia} — {weep_area:g}㎡당 1개소"
|
||
f" · 관 {weep['pipe_length_m']:g} m(관측값)",
|
||
(right, weep_y),
|
||
"left",
|
||
),
|
||
_text(
|
||
f"기초 {f_w:.2f} × {f_t:.2f} m (앞굽 {toe:.2f} · 뒷굽 {heel:.2f})",
|
||
(right, f_t / 2.0),
|
||
"left",
|
||
),
|
||
_text(
|
||
f"버림 T={b_t:.2f} · 폭 {f_w + 2 * over:.2f} − 키 {k_w:.2f} = {blinding_width:.2f} m",
|
||
(right, -b_t / 2.0),
|
||
"left",
|
||
),
|
||
_text(f"전단키 {k_w:.2f} × {k_d:.2f} m", (k_left + k_w / 2.0, -k_d - 0.14), "center"),
|
||
]
|
||
if rubble > 0:
|
||
shapes.append(
|
||
_text(
|
||
f"기초잡석 T={rubble:g} m — 산출 조건 · 폭은 버림과 같음",
|
||
(right, -b_t - rubble / 2.0),
|
||
"left",
|
||
)
|
||
)
|
||
if trench:
|
||
shapes.append(
|
||
_text(
|
||
f"터파기 밑폭 {f_w + 2 * clearance:.2f} · 윗폭 {f_w + 2 * (clearance + side):.2f}"
|
||
f" × 깊이 {f_t - dig_bottom:.2f} m (기초 {f_w:g} + 여유 {clearance:g}×2"
|
||
f" · 옆면 1:{trench.get('side_slope', 0.0):g})",
|
||
(right, f_t + 0.25),
|
||
"left",
|
||
)
|
||
)
|
||
below = -max(k_d, b_t + rubble) - 0.36
|
||
notes = [
|
||
f"단면 — 관측 원단위 {entry.get('source_note') or ''}의 수량이 나온 그 단면",
|
||
earthwork_missing_note(entry),
|
||
]
|
||
for index, note in enumerate(note for note in notes if note):
|
||
shapes.append(_text(note, (0.0, below - index * 0.16), "left"))
|
||
return shapes
|
||
|
||
|
||
def attach_figures(payload: dict[str, Any], rubble_thickness_m: float | None = None) -> None:
|
||
"""장마다 `figure`(모양 목록 또는 `None`)와 `figure_reason` 을 실음 — 화면은 둘 중 하나."""
|
||
for sheet in payload.get("sheets") or []:
|
||
sheet["figure_reason"] = figure_reason(sheet)
|
||
sheet["figure"] = build_figure(sheet, rubble_thickness_m)
|