# -*- coding: utf-8 -*- """KINTEX CAD(DWG) 실측 추출 파이프라인 — PLANNING R4(트렌치·기둥·비상구 실측 대체). 흐름: DWG --(LibreDWG dwg2dxf)--> DXF --(ezdxf)--> 레이어 인벤토리 / 키워드 추출 / 그리드 앵커 탐지 사용: python tools/cad/dwg_extract.py inventory [out.txt] python tools/cad/dwg_extract.py extract <레이어키워드> [out.txt] # 예: 트렌치, COL python tools/cad/dwg_extract.py anchors [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()