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>
136 lines
4.7 KiB
Python
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()
|