Files
Aislo/B07_DesignDetail/B07_DesignDetail_Engine_Cad_Sheet.py
T
eomsangdonandClaude Opus 5.5 b5442ccc45 feat(b07): 횡단 장 칸 = 아래 가운데 표 먼저 · 그 위 글 · 그림 도로 중심 = 칸 가운데 (174-1)
- 표 아래선 = 칸 아래 + 5 mm · 같은 행 표 윗선 · 아랫선 한 높이 · 표 가운데 x = 칸 가운데
- [상세] 글 표 바로 위 · 그림 아래 끝 = 글(없으면 표) 위 8 mm · 도로 중심(offset 0) = 칸 가운데
- 넘침 = 블록 틀이 칸 위 · 옆을 넘을 때 붉은 칸 틀(축척 그대로)
- 배치 판 5 — 옛 판 확정 장 안내 글 새 배치로
- test_140 칸 가운데 판정을 새 배치로 · test_174 새 시험

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TNMXVCZXMpHpVvqmFkeChL
2026-10-10 09:30:52 +09:00

244 lines
10 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 횡단면도 장 배치 — 장마다 열 × 행 칸(기본 4 × 4)에 단면을 하나씩 담는다(140-3).
채우는 순서는 **좌하단부터 아래에서 위로, 그다음 오른쪽 열**이다
(2026-08-29 사용자 확정). 칸 = 도각 그림칸(usable_area)을 똑같이 나눈 자리 · 단면은
칸 가운데 · 작성 척도 1/100 그대로(큰 단면이 칸을 넘으면 겹침 — 사용자 배치로 고침).
칸 = 인쇄되는 표 격자선 · 블록 테두리 없음 · 칸보다 큰 단면은 `overflow_cells`(169-2).
칸 안 = 표를 칸 아래 가운데에 먼저 · 그 위 글 · 그림(도로 중심 = 칸 가운데)(174-1).
"""
import re
from typing import Any
from B05_Profile.B05_Profile_Engine_Sections import cross_key
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
CROSS_BORDER_PAD_MM,
CROSS_MM,
CROSS_TABLE_LAYER_ID,
DESIGN_LAYER_ID,
DRAWING_FORMAT,
FRAME_LAYER_ID,
GROUND_LAYER_ID,
ROCK_LAYER_ID,
STRUCTURE_LAYER_ID,
TABLE_LINE_COLOR,
_clip_polyline,
_layer,
_section_window,
build_cross_drawing,
points_from_samples,
polyline_entity,
)
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Table import (
cross_note_height,
cross_note_lines,
cross_table_height,
cross_table_width,
)
from B07_DesignDetail.B07_DesignDetail_Engine_Template import (
frame_entities,
scale_fields,
usable_area,
)
from config.config_system import DRAWING_SCALE_CROSS
# 장 도면 id — 측점 도면(cross_00020m)과 겹치지 않게 `s`를 끼운다(cross_s00020m).
# 옛 순번 형식(cross_s01)도 읽어 준다 — 이미 확정한 매니페스트가 그 이름을 갖고 있다.
CROSS_SHEET_ID = re.compile(r"cross_s(\d{2,}(?:\.\d{2})?)m?")
# 테두리 여유 — build_cross_drawing의 테두리 값과 맞춘다. 블록은 서로 맞닿게 쌓는다.
_BORDER_PAD_MM = CROSS_BORDER_PAD_MM
# 칸 아래선 ~ 표 아래선 여백(mm) — 같은 행 표가 한 높이(174-1)
CELL_TABLE_PAD_MM = 5.0
def _design_points(design_line: list[Any] | None) -> list[tuple[float, float]]:
points: list[tuple[float, float]] = []
for point in design_line if isinstance(design_line, list) else []:
if not isinstance(point, dict):
continue
x = point.get("offset_m")
y = point.get("elevation_m")
if isinstance(x, (int, float)) and isinstance(y, (int, float)):
points.append((float(x), float(y)))
return points
def section_block_size(
source: dict[str, Any],
design_line: list[Any] | None,
with_table: bool = True,
design: dict[str, Any] | None = None,
overlay: tuple[float, float, float, float] | None = None,
) -> tuple[float, float]:
"""단면 블록 하나의 종이 크기(mm) — 엔티티를 만들지 않고 치수만 잰다.
build_cross_drawing의 테두리 계산과 같은 규칙을 쓴다: 가로는 그리는 범위와
수량표 중 넓은 쪽, 세로는 단면 높이(암 경계선 포함) + 수량표.
"""
design_spec = design if isinstance(design, dict) else None
ground = points_from_samples(source.get("samples", []), "offset_m")
design = _design_points(design_line)
x0, x1 = _section_window(ground, design, overlay=overlay)
ground_clipped = _clip_polyline(ground, x0, x1)
ys = [y for _x, y in ground_clipped] + [y for x, y in design if x0 <= x <= x1]
rock_offset = (design_spec or {}).get("rock_boundary_offset_m")
if isinstance(rock_offset, (int, float)):
ys.extend(y + float(rock_offset) for _x, y in ground_clipped)
if overlay is not None:
ys.extend(overlay[2:])
half_height = ((max(ys) - min(ys)) / 2.0 if ys else 5.0) * CROSS_MM
width = 2.0 * max(
(x1 - x0) / 2.0 * CROSS_MM + _BORDER_PAD_MM,
cross_table_width() / 2.0 + _BORDER_PAD_MM,
)
# 위: 단면 반높이 + 테두리 여유 / 아래: 반높이 + (표 간격 + 표) + 테두리 여유
top = half_height + _BORDER_PAD_MM
below = half_height + _BORDER_PAD_MM
if with_table:
below += 8.0 + cross_note_height(cross_note_lines(design_spec)) + cross_table_height()
return (width, top + below)
def grid_slots(n: int, cols: int, rows: int) -> list[list[float]]:
"""n 개 칸 가운데 — 그림칸(usable_area)을 열 × 행으로 똑같이 나눔 · 좌하단 → 위 → 오른쪽 열."""
usable_w, usable_h = usable_area()
cell_w, cell_h = usable_w / cols, usable_h / rows
return [
[
-usable_w / 2.0 + (index // rows + 0.5) * cell_w,
-usable_h / 2.0 + (index % rows + 0.5) * cell_h,
]
for index in range(n)
]
def sheet_grid(layout: dict[str, Any] | None, number: int) -> tuple[int, int]:
"""장 번호의 (열, 행) — 장마다 값이 없으면 기본(없으면 4 × 4)."""
layout = layout or {}
grid = (layout.get("sheets") or {}).get(str(number)) or layout.get("default") or (4, 4)
return int(grid[0]), int(grid[1])
def plan_cross_sheets(
chainages: list[int | float], layout: dict[str, Any] | None = None
) -> list[dict[str, Any]]:
"""측점 목록을 A1 장으로 나눈다 — 장마다 열 × 행 칸(기본 4 × 4 · 140-3).
단면 크기와 무관하게 칸 수만큼 담는다(큰 단면이 칸을 넘는 겹침은 사용자가 배치로 고침).
작성 척도는 1/100 고정 — 줄이지 않는다(지식DB 「설계제원_총괄」 측량·도면 기준).
"""
sheets: list[dict[str, Any]] = []
start = 0
while start < len(chainages):
cols, rows = sheet_grid(layout, len(sheets) + 1)
group = list(chainages[start : start + cols * rows])
sheets.append(
{
# id는 **시작 측점**으로 짓는다. 순번(`cross_s01`)으로 지으면 배치가
# 바뀌어 한 장에 담기는 측점 수가 달라졌을 때 같은 이름이 다른 구간을
# 가리키고, 옛 확정 표시가 그대로 새 구간에 붙는다(2026-09-01 지적).
"id": f"cross_s{cross_key(group[0])}m",
"number": len(sheets) + 1,
"chainages": group,
"grid": [cols, rows],
"slots": grid_slots(len(group), cols, rows),
}
)
start += len(group)
return sheets
def _grid_lines(sheet_id: str, cols: int, rows: int) -> list[dict[str, Any]]:
"""장 칸 격자선(169-2) — 세로 열 + 1 · 가로 행 + 1 · 도각 층(인쇄 · DXF 에 나감)."""
usable_w, usable_h = usable_area()
left, bottom = -usable_w / 2.0, -usable_h / 2.0
lines = [
((left + usable_w * i / cols, bottom), (left + usable_w * i / cols, -bottom), f"v{i}")
for i in range(cols + 1)
] + [
((left, bottom + usable_h * j / rows), (-left, bottom + usable_h * j / rows), f"h{j}")
for j in range(rows + 1)
]
return [
entity
for start, end, name in lines
if (
entity := polyline_entity(
sheet_id, [start, end], FRAME_LAYER_ID, TABLE_LINE_COLOR, f":grid-{name}"
)
)
]
def build_cross_sheet(sheet: dict[str, Any], sections: list[dict[str, Any]]) -> dict[str, Any]:
"""한 장을 만든다. sections는 이 장에 담을 측점 입력들(배치 순서대로)."""
entities: list[dict[str, Any]] = []
placements: list[dict[str, Any]] = []
overflow: list[list[float]] = []
slots = sheet.get("slots") or []
cols, rows = sheet.get("grid") or (4, 4)
usable_w, usable_h = usable_area()
cell_w, cell_h = usable_w / cols, usable_h / rows
for index, section in enumerate(sections):
if index >= len(slots):
break
seed_id = f"{sheet['id']}:{section['chainage']}"
center_x, center_y = slots[index]
cell = [
center_x - cell_w / 2.0,
center_y - cell_h / 2.0,
center_x + cell_w / 2.0,
center_y + cell_h / 2.0,
]
# 표 아래선 = 칸 아래 + 여백 · 도로 중심 = 칸 가운데 · 글 · 그림은 위로(174-1)
placed = build_cross_drawing(
section["source"],
seed_id,
section.get("design_line"),
section.get("design"),
section.get("quantity_table"),
section.get("title", ""),
origin=(center_x, 0.0),
overlay=section.get("overlay"),
border=False,
table_bottom=cell[1] + CELL_TABLE_PAD_MM,
)
# 칸을 넘는 단면 — 축척 그대로 두고 그 칸을 화면 가이드로 짚음(169-2)
frames = [p.get("frame") for p in placed.get("cross_placements") or []]
left, _bottom, right, top = frames[0] if frames and frames[0] else cell
if left < cell[0] - 1e-6 or right > cell[2] + 1e-6 or top > cell[3] + 1e-6:
overflow.append(cell)
entities.extend(placed["entities"])
placements.extend(placed.get("cross_placements") or [])
if entities:
entities.extend(_grid_lines(sheet["id"], cols, rows))
# 도각 = 고정 그림칸 — 내용 크기와 무관하게 칸 격자와 같은 자리(140-3).
entities.extend(
frame_entities(
sheet["id"],
(-usable_w / 2.0, -usable_h / 2.0, usable_w / 2.0, usable_h / 2.0),
fit=False,
fields={"도면명": "횡단면도", **scale_fields(("", DRAWING_SCALE_CROSS))},
)
)
return {
"format": DRAWING_FORMAT,
"entities": entities,
"cross_placements": placements,
# 칸보다 큰 단면의 칸 [x0, y0, x1, y1] — 라우터가 CAD 가이드 붉은 틀로 보냄(169-2)
"overflow_cells": overflow,
"layers": [
_layer(GROUND_LAYER_ID, "원지반", locked=True),
_layer(DESIGN_LAYER_ID, "계획선"),
_layer(STRUCTURE_LAYER_ID, "구조물"),
_layer(ROCK_LAYER_ID, "암 경계선"),
_layer(CROSS_TABLE_LAYER_ID, "수량 산출표"),
_layer(FRAME_LAYER_ID, "도각", locked=True),
],
}