Files
Aislo/B07_DesignDetail/B07_DesignDetail_Engine_Cad_Standard.py
T
eomsangdonandClaude Opus 5 02bd45bf3c feat(B07): 표준 횡단면도 — 치수·측구 확대도·암반선 2단
빈 도각이던 표준 횡단면도를 실제 도면으로 만듦.

- 본 그림: B06 좌측 패널 모식도와 같은 배치를 실치수로 그림(1/50). 노폭·노견·
  측구 상단폭·노면 전폭에 치수선(눈금+치수값)을 붙이고, 절토·성토 경사비와
  횡단경사를 표기. 값은 B06 표준 횡단면 설정을 그대로 읽고 없으면 config 기본값.
- 측구 부분 확대도(1/10): 상단폭·저폭·깊이 치수 + 확대 축척 표기.
- 암반 2단 절토: 아래는 암반 경사(1:0.4), 위는 토사 경사(1:1), 갈리는 높이에
  암반선을 파선으로 긋고 각도를 함께 적음.
- 암 L형 측구·포장 횡단경사는 주기(※)로 적음 — 본 그림은 토사 기준.

곁들여: Router_Support 가 700줄을 넘어 재수출 import 를 한 문장으로 합쳐 681줄로
줄임(기능 변화 없음).

검증: 치수값이 STANDARD_CROSS_SECTION 과 일치(500·3000·500·900·4000), 확대도
900·300·300 + S=1/10, 2단 절토선 꺾임점 1개, 콘텐츠 358.2x172.6 mm ≤ A1 작도영역.

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

582 lines
19 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.
"""B07 표준 횡단면도 CAD 조립 — 변수 모식도에 치수를 넣고, 측구 확대도·암반선 2단을 함께 낸다.
사용자 지시(2026-09-04) — 「표준 횡단면도 좌상단에 기본값으로 B06 좌측 패널의 변수 위치
안내와 비슷한 그림을 넣고, 변수 이름 자리에 도면처럼 치수를 적을 것. 측구는 작으니 부분
확대도로. 발파·암반이면 암반선과 각도를 넣어 절토측이 2단(토사 각도 + 암반 각도)으로
표현될 것」.
배치는 B06 좌측 패널 모식도(`B06_Section_UI_Standard_Diagram.ts`)와 같다 — 좌가 절토·측구,
우가 성토, 가운데가 계획고. 다른 점은 **실치수**라는 것이다. 모식도는 위치 안내라 비율이
없지만 도면은 축척(본 그림 1/50, 측구 확대도 1/10)대로 그리고 치수선을 붙인다.
값은 B06 「표준 횡단면 설정」(`standard_cross_section`)을 그대로 읽는다 — 여기서 기하를
다시 정하지 않는다. 저장값이 없으면 config 기본값을 쓴다.
좌표 규약: 종이 mm = 실거리 m x MM. 원점은 계획고(노면 중심).
"""
import math
from typing import Any
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
DRAWING_FORMAT,
FRAME_LAYER_ID,
TABLE_LABEL_COLOR,
_layer,
_text_entity,
polyline_entity,
)
from B07_DesignDetail.B07_DesignDetail_Engine_Template import (
entities_bbox,
frame_entities,
scale_fields,
)
from config.config_system import (
DRAWING_SCALE_CROSS_STANDARD,
DRAWING_SCALE_DITCH_DETAIL,
STANDARD_CROSS_SECTION,
)
STANDARD_KIND = "cross_standard"
STANDARD_LABEL = "표준 횡단면도"
# 도면 좌표 = 종이 mm. 본 그림 1/50 -> 1 m = 20 mm, 측구 확대도 1/10 -> 1 m = 100 mm.
MM = 1000.0 / DRAWING_SCALE_CROSS_STANDARD
DETAIL_MM = 1000.0 / DRAWING_SCALE_DITCH_DETAIL
SECTION_LAYER_ID = "b07-std-section"
ROCK_LAYER_ID = "b07-std-rock"
DIM_LAYER_ID = "b07-std-dim"
DETAIL_LAYER_ID = "b07-std-detail"
NOTE_LAYER_ID = "b07-std-note"
TITLE_LAYER_ID = "b07-std-title"
SECTION_COLOR = "#111111"
ROCK_COLOR = "#a63d3d"
DIM_COLOR = "#2f6fb0"
DETAIL_COLOR = "#111111"
NOTE_COLOR = "#333333"
_LINE_WIDTH = 2
_TITLE_FONT_SIZE = 7.0
_LABEL_FONT_SIZE = 3.0
_DIM_FONT_SIZE = 2.6
_NOTE_FONT_SIZE = 2.8
# 그림에 세울 절·성토 높이(m) — 표준도는 실제 지형이 없으므로 대표 높이로 그린다.
_CUT_HEIGHT_M = 3.0
_FILL_HEIGHT_M = 3.0
# 암반 구간: 절토 밑에서 이만큼이 암반이고 그 위가 토사다(2단 절토).
_ROCK_HEIGHT_M = 1.5
_DIM_TICK_MM = 1.6 # 치수선 끝 눈금 반길이
_DIM_OFFSET_MM = 8.0 # 치수선을 그림에서 띄우는 거리
_DIM_GAP_MM = 6.0 # 치수선 단 사이
def _ground(kind: str, standard: dict[str, Any] | None) -> dict[str, Any]:
"""표준 횡단면 설정에서 한 지반유형 값을 꺼낸다. 없으면 config 기본값."""
stored = (standard or {}).get(kind)
if isinstance(stored, dict) and stored:
merged = dict(STANDARD_CROSS_SECTION.get(kind) or {})
merged.update(stored)
return merged
return dict(STANDARD_CROSS_SECTION.get(kind) or {})
def _number(value: Any, fallback: float) -> float:
return float(value) if isinstance(value, (int, float)) else fallback
def _dim_entities(
drawing_id: str,
tag: str,
start: tuple[float, float],
end: tuple[float, float],
label: str,
layer_id: str = DIM_LAYER_ID,
) -> list[dict[str, Any]]:
"""치수선 한 벌(치수선 + 양끝 눈금 + 치수값). 좌표는 종이 mm."""
entities: list[dict[str, Any]] = []
line = polyline_entity(drawing_id, [start, end], layer_id, DIM_COLOR, suffix=f":dim:{tag}")
if line:
entities.append(line)
dx, dy = end[0] - start[0], end[1] - start[1]
length = math.hypot(dx, dy) or 1.0
nx, ny = -dy / length, dx / length
for index, point in enumerate((start, end)):
tick = polyline_entity(
drawing_id,
[
(point[0] - nx * _DIM_TICK_MM, point[1] - ny * _DIM_TICK_MM),
(point[0] + nx * _DIM_TICK_MM, point[1] + ny * _DIM_TICK_MM),
],
layer_id,
DIM_COLOR,
suffix=f":dim:{tag}:tick:{index}",
)
if tick:
entities.append(tick)
entities.append(
_text_entity(
f"{drawing_id}:dim:{tag}:text",
label,
(start[0] + end[0]) / 2.0 + nx * 2.2,
(start[1] + end[1]) / 2.0 + ny * 2.2,
layer_id,
_DIM_FONT_SIZE,
DIM_COLOR,
)
)
return entities
def _section_geometry(values: dict[str, Any]) -> dict[str, Any]:
"""표준 단면의 실좌표(m) 꼭짓점. 좌가 절토·측구, 우가 성토(B06 모식도와 같은 배치)."""
road = _number(values.get("road_width_m"), 3.0)
shoulder_left = _number(values.get("shoulder_left_m"), 0.5)
shoulder_right = _number(values.get("shoulder_right_m"), 0.5)
ditch = values.get("ditch") if isinstance(values.get("ditch"), dict) else {}
top_width = _number(ditch.get("top_width_m"), 0.9)
bottom_width = _number(ditch.get("bottom_width_m"), 0.3)
depth = _number(ditch.get("depth_m"), 0.3)
slope = values.get("cross_slope_pct") if isinstance(values.get("cross_slope_pct"), dict) else {}
cross_pct = _number(slope.get("max"), _number(slope.get("min"), 3.0))
cut_ratio = _number(values.get("cut_slope_ratio"), 1.0)
fill_ratio = _number(values.get("fill_slope_ratio"), 1.2)
road_left = -(road / 2.0 + shoulder_left)
road_right = road / 2.0 + shoulder_right
# 횡단경사는 측구(좌) 쪽으로 내려간다 — 노면 좌끝이 계획고보다 낮다.
drop = abs(road_left) * cross_pct / 100.0
surface = [(road_right, 0.0), (road_left, -drop)]
ditch_top_left = road_left - top_width
ditch_bottom_y = -drop - depth
inset = (top_width - bottom_width) / 2.0
ditch_line = [
(road_left, -drop),
(road_left - inset, ditch_bottom_y),
(ditch_top_left + inset, ditch_bottom_y),
(ditch_top_left, -drop),
]
cut_top = (ditch_top_left - _CUT_HEIGHT_M * cut_ratio, -drop + _CUT_HEIGHT_M)
fill_toe = (road_right + _FILL_HEIGHT_M * fill_ratio, -_FILL_HEIGHT_M)
return {
"road": road,
"shoulder_left": shoulder_left,
"shoulder_right": shoulder_right,
"top_width": top_width,
"bottom_width": bottom_width,
"depth": depth,
"cross_pct": cross_pct,
"cut_ratio": cut_ratio,
"fill_ratio": fill_ratio,
"road_left": road_left,
"road_right": road_right,
"drop": drop,
"surface": surface,
"ditch_line": ditch_line,
"ditch_top_left": ditch_top_left,
"cut_start": (ditch_top_left, -drop),
"cut_top": cut_top,
"fill_toe": fill_toe,
}
def _rock_entities(
drawing_id: str, geometry: dict[str, Any], rock_ratio: float, paper: Any
) -> list[dict[str, Any]]:
"""암반선과 2단 절토(아래=암반각, 위=토사각)를 절토측에 덧그린다."""
entities: list[dict[str, Any]] = []
start_x, start_y = geometry["cut_start"]
soil_ratio = geometry["cut_ratio"]
# 아래 단: 암반각으로 _ROCK_HEIGHT_M 만큼 올라간다.
bench = (start_x - _ROCK_HEIGHT_M * rock_ratio, start_y + _ROCK_HEIGHT_M)
# 위 단: 그 위는 토사각.
upper = (
bench[0] - (_CUT_HEIGHT_M - _ROCK_HEIGHT_M) * soil_ratio,
bench[1] + (_CUT_HEIGHT_M - _ROCK_HEIGHT_M),
)
two_stage = polyline_entity(
drawing_id,
[paper((start_x, start_y)), paper(bench), paper(upper)],
ROCK_LAYER_ID,
ROCK_COLOR,
suffix=":rock:cut",
width=_LINE_WIDTH,
)
if two_stage:
entities.append(two_stage)
# 암반선 — 2단이 갈리는 높이의 수평 파선.
boundary = polyline_entity(
drawing_id,
[paper((bench[0] - 1.5, bench[1])), paper((geometry["road_right"], bench[1]))],
ROCK_LAYER_ID,
ROCK_COLOR,
suffix=":rock:boundary",
dash=[6, 4],
)
if boundary:
entities.append(boundary)
label_x, label_y = paper((bench[0] - 1.6, bench[1]))
entities.append(
_text_entity(
f"{drawing_id}:rock:boundary:text",
"암반선",
label_x,
label_y + 2.5,
ROCK_LAYER_ID,
_LABEL_FONT_SIZE,
ROCK_COLOR,
align="right",
)
)
mid_lower = paper(((start_x + bench[0]) / 2.0, (start_y + bench[1]) / 2.0))
mid_upper = paper(((bench[0] + upper[0]) / 2.0, (bench[1] + upper[1]) / 2.0))
entities.append(
_text_entity(
f"{drawing_id}:rock:lower",
f"암반 1:{rock_ratio:g}",
mid_lower[0] - 6.0,
mid_lower[1],
ROCK_LAYER_ID,
_DIM_FONT_SIZE,
ROCK_COLOR,
align="right",
)
)
entities.append(
_text_entity(
f"{drawing_id}:rock:upper",
f"토사 1:{soil_ratio:g}",
mid_upper[0] - 6.0,
mid_upper[1],
ROCK_LAYER_ID,
_DIM_FONT_SIZE,
ROCK_COLOR,
align="right",
)
)
return entities
def _ditch_detail_entities(
drawing_id: str, geometry: dict[str, Any], origin: tuple[float, float]
) -> list[dict[str, Any]]:
"""측구 부분 확대도(1/10) — 작아서 본 그림에서는 치수를 읽을 수 없다."""
entities: list[dict[str, Any]] = []
top_width = geometry["top_width"]
bottom_width = geometry["bottom_width"]
depth = geometry["depth"]
inset = (top_width - bottom_width) / 2.0
ox, oy = origin
def paper(point: tuple[float, float]) -> tuple[float, float]:
return (ox + point[0] * DETAIL_MM, oy + point[1] * DETAIL_MM)
shape = [
(0.0, 0.0),
(inset, -depth),
(inset + bottom_width, -depth),
(top_width, 0.0),
]
outline = polyline_entity(
drawing_id,
[paper(point) for point in shape],
DETAIL_LAYER_ID,
DETAIL_COLOR,
suffix=":detail:ditch",
width=_LINE_WIDTH,
)
if outline:
entities.append(outline)
entities.extend(
_dim_entities(
drawing_id,
"detail-top",
paper((0.0, 0.0 + 0.06)),
paper((top_width, 0.0 + 0.06)),
f"{top_width * 1000:.0f}",
DETAIL_LAYER_ID,
)
)
entities.extend(
_dim_entities(
drawing_id,
"detail-bottom",
paper((inset, -depth - 0.06)),
paper((inset + bottom_width, -depth - 0.06)),
f"{bottom_width * 1000:.0f}",
DETAIL_LAYER_ID,
)
)
entities.extend(
_dim_entities(
drawing_id,
"detail-depth",
paper((top_width + 0.08, 0.0)),
paper((top_width + 0.08, -depth)),
f"{depth * 1000:.0f}",
DETAIL_LAYER_ID,
)
)
title = paper((top_width / 2.0, 0.3))
entities.append(
_text_entity(
f"{drawing_id}:detail:title",
f"측구 상세도 (S = 1/{DRAWING_SCALE_DITCH_DETAIL})",
title[0],
title[1],
DETAIL_LAYER_ID,
_LABEL_FONT_SIZE,
TABLE_LABEL_COLOR,
)
)
return entities
def build_standard_cross_drawing(
drawing_id: str, label: str, standard: dict[str, Any] | None = None
) -> dict[str, Any]:
"""표준 횡단면도 한 장을 만든다 — 본 그림 + 치수 + 측구 확대도 + 암반 2단 + 주기."""
soil = _ground("soil", standard)
rock = _ground("rock", standard)
paved = _ground("paved", standard)
geometry = _section_geometry(soil)
def paper(point: tuple[float, float]) -> tuple[float, float]:
return (point[0] * MM, point[1] * MM)
entities: list[dict[str, Any]] = []
# 본 그림: 절토면 - 측구 - 노면 - 성토면을 한 줄로 잇는다.
outline = [
geometry["cut_top"],
*geometry["ditch_line"][::-1],
*geometry["surface"][::-1],
geometry["fill_toe"],
]
body = polyline_entity(
drawing_id,
[paper(point) for point in outline],
SECTION_LAYER_ID,
SECTION_COLOR,
suffix=":section",
width=_LINE_WIDTH,
)
if body:
entities.append(body)
# 중심선(계획고).
center = polyline_entity(
drawing_id,
[paper((0.0, 1.2)), paper((0.0, -1.2))],
SECTION_LAYER_ID,
SECTION_COLOR,
suffix=":center",
dash=[10, 3, 2, 3],
)
if center:
entities.append(center)
entities.append(
_text_entity(
f"{drawing_id}:center:text",
"계획고",
*paper((0.0, 1.45)),
SECTION_LAYER_ID,
_LABEL_FONT_SIZE,
SECTION_COLOR,
)
)
# 치수선 — 노면 아래 두 단(위: 노견·노폭·노견, 아래: 노면 전폭).
base_y = min(geometry["fill_toe"][1], -geometry["drop"] - geometry["depth"])
dim_y = base_y * MM - _DIM_OFFSET_MM
half = geometry["road"] / 2.0
entities.extend(
_dim_entities(
drawing_id,
"shoulder-left",
(geometry["road_left"] * MM, dim_y),
(-half * MM, dim_y),
f"{geometry['shoulder_left'] * 1000:.0f}",
)
)
entities.extend(
_dim_entities(
drawing_id,
"road",
(-half * MM, dim_y),
(half * MM, dim_y),
f"{geometry['road'] * 1000:.0f}",
)
)
entities.extend(
_dim_entities(
drawing_id,
"shoulder-right",
(half * MM, dim_y),
(geometry["road_right"] * MM, dim_y),
f"{geometry['shoulder_right'] * 1000:.0f}",
)
)
entities.extend(
_dim_entities(
drawing_id,
"ditch-top",
(geometry["ditch_top_left"] * MM, dim_y),
(geometry["road_left"] * MM, dim_y),
f"{geometry['top_width'] * 1000:.0f}",
)
)
roadbed_width_m = geometry["road"] + geometry["shoulder_left"] + geometry["shoulder_right"]
entities.extend(
_dim_entities(
drawing_id,
"roadbed",
(geometry["road_left"] * MM, dim_y - _DIM_GAP_MM),
(geometry["road_right"] * MM, dim_y - _DIM_GAP_MM),
f"{roadbed_width_m * 1000:.0f}",
)
)
# 경사·횡단경사 표기.
cut_mid = paper(
(
(geometry["cut_start"][0] + geometry["cut_top"][0]) / 2.0,
(geometry["cut_start"][1] + geometry["cut_top"][1]) / 2.0,
)
)
fill_mid = paper(
(
(geometry["road_right"] + geometry["fill_toe"][0]) / 2.0,
(0.0 + geometry["fill_toe"][1]) / 2.0,
)
)
entities.append(
_text_entity(
f"{drawing_id}:cut:text",
f"절토 1:{geometry['cut_ratio']:g}",
cut_mid[0] - 4.0,
cut_mid[1] + 3.0,
SECTION_LAYER_ID,
_DIM_FONT_SIZE,
SECTION_COLOR,
align="right",
)
)
entities.append(
_text_entity(
f"{drawing_id}:fill:text",
f"성토 1:{geometry['fill_ratio']:g}",
fill_mid[0] + 4.0,
fill_mid[1] + 3.0,
SECTION_LAYER_ID,
_DIM_FONT_SIZE,
SECTION_COLOR,
align="left",
)
)
entities.append(
_text_entity(
f"{drawing_id}:cross-slope:text",
f"횡단경사 {geometry['cross_pct']:g}%",
*paper((geometry["road_left"] / 2.0, 0.6)),
SECTION_LAYER_ID,
_DIM_FONT_SIZE,
SECTION_COLOR,
)
)
# 암반 구간 2단 절토.
entities.extend(
_rock_entities(drawing_id, geometry, _number(rock.get("cut_slope_ratio"), 0.4), paper)
)
body_bbox = entities_bbox(entities)
right = body_bbox[2] if body_bbox else 0.0
top = body_bbox[3] if body_bbox else 0.0
# 측구 부분 확대도 — 본 그림 오른쪽 위.
entities.extend(_ditch_detail_entities(drawing_id, geometry, (right + 28.0, top - 30.0)))
# 주기: 구간별로 달라지는 값만 적는다(기본은 토사).
notes = [
"※ 본 그림은 토사 구간 기준임.",
f"※ 암 구간 — 절토 1:{_number(rock.get('cut_slope_ratio'), 0.4):g}, "
f"L형 측구 {_number((rock.get('ditch_l_type') or {}).get('width_m'), 0.5) * 1000:.0f}"
f"×{_number((rock.get('ditch_l_type') or {}).get('depth_m'), 0.1) * 1000:.0f}mm "
"(횡단면도에서 일반·L형 중 선택).",
f"※ 포장 구간 — 절·성토 경사는 토사와 같고 횡단경사만 "
f"{_number((paved.get('cross_slope_pct') or {}).get('min'), 1.5):g}~"
f"{_number((paved.get('cross_slope_pct') or {}).get('max'), 2.0):g}% 임.",
"※ 치수 단위 mm.",
]
note_bbox = entities_bbox(entities)
note_x = note_bbox[0] if note_bbox else 0.0
note_y = (note_bbox[1] if note_bbox else 0.0) - 12.0
for index, note in enumerate(notes):
entities.append(
_text_entity(
f"{drawing_id}:note:{index}",
note,
note_x,
note_y - index * 5.0,
NOTE_LAYER_ID,
_NOTE_FONT_SIZE,
NOTE_COLOR,
align="left",
)
)
bbox = entities_bbox(entities)
if bbox:
min_bx, _min_by, max_bx, max_by = bbox
entities.append(
_text_entity(
f"{drawing_id}:title",
label,
(min_bx + max_bx) / 2.0,
max_by + 12.0,
TITLE_LAYER_ID,
_TITLE_FONT_SIZE,
TABLE_LABEL_COLOR,
)
)
entities.append(
_text_entity(
f"{drawing_id}:scale",
f"S = 1/{DRAWING_SCALE_CROSS_STANDARD}",
max_bx,
max_by + 5.0,
TITLE_LAYER_ID,
_DIM_FONT_SIZE,
TABLE_LABEL_COLOR,
align="right",
)
)
entities.extend(
frame_entities(
drawing_id,
entities_bbox(entities) or bbox,
fit=False,
fields={
"도면명": label,
**scale_fields(("", DRAWING_SCALE_CROSS_STANDARD)),
},
)
)
return {
"format": DRAWING_FORMAT,
"entities": entities,
"layers": [
_layer(SECTION_LAYER_ID, "표준 단면"),
_layer(ROCK_LAYER_ID, "암반선·2단 절토"),
_layer(DIM_LAYER_ID, "치수"),
_layer(DETAIL_LAYER_ID, "측구 상세도"),
_layer(NOTE_LAYER_ID, "주기"),
_layer(TITLE_LAYER_ID, "표제"),
_layer(FRAME_LAYER_ID, "도각", locked=True),
],
}