# -*- 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 좌표 규약: 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()