kintex/tools/cad/dwg_extract.py
zio d8ef5c53df feat(m2): DWG/JPG floorplan underlay tabs + CAD extraction pipeline
Owner directives 2026-07-14~15: run the DWG pipeline; split floorplan into DWG and JPG tabs.

- tools/cad/dwg_extract.py: DWG(LibreDWG dwg2dxf) -> DXF -> layer inventory /
  keyword extraction / grid-label anchor detection (ezdxf, recover fallback)
- tools/cad/render_hall_png.py: render hall-band crops from the K1 combined
  plan+trench drawing (x0=293.5m, width 171m, Y bands 80.1m + 63m pitch,
  single load -> multi-crop). Real trench layout confirmed: 7 horizontal runs
  at ~9m pitch + central feeder (differs from assumed 6m grid seed - V61 later)
- public/media/floorplans/dwg/hall1..5.png: K1 hall underlays (3m margin crop,
  deterministic calibration from render params)
- Editor left panel new tool group with JPG/DWG tabs; DWG disabled for
  halls without CAD render yet (K2 pending). Canvas switches url+region.
- docs/analysis/cad-extraction.md: pipeline findings, limits (A3002 LibreDWG
  crash, band-to-hall numbering direction to confirm), next steps

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 00:35:12 +09:00

136 lines
4.7 KiB
Python

# -*- coding: utf-8 -*-
"""KINTEX CAD(DWG) 실측 추출 파이프라인 — PLANNING R4(트렌치·기둥·비상구 실측 대체).
흐름:
DWG --(LibreDWG dwg2dxf)--> DXF --(ezdxf)--> 레이어 인벤토리 / 키워드 추출 / 그리드 앵커 탐지
사용:
python tools/cad/dwg_extract.py inventory <dxf> [out.txt]
python tools/cad/dwg_extract.py extract <dxf> <레이어키워드> [out.txt] # 예: 트렌치, COL
python tools/cad/dwg_extract.py anchors <dxf> [out.txt] # 대형 라벨·그리드축 탐지
준비물(로컬 1회):
- pip install ezdxf
- LibreDWG win64(dwg2dxf.exe): https://github.com/LibreDWG/libredwg/releases
dwg2dxf.exe -o out.dxf in.dwg
단위: KINTEX 도면은 INSUNITS=4(mm) — 본 스크립트 출력은 m 환산.
2026-07-14 1차 분석 결과 요약은 docs/analysis/cad-extraction.md 참조.
"""
import io
import sys
from collections import Counter, defaultdict
import ezdxf
MM = 1000.0
def _points_of(e):
t = e.dxftype()
if t == "LINE":
return [(e.dxf.start[0], e.dxf.start[1]), (e.dxf.end[0], e.dxf.end[1])]
if t == "LWPOLYLINE":
return [(p[0], p[1]) for p in e.get_points()]
if t in ("INSERT", "TEXT", "MTEXT"):
p = e.dxf.insert
return [(p[0], p[1])]
if t == "CIRCLE":
p = e.dxf.center
return [(p[0], p[1])]
return []
def inventory(msp, rep):
by_layer = defaultdict(Counter)
for e in msp:
by_layer[e.dxf.layer][e.dxftype()] += 1
rep.write("레이어별 엔티티(상위 40):\n")
for layer, cnt in sorted(by_layer.items(), key=lambda kv: -sum(kv[1].values()))[:40]:
top = ", ".join(f"{t}:{c}" for t, c in cnt.most_common(4))
rep.write(f"{sum(cnt.values()):7d} {layer} [{top}]\n")
def extract(msp, keyword, rep):
segs = []
for e in msp:
if keyword.upper() not in e.dxf.layer.upper():
continue
pts = _points_of(e)
for a, b in zip(pts, pts[1:]):
segs.append((e.dxf.layer, a, b))
rep.write(f"'{keyword}' 레이어 세그먼트: {len(segs)}\n")
if not segs:
return
allp = [p for _, a, b in segs for p in (a, b)]
xs = [p[0] / MM for p in allp]
ys = [p[1] / MM for p in allp]
rep.write(f"bbox(m): x[{min(xs):.1f}..{max(xs):.1f}] y[{min(ys):.1f}..{max(ys):.1f}]\n")
for layer, a, b in segs[:80]:
rep.write(f" {layer}: ({a[0]/MM:.2f},{a[1]/MM:.2f}) -> ({b[0]/MM:.2f},{b[1]/MM:.2f})\n")
if len(segs) > 80:
rep.write(f" … 외 {len(segs)-80}건 생략\n")
def anchors(msp, rep):
"""대형 텍스트(도면 제목·홀 라벨)와 구조 그리드 축 라벨(X/Y·63m 피치) 탐지."""
texts = []
for e in msp:
if e.dxftype() not in ("TEXT", "MTEXT"):
continue
try:
txt = (e.dxf.text if e.dxftype() == "TEXT" else e.text).strip()
h = e.dxf.height if e.dxftype() == "TEXT" else e.dxf.char_height
p = e.dxf.insert
except Exception:
continue
if txt:
texts.append((h, txt, p[0] / MM, p[1] / MM))
texts.sort(key=lambda t: -t[0])
rep.write("대형 텍스트(상위 40):\n")
for h, t, x, y in texts[:40]:
rep.write(f" {h:7.0f}mm ({x:7.1f},{y:7.1f})m {t[:60]}\n")
ys = sorted(y for h, t, x, y in texts if t in ("Y", "RY"))
if len(ys) >= 3:
diffs = [round(b - a, 1) for a, b in zip(ys, ys[1:])]
rep.write(f"\nY 그리드 라벨 y좌표(m): {[round(v,1) for v in ys]}\n인접 간격: {diffs}\n")
rep.write("(63m 간격이면 = 홀 깊이 베이 — 홀별 Y밴드 앵커)\n")
def main():
if len(sys.argv) < 3:
print(__doc__)
sys.exit(1)
cmd, src = sys.argv[1], sys.argv[2]
try:
doc = ezdxf.readfile(src)
except ezdxf.DXFStructureError:
# LibreDWG 산출 DXF는 구조 결함이 있을 수 있음 — 관용 파서로 복구 로드.
from ezdxf import recover
doc, auditor = recover.readfile(src)
print(f"recover: errors={len(auditor.errors)} fixes={len(auditor.fixes)}", file=sys.stderr)
msp = doc.modelspace()
rep = io.StringIO()
rep.write(f"FILE {src}\nINSUNITS={doc.header.get('$INSUNITS', '?')}\n\n")
if cmd == "inventory":
inventory(msp, rep)
out_default = "cad_inventory.txt"
elif cmd == "extract":
extract(msp, sys.argv[3], rep)
out_default = "cad_extract.txt"
elif cmd == "anchors":
anchors(msp, rep)
out_default = "cad_anchors.txt"
else:
print(__doc__)
sys.exit(1)
out = sys.argv[-1] if sys.argv[-1].endswith(".txt") else out_default
with open(out, "w", encoding="utf-8") as f:
f.write(rep.getvalue())
print(f"OK -> {out}")
if __name__ == "__main__":
main()