- 장 · 측점 한 장 원지반 층 잠금 지움(종단 그대로) - 장의 측점마다 그림 · 표 · 글 groupId 하나 · cross_placements group_id - 화면 덧그림(설계선 · 구조물)도 같은 groupId - 새 시험 test_180_b07_cross_group Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TNMXVCZXMpHpVvqmFkeChL
252 lines
10 KiB
Python
252 lines
10 KiB
Python
"""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 uuid import uuid5
|
||
|
||
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,
|
||
_ENTITY_NS,
|
||
_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)
|
||
# 측점 한 벌 = groupId 하나 — 한 번 클릭에 그림 · 표 · 글 함께(180-1)
|
||
group_id = str(uuid5(_ENTITY_NS, f"{seed_id}:group"))
|
||
for entity in placed["entities"]:
|
||
entity["groupId"] = group_id
|
||
for placement in placed.get("cross_placements") or []:
|
||
placement["group_id"] = group_id
|
||
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, "원지반"),
|
||
_layer(DESIGN_LAYER_ID, "계획선"),
|
||
_layer(STRUCTURE_LAYER_ID, "구조물"),
|
||
_layer(ROCK_LAYER_ID, "암 경계선"),
|
||
_layer(CROSS_TABLE_LAYER_ID, "수량 산출표"),
|
||
_layer(FRAME_LAYER_ID, "도각", locked=True),
|
||
],
|
||
}
|