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>
This commit is contained in:
226
retaining_wall_parser.py
Normal file
226
retaining_wall_parser.py
Normal file
@@ -0,0 +1,226 @@
|
||||
"""옹벽 (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())
|
||||
Reference in New Issue
Block a user