kintex/tools/cad/extract_trench_seed.py
zio 6be091aa7c feat(m2): V61 measured trench seed for K1 halls 1-5 from CAD
Owner directive 2026-07-15: seed real trench coordinates.

- tools/cad/extract_trench_seed.py: deterministic generator - extracts trench
  channels from CAD layer 5-tranchi (spelling differs from expected), merges
  paired channel edges into centerlines, emits supply points every 6m
- Measured layout (identical across halls 1-5, cross-validating band alignment):
  7 horizontal channels per hall, 9.0m pitch, y=5.85..59.8m local,
  extent x[20.6..170.2]m
- V61: delete assumed grid points for H1-H5, insert 875 measured points
  (is_assumed=false); supply flags keep V5 policy; K2 stays assumed (no trench
  layer in its plan drawing - power trunk drawing analysis is follow-up)

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

118 lines
4.9 KiB
Python

# -*- coding: utf-8 -*-
"""K1 트렌치 실측 시드 생성기 — CAD(전기,설비 트렌치.dwg → DXF)에서 홀1~5 트렌치 채널을
추출해 V61 마이그레이션 SQL을 산출한다(PLANNING R4 실측 대체).
실측 구조(레이어 5-트랜치): 홀당 가로 채널 7개(쌍선 폭 ~1.3m·피치 9m·로컬 y≈5.85 시작),
채널을 따라 6m 간격 공급 포인트를 생성한다(포인트 간격은 샘플링 — 채널 위치·연장은 실측).
사용:
python tools/cad/extract_trench_seed.py <k1_trench.dxf> <out.sql>
좌표 규약: CAD 전건물 mm → 홀 로컬 m. 홀 바닥 X원점 293.5m·폭 171m, Y밴드 80.1m+63m 피치
(밴드 n ↔ 홀 n 가정 — docs/analysis/cad-extraction.md 한계 참조).
"""
import io
import math
import sys
from collections import defaultdict
import ezdxf
MM = 1000.0
FLOOR_X0, FLOOR_W = 293.5, 171.0
BAND_Y0, BAND_H = 80.1, 63.0
HALLS = [1, 2, 3, 4, 5]
LAYER_KEYS = ("트렌치", "트랜치")
MIN_RUN_M = 30.0
POINT_STEP_M = 6.0
ROW_SNAP_M = 1.5 # 쌍선(채널 양변) 병합 허용
def main():
src, out_sql = sys.argv[1], sys.argv[2]
doc = ezdxf.readfile(src)
msp = doc.modelspace()
# 홀별 → 채널 행(스냅된 로컬 y) → x구간 목록
runs = {n: defaultdict(list) for n in HALLS}
for e in msp:
if not any(k in e.dxf.layer for k in LAYER_KEYS):
continue
pts = None
if e.dxftype() == "LINE":
pts = [(e.dxf.start[0] / MM, e.dxf.start[1] / MM), (e.dxf.end[0] / MM, e.dxf.end[1] / MM)]
elif e.dxftype() == "LWPOLYLINE":
p = [(q[0] / MM, q[1] / MM) for q in e.get_points()]
if len(p) >= 2:
pts = p
if not pts:
continue
for a, b in zip(pts, pts[1:]):
if math.hypot(b[0] - a[0], b[1] - a[1]) < MIN_RUN_M:
continue
if abs(b[1] - a[1]) > 0.5: # 가로 채널만(세로 피더는 M4 후속)
continue
my = (a[1] + b[1]) / 2
for n in HALLS:
y0 = BAND_Y0 + (n - 1) * BAND_H
if y0 <= my < y0 + BAND_H:
lx0 = max(0.0, min(a[0], b[0]) - FLOOR_X0)
lx1 = min(FLOOR_W, max(a[0], b[0]) - FLOOR_X0)
if lx1 - lx0 >= MIN_RUN_M:
runs[n][round(my - y0, 1)].append((lx0, lx1))
sql = io.StringIO()
sql.write(
"-- V61: K1 홀1~5 트렌치 실측 시드 — CAD(전기,설비 트렌치.dwg) 레이어 5-트랜치 추출.\n"
"-- 생성기: tools/cad/extract_trench_seed.py (재생성 가능·결정적). 근거: docs/analysis/cad-extraction.md.\n"
"-- 실측: 홀당 가로 채널(쌍선 병합 중심선)·연장 실좌표. 포인트 6m 간격은 샘플링 규약.\n"
"-- 정책: 실측 확보 홀(H1~H5)의 가정(is_assumed) 포인트를 실측 포인트로 대체. K2(H6~10)는 가정 유지.\n"
"-- 공급 플래그는 V5 정책 유지(공동구 — 전 서비스 true·gas는 홀 보유 여부), 위치만 실측 대체.\n\n"
)
total_rows = 0
report = []
for n in HALLS:
# 쌍선 병합: 인접(<=ROW_SNAP_M) 행을 채널 하나로
rows = sorted(runs[n].items())
merged = [] # (center_y, x0, x1)
for y, spans in rows:
x0 = min(s[0] for s in spans)
x1 = max(s[1] for s in spans)
if merged and y - merged[-1][0] <= ROW_SNAP_M:
py, px0, px1 = merged[-1]
merged[-1] = ((py + y) / 2, min(px0, x0), max(px1, x1))
else:
merged.append((y, x0, x1))
report.append(f"H{n}: 채널 {len(merged)}" + ", ".join(f"y={m[0]:.1f} x[{m[1]:.1f}..{m[2]:.1f}]" for m in merged))
if not merged:
continue
total_rows += len(merged)
sql.write(f"-- H{n}: 실측 채널 {len(merged)}\n")
sql.write(f"DELETE FROM trench WHERE hall_id = 'H{n}' AND is_assumed;\n")
values = []
for ri, (cy, x0, x1) in enumerate(merged, 1):
k = 0
x = x0
while x <= x1 + 1e-6:
k += 1
values.append(
f"('H{n}-RT-{ri}-{k}', 'H{n}', ST_SetSRID(ST_MakePoint({x:.2f}, {cy:.2f}), 0), "
f"true, true, true, true, (SELECT has_gas FROM hall WHERE id='H{n}'), false)"
)
x += POINT_STEP_M
sql.write(
"INSERT INTO trench (id, hall_id, geom, supply_power, supply_water, supply_air, supply_network, supply_gas, is_assumed)\nVALUES\n "
+ ",\n ".join(values)
+ "\nON CONFLICT (id) DO UPDATE SET geom = EXCLUDED.geom, is_assumed = EXCLUDED.is_assumed;\n\n"
)
with open(out_sql, "w", encoding="utf-8") as f:
f.write(sql.getvalue())
for line in report:
print(line)
print(f"OK -> {out_sql} (채널 {total_rows}개)")
if __name__ == "__main__":
main()