"""B07 횡단면도 장 배치 — 장마다 열 × 행 칸(기본 4 × 4)에 단면을 하나씩 담는다(140-3). 채우는 순서는 **좌하단부터 아래에서 위로, 그다음 오른쪽 열**이다 (2026-08-29 사용자 확정). 칸 = 도각 그림칸(usable_area)을 똑같이 나눈 자리 · 단면은 칸 가운데 · 작성 척도 1/100 그대로(큰 단면이 칸을 넘으면 겹침 — 사용자 배치로 고침). """ 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, _clip_polyline, _layer, _section_window, build_cross_drawing, points_from_samples, ) 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 ( entities_bbox, 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 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 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]] = [] slots = sheet.get("slots") or [] for index, section in enumerate(sections): if index >= len(slots): break seed_id = f"{sheet['id']}:{section['chainage']}" # 1) 원점(0,0)에 한 번 만들어 블록이 원점 대비 어디에 놓이는지 잰다. probe = build_cross_drawing( section["source"], seed_id, section.get("design_line"), section.get("design"), section.get("quantity_table"), section.get("title", ""), overlay=section.get("overlay"), ) bbox = entities_bbox(probe["entities"]) if bbox is None: continue min_x, min_y, max_x, max_y = bbox center_x, center_y = slots[index] # 2) 블록(테두리 = 자기 그림 + 1 mm) 가운데를 칸 가운데에 맞춰 다시 만든다. origin = (center_x - (min_x + max_x) / 2.0, center_y - (min_y + max_y) / 2.0) placed = build_cross_drawing( section["source"], seed_id, section.get("design_line"), section.get("design"), section.get("quantity_table"), section.get("title", ""), origin=origin, overlay=section.get("overlay"), ) entities.extend(placed["entities"]) placements.extend(placed.get("cross_placements") or []) if entities: # 도각 = 고정 그림칸 — 내용 크기와 무관하게 칸 격자와 같은 자리(140-3). usable_w, usable_h = usable_area() 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, "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), ], }