Add source code, design assets, and CAD samples
This commit is contained in:
584
dxf_geometry.py
Normal file
584
dxf_geometry.py
Normal file
@@ -0,0 +1,584 @@
|
||||
"""DXF 지오메트리 추출 공통 유틸리티.
|
||||
|
||||
모든 구조물 템플릿이 공유하는 DXF 처리 로직:
|
||||
1. 단위 자동 감지 (mm vs m)
|
||||
2. 주석/치수/해치 레이어 자동 필터링
|
||||
3. LINE/LWPOLYLINE/POLYLINE/ARC/SPLINE/CIRCLE 통합 추출
|
||||
4. 뷰 영역 자동 분할 (평면/정면/측면)
|
||||
|
||||
사용법:
|
||||
from dxf_geometry import extract_structural_geometry
|
||||
|
||||
result = extract_structural_geometry(dxf_path)
|
||||
for layer_name, shapes in result.by_layer.items():
|
||||
for shape in shapes:
|
||||
# shape.points: [(x, y), ...] (단위 정규화 완료)
|
||||
# shape.closed: bool
|
||||
# shape.kind: "polyline" | "line" | "arc" | ...
|
||||
...
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import ezdxf
|
||||
import numpy as np
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 데이터 구조
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class Shape:
|
||||
"""추출된 단일 지오메트리 요소 (단위: m)."""
|
||||
kind: str # "polyline" | "line" | "arc" | "circle"
|
||||
layer: str
|
||||
points: list # [(x, y), ...] — m 단위
|
||||
closed: bool = False
|
||||
extra: dict = field(default_factory=dict) # 부가정보 (center, radius, angles 등)
|
||||
|
||||
@property
|
||||
def bbox(self) -> tuple[float, float, float, float]:
|
||||
"""(xmin, ymin, xmax, ymax)"""
|
||||
if not self.points:
|
||||
return (0, 0, 0, 0)
|
||||
arr = np.array(self.points)
|
||||
return (float(arr[:, 0].min()), float(arr[:, 1].min()),
|
||||
float(arr[:, 0].max()), float(arr[:, 1].max()))
|
||||
|
||||
@property
|
||||
def centroid(self) -> tuple[float, float]:
|
||||
if not self.points:
|
||||
return (0, 0)
|
||||
arr = np.array(self.points)
|
||||
return (float(arr[:, 0].mean()), float(arr[:, 1].mean()))
|
||||
|
||||
@property
|
||||
def area(self) -> float:
|
||||
"""closed polygon 면적 (shoelace). open이면 0."""
|
||||
if not self.closed or len(self.points) < 3:
|
||||
return 0.0
|
||||
n = len(self.points)
|
||||
s = 0.0
|
||||
for i in range(n):
|
||||
x1, y1 = self.points[i]
|
||||
x2, y2 = self.points[(i + 1) % n]
|
||||
s += x1 * y2 - x2 * y1
|
||||
return abs(s) / 2
|
||||
|
||||
@property
|
||||
def length(self) -> float:
|
||||
"""폴리라인 누적 길이."""
|
||||
if len(self.points) < 2:
|
||||
return 0.0
|
||||
arr = np.array(self.points)
|
||||
diffs = np.diff(arr, axis=0)
|
||||
return float(np.sum(np.linalg.norm(diffs, axis=1)))
|
||||
|
||||
|
||||
@dataclass
|
||||
class GeometryResult:
|
||||
"""DXF에서 추출된 전체 지오메트리."""
|
||||
dxf_path: str
|
||||
unit_scale: float = 1.0 # 원본 → m 변환 계수 (mm이면 0.001)
|
||||
detected_unit: str = "m" # "mm" | "m"
|
||||
shapes: list[Shape] = field(default_factory=list)
|
||||
|
||||
# 분류별 접근 편의 속성
|
||||
by_layer: dict[str, list[Shape]] = field(default_factory=dict)
|
||||
closed_shapes: list[Shape] = field(default_factory=list)
|
||||
open_shapes: list[Shape] = field(default_factory=list)
|
||||
|
||||
# 메타
|
||||
total_bounds: tuple[float, float, float, float] = (0, 0, 0, 0)
|
||||
raw_text_count: int = 0
|
||||
dimension_count: int = 0
|
||||
excluded_layers: list[str] = field(default_factory=list)
|
||||
|
||||
def largest_closed(self) -> Optional[Shape]:
|
||||
"""면적이 가장 큰 closed shape 반환."""
|
||||
if not self.closed_shapes:
|
||||
return None
|
||||
return max(self.closed_shapes, key=lambda s: s.area)
|
||||
|
||||
def longest_polyline(self) -> Optional[Shape]:
|
||||
"""가장 긴 폴리라인 반환."""
|
||||
polys = [s for s in self.shapes if s.kind == "polyline" and len(s.points) > 2]
|
||||
if not polys:
|
||||
return None
|
||||
return max(polys, key=lambda s: s.length)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 레이어 필터링
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# 구조물 지오메트리로 간주하지 않는 레이어 이름 패턴
|
||||
# (대소문자 무시, 부분 매칭)
|
||||
# 주의: MZ-HIDL(숨김선)은 도면에 따라 주 구조물이 있을 수 있어 제외 대상에서 뺌
|
||||
_EXCLUDE_LAYER_PATTERNS = [
|
||||
# 치수선
|
||||
r"^DIM$", r"-DIM$", r"^CS-DIM", r"^MZ-DIM", r"치수선",
|
||||
# 해치/패턴
|
||||
r"^HATCH$", r"-HATCH$", r"^CS-PATT", r"^CZ-PATT", r"^MZ-HATCH",
|
||||
r"^CS-HATCH", r"^PATT-", r"해치",
|
||||
# 텍스트 (레이어명이 명확히 TEXT 전용인 경우만)
|
||||
r"^CS-TEXT$", r"^CZ-TEXT$", r"^CX-BORD", r"^TEXT$", r"^문자$",
|
||||
r"^CZ-TEX[0-9]", r"^텍스트$",
|
||||
# 지시선/리더
|
||||
r"^CS-LEAT$", r"^CS-LEAL$", r"^CS-LEA$", r"^지시선$", r"^LEADER$",
|
||||
# 표/프레임
|
||||
r"^CS-TABL",
|
||||
# 중심선 (완전히 중심선 전용 레이어만)
|
||||
r"^중심선\(", r"중심선$",
|
||||
# 기타 주석
|
||||
r"^Defpoints$", r"^심볼$", r"^SYMB$", r"^AA-",
|
||||
]
|
||||
|
||||
|
||||
def is_excluded_layer(layer_name: str) -> bool:
|
||||
"""레이어명이 주석/치수/해치 등 비구조 레이어인지 확인."""
|
||||
if not layer_name:
|
||||
return False
|
||||
for pat in _EXCLUDE_LAYER_PATTERNS:
|
||||
if re.search(pat, layer_name, re.IGNORECASE):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 단위 자동 감지
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def detect_unit_scale(doc) -> tuple[float, str]:
|
||||
"""DXF에서 단위를 자동 감지하여 m로 변환할 scale 반환.
|
||||
|
||||
한국 토목도면은 $INSUNITS가 잘못 설정되는 경우가 많으므로
|
||||
DIMENSION 값을 최우선 판단 기준으로 사용.
|
||||
|
||||
판단 순서:
|
||||
1. DIMENSION 값 분포 (최우선) — 치수 값이 설계 의도를 반영
|
||||
2. $INSUNITS header variable (보조 확인)
|
||||
3. 전체 bbox 크기 (폴백)
|
||||
|
||||
Returns:
|
||||
(scale, unit_name): 원본 × scale = m. unit_name은 "mm"/"m"/"cm"
|
||||
"""
|
||||
msp = doc.modelspace()
|
||||
|
||||
# 1) DIMENSION 값 분포 분석 (가장 신뢰할 만한 신호)
|
||||
dim_values = []
|
||||
for e in msp:
|
||||
if e.dxftype() == "DIMENSION":
|
||||
try:
|
||||
m = e.dxf.get("actual_measurement", None)
|
||||
if m is not None and m > 0:
|
||||
dim_values.append(float(m))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if len(dim_values) >= 3:
|
||||
median_val = float(np.median(dim_values))
|
||||
# 토목 구조물 치수 관례:
|
||||
# - m 단위: 중앙값이 보통 0.5~100 범위
|
||||
# - mm 단위: 중앙값이 보통 100~100,000 범위
|
||||
# - cm 단위: 중앙값이 50~10,000 범위 (드뭄)
|
||||
if median_val >= 100:
|
||||
return 0.001, "mm"
|
||||
elif median_val >= 1 and median_val < 100:
|
||||
return 1.0, "m"
|
||||
|
||||
# 2) $INSUNITS header (DIMENSION이 없거나 모호할 때)
|
||||
try:
|
||||
insunits = int(doc.header.get("$INSUNITS", 0))
|
||||
# 0=unitless, 1=inch, 2=feet, 4=mm, 5=cm, 6=m
|
||||
if insunits == 4:
|
||||
return 0.001, "mm"
|
||||
elif insunits == 6:
|
||||
return 1.0, "m"
|
||||
elif insunits == 5:
|
||||
return 0.01, "cm"
|
||||
# inch/feet는 한국 토목도면에서 거의 없음 → 무시하고 bbox로 판단
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 3) Bbox 크기로 추정
|
||||
try:
|
||||
# 엔티티 bbox 직접 계산 (header extents는 부정확할 수 있음)
|
||||
all_x = []
|
||||
all_y = []
|
||||
for e in msp:
|
||||
if e.dxftype() in ("LWPOLYLINE", "LINE", "CIRCLE", "ARC"):
|
||||
try:
|
||||
if e.dxftype() == "LWPOLYLINE":
|
||||
for p in e.get_points():
|
||||
all_x.append(p[0])
|
||||
all_y.append(p[1])
|
||||
elif e.dxftype() == "LINE":
|
||||
all_x.extend([e.dxf.start.x, e.dxf.end.x])
|
||||
all_y.extend([e.dxf.start.y, e.dxf.end.y])
|
||||
elif e.dxftype() in ("CIRCLE", "ARC"):
|
||||
all_x.append(e.dxf.center.x)
|
||||
all_y.append(e.dxf.center.y)
|
||||
except Exception:
|
||||
pass
|
||||
# 성능: 처음 1000개만 샘플
|
||||
if len(all_x) > 1000:
|
||||
break
|
||||
|
||||
if all_x and all_y:
|
||||
diag = math.sqrt((max(all_x) - min(all_x)) ** 2 + (max(all_y) - min(all_y)) ** 2)
|
||||
# 토목구조물 전체 크기: 수 m ~ 수백 m
|
||||
# mm이면 대각선 수천 ~ 수십만
|
||||
if diag > 2000:
|
||||
return 0.001, "mm"
|
||||
elif diag < 500:
|
||||
return 1.0, "m"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 기본값: 토목도면은 대부분 mm
|
||||
return 0.001, "mm"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 메인 추출 함수
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def extract_structural_geometry(
|
||||
dxf_path: str | Path,
|
||||
exclude_layers: bool = True,
|
||||
include_open: bool = True,
|
||||
min_points: int = 2,
|
||||
unit_override: Optional[str] = None,
|
||||
explode_blocks: bool = False,
|
||||
max_block_depth: int = 4,
|
||||
) -> GeometryResult:
|
||||
"""DXF에서 구조물 지오메트리를 추출.
|
||||
|
||||
Args:
|
||||
dxf_path: DXF 파일 경로
|
||||
exclude_layers: True면 주석/치수/해치 레이어 제외
|
||||
include_open: False면 closed 지오메트리만
|
||||
min_points: 최소 점 개수
|
||||
unit_override: "mm" | "m" 중 하나면 자동 감지 무시
|
||||
explode_blocks: True면 INSERT(블록 참조)를 virtual_entities로 재귀 확장.
|
||||
평면도에 구조물이 블록으로 배치된 경우 필수.
|
||||
max_block_depth: 중첩 블록 재귀 최대 깊이
|
||||
|
||||
Returns:
|
||||
GeometryResult
|
||||
"""
|
||||
doc = ezdxf.readfile(str(dxf_path))
|
||||
msp = doc.modelspace()
|
||||
|
||||
# 단위 감지
|
||||
if unit_override == "mm":
|
||||
unit_scale, unit_name = 0.001, "mm"
|
||||
elif unit_override == "m":
|
||||
unit_scale, unit_name = 1.0, "m"
|
||||
else:
|
||||
unit_scale, unit_name = detect_unit_scale(doc)
|
||||
|
||||
result = GeometryResult(
|
||||
dxf_path=str(dxf_path),
|
||||
unit_scale=unit_scale,
|
||||
detected_unit=unit_name,
|
||||
)
|
||||
|
||||
excluded_set = set()
|
||||
|
||||
def _process_entity(entity, inherited_layer: str = None, depth: int = 0):
|
||||
"""단일 엔티티 처리. INSERT면 explode_blocks에 따라 재귀 확장."""
|
||||
etype = entity.dxftype()
|
||||
# 블록 내부 엔티티의 layer가 "0"이면 INSERT의 레이어를 상속
|
||||
raw_layer = getattr(entity.dxf, "layer", "")
|
||||
if inherited_layer and raw_layer in ("", "0"):
|
||||
layer = inherited_layer
|
||||
else:
|
||||
layer = raw_layer
|
||||
|
||||
# 레이어 필터
|
||||
if exclude_layers and is_excluded_layer(layer):
|
||||
excluded_set.add(layer)
|
||||
return
|
||||
|
||||
# INSERT 재귀 확장
|
||||
if etype == "INSERT":
|
||||
if not explode_blocks or depth >= max_block_depth:
|
||||
return
|
||||
try:
|
||||
for sub in entity.virtual_entities():
|
||||
_process_entity(sub, inherited_layer=layer, depth=depth + 1)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
|
||||
# 엔티티별 추출
|
||||
try:
|
||||
shape = _extract_entity(entity, etype, unit_scale)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
if shape is None:
|
||||
return
|
||||
if len(shape.points) < min_points:
|
||||
return
|
||||
if not include_open and not shape.closed:
|
||||
return
|
||||
|
||||
shape.layer = layer
|
||||
result.shapes.append(shape)
|
||||
|
||||
# 레이어별 집계
|
||||
result.by_layer.setdefault(layer, []).append(shape)
|
||||
|
||||
if shape.closed:
|
||||
result.closed_shapes.append(shape)
|
||||
else:
|
||||
result.open_shapes.append(shape)
|
||||
|
||||
for entity in msp:
|
||||
_process_entity(entity)
|
||||
|
||||
# 메타 카운트
|
||||
for e in msp:
|
||||
et = e.dxftype()
|
||||
if et in ("TEXT", "MTEXT"):
|
||||
result.raw_text_count += 1
|
||||
elif et == "DIMENSION":
|
||||
result.dimension_count += 1
|
||||
|
||||
result.excluded_layers = sorted(excluded_set)
|
||||
|
||||
# 전체 bbox
|
||||
if result.shapes:
|
||||
all_pts = []
|
||||
for s in result.shapes:
|
||||
all_pts.extend(s.points)
|
||||
arr = np.array(all_pts)
|
||||
result.total_bounds = (
|
||||
float(arr[:, 0].min()), float(arr[:, 1].min()),
|
||||
float(arr[:, 0].max()), float(arr[:, 1].max()),
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _extract_entity(entity, etype: str, scale: float) -> Optional[Shape]:
|
||||
"""개별 DXF 엔티티 → Shape 변환."""
|
||||
if etype == "LWPOLYLINE":
|
||||
pts = [(p[0] * scale, p[1] * scale) for p in entity.get_points()]
|
||||
if len(pts) < 2:
|
||||
return None
|
||||
return Shape(kind="polyline", layer="", points=pts, closed=entity.closed)
|
||||
|
||||
elif etype == "POLYLINE":
|
||||
pts = [(v.dxf.location.x * scale, v.dxf.location.y * scale) for v in entity.vertices]
|
||||
if len(pts) < 2:
|
||||
return None
|
||||
return Shape(kind="polyline", layer="", points=pts, closed=entity.is_closed)
|
||||
|
||||
elif etype == "LINE":
|
||||
s = entity.dxf.start
|
||||
e = entity.dxf.end
|
||||
return Shape(
|
||||
kind="line", layer="",
|
||||
points=[(s.x * scale, s.y * scale), (e.x * scale, e.y * scale)],
|
||||
closed=False,
|
||||
)
|
||||
|
||||
elif etype == "ARC":
|
||||
c = entity.dxf.center
|
||||
r = entity.dxf.radius * scale
|
||||
sa = math.radians(entity.dxf.start_angle)
|
||||
ea = math.radians(entity.dxf.end_angle)
|
||||
if ea < sa:
|
||||
ea += 2 * math.pi
|
||||
n = max(8, int((ea - sa) / math.radians(5)))
|
||||
pts = []
|
||||
for i in range(n + 1):
|
||||
t = sa + (ea - sa) * i / n
|
||||
pts.append((c.x * scale + r * math.cos(t), c.y * scale + r * math.sin(t)))
|
||||
return Shape(
|
||||
kind="arc", layer="", points=pts, closed=False,
|
||||
extra={"center": (c.x * scale, c.y * scale), "radius": r,
|
||||
"start_angle": sa, "end_angle": ea},
|
||||
)
|
||||
|
||||
elif etype == "CIRCLE":
|
||||
c = entity.dxf.center
|
||||
r = entity.dxf.radius * scale
|
||||
n = 32
|
||||
pts = []
|
||||
for i in range(n + 1):
|
||||
t = 2 * math.pi * i / n
|
||||
pts.append((c.x * scale + r * math.cos(t), c.y * scale + r * math.sin(t)))
|
||||
return Shape(
|
||||
kind="circle", layer="", points=pts, closed=True,
|
||||
extra={"center": (c.x * scale, c.y * scale), "radius": r},
|
||||
)
|
||||
|
||||
elif etype == "SPLINE":
|
||||
try:
|
||||
# 제어점으로 근사
|
||||
pts = [(pt[0] * scale, pt[1] * scale) for pt in entity.control_points]
|
||||
if len(pts) < 2:
|
||||
return None
|
||||
return Shape(
|
||||
kind="polyline", layer="", points=pts, closed=entity.closed,
|
||||
)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
elif etype == "ELLIPSE":
|
||||
# 타원 → 폴리라인 샘플링
|
||||
try:
|
||||
c = entity.dxf.center
|
||||
# flattening으로 점 목록 얻기
|
||||
pts = []
|
||||
for pt in entity.flattening(distance=0.1):
|
||||
pts.append((pt.x * scale, pt.y * scale))
|
||||
if len(pts) < 2:
|
||||
return None
|
||||
return Shape(kind="polyline", layer="", points=pts, closed=True)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 뷰 영역 분할 (평면/정면/측면)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def split_views_by_y(result: GeometryResult, n_views: int = 2) -> list[GeometryResult]:
|
||||
"""Y 좌표 분포로 도면을 n개 뷰로 분할.
|
||||
|
||||
토목도면은 보통 평면(위)/정면(아래) 또는 측면/단면을 수직 배치.
|
||||
"""
|
||||
if not result.shapes or n_views < 2:
|
||||
return [result]
|
||||
|
||||
# Y 좌표 집계
|
||||
y_vals = []
|
||||
for s in result.shapes:
|
||||
arr = np.array(s.points)
|
||||
y_vals.append(arr[:, 1].mean())
|
||||
y_vals = np.array(y_vals)
|
||||
|
||||
# 분위수로 분할
|
||||
thresholds = np.quantile(y_vals, [i / n_views for i in range(1, n_views)])
|
||||
|
||||
views = [[] for _ in range(n_views)]
|
||||
for i, s in enumerate(result.shapes):
|
||||
bucket = n_views - 1
|
||||
for ti, t in enumerate(thresholds):
|
||||
if y_vals[i] <= t:
|
||||
bucket = ti
|
||||
break
|
||||
views[bucket].append(s)
|
||||
|
||||
# 각 뷰를 GeometryResult로 래핑
|
||||
out = []
|
||||
for shapes in views:
|
||||
v = GeometryResult(
|
||||
dxf_path=result.dxf_path,
|
||||
unit_scale=result.unit_scale,
|
||||
detected_unit=result.detected_unit,
|
||||
)
|
||||
v.shapes = shapes
|
||||
for s in shapes:
|
||||
v.by_layer.setdefault(s.layer, []).append(s)
|
||||
if s.closed:
|
||||
v.closed_shapes.append(s)
|
||||
else:
|
||||
v.open_shapes.append(s)
|
||||
if shapes:
|
||||
all_pts = []
|
||||
for s in shapes:
|
||||
all_pts.extend(s.points)
|
||||
arr = np.array(all_pts)
|
||||
v.total_bounds = (
|
||||
float(arr[:, 0].min()), float(arr[:, 1].min()),
|
||||
float(arr[:, 0].max()), float(arr[:, 1].max()),
|
||||
)
|
||||
out.append(v)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 편의 함수
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def extract_all(dxf_paths: list[str], **kwargs) -> GeometryResult:
|
||||
"""여러 DXF 파일을 모두 파싱하여 단일 GeometryResult로 반환."""
|
||||
combined = GeometryResult(dxf_path=";".join(dxf_paths))
|
||||
|
||||
first_scale = None
|
||||
for p in dxf_paths:
|
||||
try:
|
||||
r = extract_structural_geometry(p, **kwargs)
|
||||
if first_scale is None:
|
||||
first_scale = r.unit_scale
|
||||
combined.unit_scale = r.unit_scale
|
||||
combined.detected_unit = r.detected_unit
|
||||
combined.shapes.extend(r.shapes)
|
||||
for layer, shapes in r.by_layer.items():
|
||||
combined.by_layer.setdefault(layer, []).extend(shapes)
|
||||
combined.closed_shapes.extend(r.closed_shapes)
|
||||
combined.open_shapes.extend(r.open_shapes)
|
||||
combined.raw_text_count += r.raw_text_count
|
||||
combined.dimension_count += r.dimension_count
|
||||
combined.excluded_layers.extend(r.excluded_layers)
|
||||
except Exception as e:
|
||||
print(f" 추출 실패 ({p}): {e}")
|
||||
|
||||
if combined.shapes:
|
||||
all_pts = []
|
||||
for s in combined.shapes:
|
||||
all_pts.extend(s.points)
|
||||
arr = np.array(all_pts)
|
||||
combined.total_bounds = (
|
||||
float(arr[:, 0].min()), float(arr[:, 1].min()),
|
||||
float(arr[:, 0].max()), float(arr[:, 1].max()),
|
||||
)
|
||||
|
||||
return combined
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 샘플 테스트
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
paths = sys.argv[1:]
|
||||
if not paths:
|
||||
base = Path("Gate_Sample")
|
||||
paths = [
|
||||
str(base / "12995740-M40-001 여수로 수문 설치도(1/2).dxf"),
|
||||
str(base / "12995740-M40-002 여수로 수문 설치도(2/2).dxf"),
|
||||
]
|
||||
|
||||
for p in paths:
|
||||
print(f"\n=== {Path(p).name} ===")
|
||||
r = extract_structural_geometry(p)
|
||||
print(f" 단위: {r.detected_unit} (scale={r.unit_scale})")
|
||||
print(f" 총 지오메트리: {len(r.shapes)}개")
|
||||
print(f" closed: {len(r.closed_shapes)}, open: {len(r.open_shapes)}")
|
||||
print(f" 레이어별:")
|
||||
for layer, shapes in sorted(r.by_layer.items(), key=lambda x: -len(x[1]))[:10]:
|
||||
print(f" {layer}: {len(shapes)}개")
|
||||
b = r.total_bounds
|
||||
print(f" bbox: ({b[0]:.2f}, {b[1]:.2f}) ~ ({b[2]:.2f}, {b[3]:.2f}) m")
|
||||
print(f" 제외된 레이어: {r.excluded_layers[:5]}")
|
||||
if r.largest_closed():
|
||||
lc = r.largest_closed()
|
||||
print(f" 최대 closed: {lc.layer} ({lc.area:.2f} m², {len(lc.points)}pts)")
|
||||
Reference in New Issue
Block a user