계획서 4-12. **빈 자리를 그냥 두면 사용자가 「고장인가」로 읽음** — 장마다 왜 그림이 없는지와 **무엇을 받아야 서는지**를 표 위에 한 줄 적음. - 돌쌓기(찰) — 「뒷길이를 고르면 그림이 섭니다」 · 옹벽 — 「단면이 달라 아직 안 그림」 ⚠ **큰돌쌓기를 `FIGURE_TYPE_IDS` 에서 뺌** — 그림은 두께를 뒷길이에서 내는데 큰돌쌓기는 규격이 **직경**이라 목록에만 있고 결과는 늘 빈 그림이었음(품셈 13-6 직경 ↔ 13-4 뒷길이, 수량 쪽에서 이미 갈라 둔 자리). **식은 만들지 않음** — 「직경에서 두께를 내는 법」은 도메인 판단이라 사용자 몫이고, 지금은 사유만 띄움. ⚠ 곁가지 하나를 화면에서 잡아 함께 고침 — 그림 대상 목록을 **기울기 판정**에도 쓰고 있어 큰돌쌓기를 빼자 **장 제목의 「1:0.3」까지 사라졌음**. `SLOPE_TYPE_IDS` 로 갈라 되살림. 화면 실측 — 표준도 1장에 「그림 없음 — 큰돌쌓기는 규격이 직경이라 …」 한 줄이 표 위에 뜸. 시험 다섯 추가, 전체 1225 통과·실패 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
309 lines
13 KiB
Python
309 lines
13 KiB
Python
"""표준도 **위쪽 그림** — 돌쌓기 단면(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.5×0.9 (기초유) · 0.1×0.7 (기초버림)
|
||
|
||
⚠ 터파기 치수는 **`common_util_excavation` 한 벌**을 읽는다 — 횡단도가 쓰는 그 상수다.
|
||
여기서 다시 적으면 두 도면이 다른 터파기를 그린다.
|
||
|
||
⚠ **뒷길이가 두께에 들어간다**(확정 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
|
||
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㎜). 표(줄 높이 9㎜) 위에 얹어도 한 면에 드는 크기다.
|
||
SCALE_MM_PER_M = 40.0
|
||
|
||
#: 이 그림을 그리는 종류. 옹벽·집수정처럼 단면이 다른 것은 아직 그리지 않는다.
|
||
#: ⚠ **큰돌쌓기(`boulder_masonry`)를 뺐다**(2026-09-09). 여기 있었지만 그림이 두께를
|
||
#: **뒷길이**에서 내는데 큰돌쌓기는 규격 축이 **직경**(`stone_cm`)이라 **영영 못 그렸다** —
|
||
#: 목록에만 있고 결과는 늘 빈 그림이었다. 품셈 **13-6(직경) ↔ 13-4(뒷길이)** 로 축이
|
||
#: 갈리는 자리이고 수량 쪽에서 이미 갈라 둔 그것이다(`EXPANDERS` 주석).
|
||
#: ⇒ 「직경에서 두께를 내는 식」은 **도메인 판단이라 사용자 몫** — 계획서 4-12 답 대기 줄.
|
||
FIGURE_TYPE_IDS: frozenset[str] = frozenset({"masonry_wet", "masonry_dry"})
|
||
|
||
#: **전면 기울기 판정** 대상 — 그림 대상과 **다르다**. 큰돌쌓기는 그림은 못 그려도
|
||
#: 기울기는 판정된다(품셈 13-6 「1:0.3 이상」). 한 목록으로 묶어 두었더니 큰돌쌓기를
|
||
#: 그림에서 뺄 때 **장 제목의 「1:0.3」까지 사라졌다**(2026-09-09 화면 실측에서 잡음).
|
||
SLOPE_TYPE_IDS: frozenset[str] = frozenset({"masonry_wet", "masonry_dry", "boulder_masonry"})
|
||
|
||
#: 그림이 못 서는 장에 **까닭을 적는다** — 빈 자리를 그냥 두면 사용자가 「고장」으로 읽는다
|
||
#: (2026-09-09 사용자 지시). 「무엇을 받아야 서는지」까지 적는다.
|
||
_NO_FIGURE_REASONS: dict[str, str] = {
|
||
"boulder_masonry": (
|
||
"그림 없음 — 큰돌쌓기는 규격이 **직경**이라 두께를 낼 식이 아직 없습니다"
|
||
"(뒷길이로 내는 돌쌓기 식과 축이 다름 · 사용자 확정 대기)"
|
||
),
|
||
"retaining_wall": "그림 없음 — 옹벽은 단면이 달라 아직 그리지 않습니다(표준도 3단계)",
|
||
}
|
||
_NO_FIGURE_DEFAULT = "그림 없음 — 이 형식은 아직 단면을 그리지 않습니다"
|
||
|
||
|
||
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
|
||
|
||
|
||
_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 + WALL_TRENCH_CLEARANCE_M
|
||
# 기초 몫 — 「기초유 / 기초버림」이 폭·깊이를 가른다(정본 탭 제목).
|
||
foundation = str((sheet.get("options") or {}).get("foundation") or "")
|
||
if foundation == "기초유":
|
||
base_w, base_d = WALL_FOUNDATION_WIDTH_M, WALL_FOUNDATION_DEPTH_M
|
||
elif foundation == "기초버림":
|
||
base_w, base_d = WALL_BLINDING_WIDTH_M, WALL_BLINDING_DEPTH_M
|
||
else:
|
||
base_w = base_d = 0.0 # 안 정한 장은 기초를 안 그린다 — 지어내지 않는다.
|
||
|
||
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)
|
||
|
||
# 기초 — 벽 밑에 놓이는 칸. 「안 정함」이면 안 그린다.
|
||
if base_d > 0:
|
||
base = polyline_entity(
|
||
f"{drawing_id}:fig:base",
|
||
[mm(p) for p in ((0.0, 0.0), (0.0, -base_d), (base_w, -base_d), (base_w, 0.0))],
|
||
layer_id,
|
||
guide_color,
|
||
)
|
||
if base is not None:
|
||
entities.append(base)
|
||
|
||
# 물구멍 — 벽을 가로지르는 짧은 선 하나. 개소 간격은 글자로 적는다(면적당이라 그림에 못 씀).
|
||
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"기초 {base_w:g} × {base_d:g} m ({foundation})"
|
||
if base_d > 0
|
||
else "기초 — 안 정함(기초유/기초버림)",
|
||
(dig_width + 0.16, -base_d / 2.0 if base_d else 0.0),
|
||
"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 + base_d) * scale
|
||
return entities, used_mm
|