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:
168
optional_detector.py
Normal file
168
optional_detector.py
Normal file
@@ -0,0 +1,168 @@
|
||||
"""구조물 부속 컴포넌트(공도교/개폐장치/사다리/덮개/에이프런 등) 존재 여부를
|
||||
DXF 레이어·엔티티·텍스트 신호로 검출하는 공통 헬퍼.
|
||||
|
||||
배경:
|
||||
각 구조물 파서(gate, valve_chamber, intake_tower, retaining_wall, …)에서
|
||||
"도면에 실제로 있는 것만 빌드"하기 위해 `has_X` 플래그를 채택. 검출 로직이
|
||||
파서마다 복붙되면 유지보수 비용과 drift 위험이 크므로, 본 모듈로 통합.
|
||||
|
||||
사용 예:
|
||||
from optional_detector import ComponentSpec, detect_components
|
||||
|
||||
specs = [
|
||||
ComponentSpec(
|
||||
name="service_bridge",
|
||||
layer_tokens=("bridge", "공도교", "공도", "관리도로", "service road"),
|
||||
text_keywords=("공도교", "service bridge", "관리교", "관리도로"),
|
||||
default=False,
|
||||
),
|
||||
ComponentSpec(
|
||||
name="hoist_housings",
|
||||
layer_tokens=("hoist", "권양", "winch", "gantry"),
|
||||
text_keywords=("권양기", "hoist", "winch"),
|
||||
default=True, # 래디얼 게이트엔 통상 동반
|
||||
preserve_default_on_no_signal=True, # 신호 부재 시 default 유지
|
||||
),
|
||||
]
|
||||
report = detect_components(msp, specs)
|
||||
params.has_service_bridge = report["service_bridge"].present
|
||||
params.has_hoist_housings = report["hoist_housings"].present
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from collections.abc import Iterable
|
||||
|
||||
|
||||
# geometry로 간주할 엔티티 타입 (TEXT/MTEXT/DIMENSION 등 주석은 제외)
|
||||
GEOM_TYPES: frozenset[str] = frozenset({
|
||||
"LWPOLYLINE", "POLYLINE", "LINE", "CIRCLE", "ARC", "ELLIPSE",
|
||||
"SPLINE", "3DFACE", "SOLID", "HATCH",
|
||||
})
|
||||
|
||||
|
||||
@dataclass
|
||||
class ComponentSpec:
|
||||
"""한 부속 컴포넌트의 검출 규칙."""
|
||||
name: str # 결과 dict의 key
|
||||
layer_tokens: tuple[str, ...] # 레이어명 부분일치 (대소문자 무시)
|
||||
text_keywords: tuple[str, ...] = () # TEXT/MTEXT 부분일치 (대소문자 무시)
|
||||
default: bool = False # 판정 불가 시 기본값
|
||||
# True면 "신호 부재"를 False로 낮추지 않고 default 유지 (예: 개폐장치처럼
|
||||
# 일반적으로 있으나 별도 레이어로 분리되지 않는 부속)
|
||||
preserve_default_on_no_signal: bool = False
|
||||
# geometry 대신 text 신호만으로도 True 허용 여부 (false positive 방지 기본 False)
|
||||
allow_text_only: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class ComponentReport:
|
||||
"""검출 결과."""
|
||||
present: bool
|
||||
geom_count: int
|
||||
text_count: int
|
||||
matched_layers: list[str] = field(default_factory=list)
|
||||
|
||||
def describe(self) -> str:
|
||||
return (f"present={self.present} (geom={self.geom_count}, "
|
||||
f"txt={self.text_count}, layers={sorted(self.matched_layers)})")
|
||||
|
||||
|
||||
def count_layer_geom(msp, tokens: Iterable[str]) -> tuple[int, set[str]]:
|
||||
"""주어진 레이어 토큰에 부분일치하는 geometry 엔티티 수 + 매칭 레이어 집합.
|
||||
|
||||
Args:
|
||||
msp: ezdxf modelspace
|
||||
tokens: 레이어명에 포함되어야 할 문자열들 (대소문자 무시, 부분일치)
|
||||
|
||||
Returns:
|
||||
(count, matched_layer_names)
|
||||
"""
|
||||
lowered = tuple(t.lower() for t in tokens)
|
||||
count = 0
|
||||
matched: set[str] = set()
|
||||
for e in msp:
|
||||
try:
|
||||
layer = e.dxf.layer
|
||||
except Exception:
|
||||
continue
|
||||
lname = layer.lower()
|
||||
if not any(t in lname for t in lowered):
|
||||
continue
|
||||
try:
|
||||
etype = e.dxftype()
|
||||
except Exception:
|
||||
continue
|
||||
if etype in GEOM_TYPES:
|
||||
count += 1
|
||||
matched.add(layer)
|
||||
return count, matched
|
||||
|
||||
|
||||
def count_text_hits(msp, keywords: Iterable[str]) -> int:
|
||||
"""TEXT/MTEXT에서 키워드 부분일치 엔티티 수 (대소문자 무시)."""
|
||||
lowered = tuple(k.lower() for k in keywords)
|
||||
if not lowered:
|
||||
return 0
|
||||
n = 0
|
||||
for e in msp:
|
||||
try:
|
||||
etype = e.dxftype()
|
||||
except Exception:
|
||||
continue
|
||||
if etype not in ("TEXT", "MTEXT"):
|
||||
continue
|
||||
try:
|
||||
txt = e.dxf.text if etype == "TEXT" else (e.text or "")
|
||||
except Exception:
|
||||
continue
|
||||
if not txt:
|
||||
continue
|
||||
tl = txt.lower()
|
||||
if any(k in tl for k in lowered):
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
def detect_component(msp, spec: ComponentSpec) -> ComponentReport:
|
||||
"""단일 컴포넌트 검출.
|
||||
|
||||
판정 규칙:
|
||||
1) layer geometry count > 0 → present = True (확정)
|
||||
2) allow_text_only=True 이고 text count > 0 → present = True
|
||||
3) preserve_default_on_no_signal=True 이고 신호 둘 다 0 → default 유지
|
||||
4) 그 외 → present = False (default가 True였어도 낮춤)
|
||||
"""
|
||||
geom_count, matched = count_layer_geom(msp, spec.layer_tokens)
|
||||
text_count = count_text_hits(msp, spec.text_keywords)
|
||||
|
||||
if geom_count > 0 or (spec.allow_text_only and text_count > 0):
|
||||
present = True
|
||||
elif spec.preserve_default_on_no_signal and geom_count == 0 and text_count == 0:
|
||||
present = spec.default
|
||||
else:
|
||||
# 신호 부재 → default를 False로 낮춤
|
||||
# 단, default=True 이지만 preserve_default_on_no_signal=False 인 경우,
|
||||
# text 약신호라도 있으면 True 유지 여지
|
||||
present = bool(spec.default and text_count > 0)
|
||||
|
||||
return ComponentReport(
|
||||
present=present,
|
||||
geom_count=geom_count,
|
||||
text_count=text_count,
|
||||
matched_layers=sorted(matched),
|
||||
)
|
||||
|
||||
|
||||
def detect_components(msp, specs: Iterable[ComponentSpec]
|
||||
) -> dict[str, ComponentReport]:
|
||||
"""다중 컴포넌트 일괄 검출. 결과는 name → ComponentReport."""
|
||||
return {spec.name: detect_component(msp, spec) for spec in specs}
|
||||
|
||||
|
||||
def summary_line(reports: dict[str, ComponentReport]) -> str:
|
||||
"""검출 결과를 raw_text_annotations에 넣기 좋은 한 줄 요약."""
|
||||
parts = []
|
||||
for name, rep in reports.items():
|
||||
parts.append(f"{name}={rep.present}(g={rep.geom_count},t={rep.text_count})")
|
||||
return "[detect] " + ", ".join(parts)
|
||||
Reference in New Issue
Block a user