"""B07 횡단면도 장 배치 — A1 한 장에 들어가는 만큼 단면을 담는다. 채우는 순서는 **좌하단부터 아래에서 위로, 그다음 오른쪽 열**이다 (2026-08-29 사용자 확정). 한 장에 몇 개가 들어가는지는 단면 블록의 크기가 정하며, 블록 크기는 작성 척도(1/100)와 "설계선이 원지반과 갈라지는 구간"이 정한다 — 즉 종이가 허용하는 만큼만 담고 남으면 다음 장으로 넘긴다. """ import re from typing import Any from B07_DesignDetail.B07_DesignDetail_Engine_Cad import ( 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_table_height, cross_table_width, ) from B07_DesignDetail.B07_DesignDetail_Engine_Template import ( entities_bbox, frame_entities, usable_area, ) # 장 도면 id — 측점 도면(cross_00020m)과 겹치지 않는 형식. CROSS_SHEET_ID = re.compile(r"cross_s(\d{2,})") # 블록 사이 여백(mm)과 테두리 여유 — build_cross_drawing의 테두리 값과 맞춘다. _BLOCK_GAP_MM = 8.0 _BORDER_PAD_MM = 4.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 ) -> tuple[float, float]: """단면 블록 하나의 종이 크기(mm) — 엔티티를 만들지 않고 치수만 잰다. build_cross_drawing의 테두리 계산과 같은 규칙을 쓴다: 가로는 그리는 범위와 수량표 중 넓은 쪽, 세로는 단면 높이 + 수량표. """ ground = points_from_samples(source.get("samples", []), "offset_m") design = _design_points(design_line) x0, x1 = _section_window(ground, design) 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] half_height = ((max(ys) - min(ys)) / 2.0 if ys else 5.0) * CROSS_MM + 5.0 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_table_height() return (width, top + below) def plan_cross_sheets(blocks: list[tuple[int, float, float]]) -> list[dict[str, Any]]: """(측점, 폭, 높이) 목록을 A1 장으로 나눈다. 칸 크기는 전체 블록의 최대 폭·높이로 통일한다 — 장마다 칸이 달라지면 도면끼리 비교가 안 되기 때문이다. 한 장에 cols x rows개가 들어간다. """ if not blocks: return [] usable_w, usable_h = usable_area() cell_w = max(w for _c, w, _h in blocks) + _BLOCK_GAP_MM cell_h = max(h for _c, _w, h in blocks) + _BLOCK_GAP_MM cols = max(1, int(usable_w // cell_w)) rows = max(1, int(usable_h // cell_h)) per_sheet = cols * rows sheets: list[dict[str, Any]] = [] for index in range(0, len(blocks), per_sheet): group = blocks[index : index + per_sheet] number = index // per_sheet + 1 sheets.append( { "id": f"cross_s{number:02d}", "number": number, "chainages": [chainage for chainage, _w, _h in group], "cols": cols, "rows": rows, "cell": (cell_w, cell_h), } ) return sheets def _cell_center(index: int, rows: int, cell: tuple[float, float]) -> tuple[float, float]: """좌하단부터 아래→위로 채우고, 열이 차면 오른쪽 열로 넘어간다.""" cell_w, cell_h = cell usable_w, usable_h = usable_area() column, row = divmod(index, rows) x = -usable_w / 2.0 + cell_w * (column + 0.5) y = -usable_h / 2.0 + cell_h * (row + 0.5) return (x, y) def build_cross_sheet(sheet: dict[str, Any], sections: list[dict[str, Any]]) -> dict[str, Any]: """한 장을 만든다. sections는 이 장에 담을 측점 입력들(배치 순서대로).""" entities: list[dict[str, Any]] = [] rows = int(sheet.get("rows", 1)) cell = tuple(sheet.get("cell", (100.0, 100.0))) # type: ignore[arg-type] for index, section in enumerate(sections): 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", ""), ) bbox = entities_bbox(probe["entities"]) if bbox is None: continue min_x, min_y, max_x, max_y = bbox center_x, center_y = _cell_center(index, rows, cell) # 2) 칸 가운데에 오도록 원점을 옮겨 다시 만든다. 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, ) entities.extend(placed["entities"]) bbox = entities_bbox(entities) if bbox: entities.extend(frame_entities(sheet["id"], bbox, fit=False)) return { "format": DRAWING_FORMAT, "entities": entities, "layers": [ _layer(GROUND_LAYER_ID, "Existing Ground", locked=True), _layer(DESIGN_LAYER_ID, "Design Plan"), _layer(STRUCTURE_LAYER_ID, "Structure"), _layer(ROCK_LAYER_ID, "Rock Boundary"), _layer(CROSS_TABLE_LAYER_ID, "Quantity Table"), _layer(FRAME_LAYER_ID, "Frame", locked=True), ], }