"""옹벽 (Retaining Wall) 전용 DXF 파서. 구조 특성: - 선형 옹벽 경로 (긴 길이방향) - 구간별로 다른 높이 (지형에 따라 변화) - 배면(뒤쪽) 앵커바 격자 배치 - 상단 안전난간/파라펫 - 기초부 (base slab, 넓음) - 수축이음 (균등 간격) - 배수공 (weep hole) 사용법: p = parse_retaining_wall(dxf_paths) """ from __future__ import annotations import re import math from dataclasses import dataclass, field from pathlib import Path from typing import Optional import ezdxf import numpy as np from view_detector import detect_view_regions, ViewRegion from dxf_geometry import extract_structural_geometry from view_reconstructor import compute_oriented_bbox # --------------------------------------------------------------------------- # 데이터 클래스 # --------------------------------------------------------------------------- @dataclass class WallSection: """옹벽 구간 하나.""" start_station: float = 0.0 # 시점 측점 (m) end_station: float = 0.0 # 종점 측점 top_el: float = 0.0 # 상단 EL bottom_el: float = 0.0 # 바닥 EL # 단면 치수 top_width: float = 0.5 bottom_width: float = 2.5 front_batter: float = 0.02 # 전면 경사 (1:N 비율 중 1 부분) @dataclass class RetainingWallParams: """옹벽 파라미터.""" # 전체 선형 total_length: float = 100.0 # 총 연장 (m) path_direction: str = "X" # "X" 또는 "Y" (주 방향) # 높이 범위 top_el: float = 60.0 bottom_el: float = 41.5 # 기초 바닥 # 단면 (평균) avg_top_width: float = 0.6 # 상단 폭 avg_bottom_width: float = 3.0 # 하단 폭 (중간) base_slab_width: float = 5.0 # 기초 slab 폭 base_slab_thickness: float = 1.0 # 기초 두께 front_batter_ratio: float = 0.05 # 전면 경사 (1:20 수준) # 구간별 (상세 있을 경우) sections: list = field(default_factory=list) # 앵커바 has_anchors: bool = True anchor_count: int = 78 anchor_spacing_h: float = 3.0 # 수평 간격 anchor_spacing_v: float = 3.0 # 수직 간격 anchor_diameter: float = 0.05 # 앵커바 직경 (50mm) anchor_length: float = 12.0 # 앵커 매입 길이 anchor_angle_deg: float = 15 # 하향 각도 # 상단 안전난간 has_parapet: bool = True parapet_height: float = 1.1 parapet_thickness: float = 0.15 # 수축이음 has_contraction_joints: bool = True joint_spacing: float = 10.0 # 간격 # 배수공 has_weep_holes: bool = True weep_hole_spacing: float = 3.0 weep_hole_diameter: float = 0.1 # 기타 ground_level: float = 41.5 # 전면 지반 EL source_files: list = field(default_factory=list) raw_annotations: list = field(default_factory=list) def total_height(self) -> float: return self.top_el - self.bottom_el def summary(self) -> str: return ( f"Retaining Wall: L={self.total_length:.1f}m, " f"EL.{self.bottom_el:.1f}~{self.top_el:.1f} (H={self.total_height():.1f}m)\n" f" 단면 상단={self.avg_top_width:.2f}m, 하단={self.avg_bottom_width:.2f}m, " f"기초 slab {self.base_slab_width:.1f}×{self.base_slab_thickness:.1f}m\n" f" 앵커 {self.anchor_count}개 ({self.anchor_spacing_h:.1f}×{self.anchor_spacing_v:.1f}m), " f"난간 {'O' if self.has_parapet else 'X'}" ) # --------------------------------------------------------------------------- # 파서 # --------------------------------------------------------------------------- EL_PATTERN = re.compile(r"EL\.?\s*[=:]?\s*(\d+\.?\d*)", re.IGNORECASE) class RetainingWallParser: def parse(self, dxf_paths: list[str]) -> RetainingWallParams: params = RetainingWallParams() params.source_files = list(dxf_paths) for path in dxf_paths: try: self._parse_single(path, params) except Exception as e: print(f" 파싱 오류: {e}") self._finalize(params) return params def _parse_single(self, path: str, params: RetainingWallParams): doc = ezdxf.readfile(path) msp = doc.modelspace() geom = extract_structural_geometry(path) scale = geom.unit_scale views = detect_view_regions(path) # EL 수집 els = [] for e in msp: if e.dxftype() not in ("TEXT", "MTEXT"): continue try: txt = e.dxf.text if e.dxftype() == "TEXT" else (e.text or "") m = EL_PATTERN.search(txt) if m: pos = e.dxf.insert els.append((pos.x * scale, pos.y * scale, float(m.group(1)))) except Exception: pass if els: ev = [v for _, _, v in els] params.top_el = max(params.top_el, max(ev)) params.bottom_el = min(params.bottom_el, min(ev)) params.raw_annotations.extend( [(f"EL.{v:.2f}", x, y) for x, y, v in els] ) # 평면도에서 총 연장 추정 plan_view = None for v in views: if v.view_type == "plan": plan_view = v break if plan_view: # 평면도 안의 shapes 점들 수집 → PCA로 길이 추정 local_shapes = plan_view.get_local_shapes() all_pts = [] for s in local_shapes: all_pts.extend(s.points) if len(all_pts) >= 5: obb = compute_oriented_bbox(all_pts) if obb and obb["aspect_ratio"] >= 1.5: params.total_length = max(params.total_length, obb["length"]) params.path_direction = "X" if abs(obb["axis_long"][0]) > abs(obb["axis_long"][1]) else "Y" # 앵커바 개수 추정 (B_Dot 블록 사용) anchor_block_count = 0 for e in msp.query("INSERT"): name = getattr(e.dxf, "name", "") if "B_Dot" in name or "anchor" in name.lower(): anchor_block_count += 1 if anchor_block_count > 10: params.anchor_count = anchor_block_count # 정면도 영역에서 앵커 격자 확인 front_view = None for v in views: if v.view_type == "front": front_view = v break if front_view: # 정면도 면적 / 앵커개수로 간격 추정 area = front_view.width * front_view.height if params.anchor_count > 0: spacing_approx = math.sqrt(area / params.anchor_count) if 1.0 <= spacing_approx <= 6.0: params.anchor_spacing_h = spacing_approx params.anchor_spacing_v = spacing_approx def _finalize(self, p: RetainingWallParams): # 기초 slab 폭: 본체 하단 폭의 1.5배 정도 p.base_slab_width = max(p.avg_bottom_width * 1.5, 4.0) # 높이가 너무 크면 하단 폭 증가 H = p.total_height() if H > 10: p.avg_bottom_width = max(p.avg_bottom_width, H * 0.15) p.base_slab_width = max(p.base_slab_width, p.avg_bottom_width * 1.4) def parse_retaining_wall(paths: list[str]) -> RetainingWallParams: return RetainingWallParser().parse(paths) if __name__ == "__main__": import sys paths = sys.argv[1:] if len(sys.argv) > 1 else [ "SAMPLE_CAD/1. 좌안옹벽 일반도 작성(2026.0109).dxf", ] p = parse_retaining_wall(paths) print(p.summary())