Files
s-canvas/retaining_wall_parser.py
HYUNJUNGLEE b9342f6726 Import S-CANVAS source + iter=1~7 lint cleanup
S-CANVAS (Saman Corp.) — DXF + DEM + AI 기반 3D 조감도 생성 엔진.
~24k LOC Python (scanvas_maker.py 7072 LOC GUI + 구조물 파서/빌더 다수).

이 커밋은 7-iter cleanup이 적용된 상태로 import:
- F821 8 + B023 6: 비동기 lambda + except/loop 변수 캡처 NameError
  (Py3.13에서 reproduce 확인된 진짜 버그)
- RUF012 4 + RUF013 1: ClassVar / implicit Optional 명시화
- F811/B905/B904/F401/F841/W293/F541/UP/SIM/RUF/PLR 700+ cleanup/modernization

신규 파일:
- ruff.toml: target=py313, Korean unicode/저자 스타일/도메인 복잡도 무력화
- requirements-py313.txt: pyproj>=3.7, scipy>=1.14, numpy>=2.0.2 (Py3.13 wheel)
- .gitignore: gcp-key.json, 캐시, 백업, 생성 이미지 제외

검증: ruff 0 errors, py_compile 0 errors, import 33/33 OK on Py3.13.13.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 10:29:08 +09:00

227 lines
7.5 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.
"""옹벽 (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
import ezdxf
from view_detector import detect_view_regions
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, *ev)
params.bottom_el = min(params.bottom_el, *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())