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:
2026-05-08 10:29:08 +09:00
parent 53d8b53c2f
commit b9342f6726
92 changed files with 3413501 additions and 0 deletions

226
retaining_wall_parser.py Normal file
View 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())