feat(m2): V65 measured exit positions + canvas real trench data
Owner directives 2026-07-16 (items 2-3 of follow-up batch). - tools/cad/extract_exit_seed.py: boundary door clusters (+-2m band) from CAD DOOR layers projected onto hall edges -> V65 replaces assumed exits for H1-H8 (34 measured); H9/H10 keep assumed (0-1 boundary doors - low trust). Cross-validated against V63/V64 obstacle door strips (same source, matching) - Canvas trench layer: renders real supply points (HallInfo.trenches, r=0.5m dots with supplies tooltip) when present; falls back to synthetic 9m grid Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
355016445b
commit
d80b761983
@ -352,3 +352,7 @@ H6~H10 장애물 31개 스트립 시드(홀 프레임 회전 변환) — 에지
|
|||||||
### 세션 5 추가 — 개발→운영 확인 (2026-07-16, 소유자 지시 "git push -> 개발 -> 운영")
|
### 세션 5 추가 — 개발→운영 확인 (2026-07-16, 소유자 지시 "git push -> 개발 -> 운영")
|
||||||
|
|
||||||
git push 최신(HEAD 7f86104, Everything up-to-date). 운영 도메인 kintex.wise.ai.kr(CNAME→wise.ai.kr→211.37.173.197)가 이미 라이브 — 개발과 동일 산출물 서빙 확인(번들 Bb9GOk5Z·index.html mtime 초 단위 일치 = 프록시 또는 동일 호스트). 운영 도메인 기준 스모크 13/13 통과. V58~V64(관리자 통계·옥션 실시간·CMS 예약게시·홀 치수 정렬·구역 트리·트렌치 875·기둥 74·장애물 91) 전부 운영 반영 상태.
|
git push 최신(HEAD 7f86104, Everything up-to-date). 운영 도메인 kintex.wise.ai.kr(CNAME→wise.ai.kr→211.37.173.197)가 이미 라이브 — 개발과 동일 산출물 서빙 확인(번들 Bb9GOk5Z·index.html mtime 초 단위 일치 = 프록시 또는 동일 호스트). 운영 도메인 기준 스모크 13/13 통과. V58~V64(관리자 통계·옥션 실시간·CMS 예약게시·홀 치수 정렬·구역 트리·트렌치 875·기둥 74·장애물 91) 전부 운영 반영 상태.
|
||||||
|
|
||||||
|
### 세션 5 추가 — V65 비상구 실좌표 + 캔버스 트렌치 실데이터 (2026-07-16)
|
||||||
|
|
||||||
|
비상구: H1~H8 실측 34개(DOOR 경계 클러스터, 장애물 door 스트립과 교차 일치 검증), H9/H10 가정 유지(신뢰 부족 명시). 캔버스: 트렌치 실데이터 도트 표시+합성 그리드 폴백. 게이트 전부 통과 → push→배포. 잔여: K2 트렌치(전력간선 도면).
|
||||||
|
|||||||
@ -124,3 +124,12 @@ H7/H8 블록 252m=126×2·폭 90m 정확 일치) + 회전 프로브 렌더 시
|
|||||||
- 특기: H9/H10 midline(y≈66)의 전폭 door 스트립 = **9A/9B·10A/10B 분리 셔터 트랙**(가동벽) —
|
- 특기: H9/H10 midline(y≈66)의 전폭 door 스트립 = **9A/9B·10A/10B 분리 셔터 트랙**(가동벽) —
|
||||||
door 1.0m 버퍼 회피로 셔터 가동선 클리어 확보(운영 정합). H7/H8은 서/동측 에지 문 + midline 포켓.
|
door 1.0m 버퍼 회피로 셔터 가동선 클리어 확보(운영 정합). H7/H8은 서/동측 에지 문 + midline 포켓.
|
||||||
- 레이어: ELEVATOR·STAIR·DOOR·석고보드·경량철골·시멘트판넬·WALL 계열(k2_slim 구조 추출본).
|
- 레이어: ELEVATOR·STAIR·DOOR·석고보드·경량철골·시멘트판넬·WALL 계열(k2_slim 구조 추출본).
|
||||||
|
|
||||||
|
## V65 비상구 실좌표 + 캔버스 트렌치 실데이터 (2026-07-16)
|
||||||
|
|
||||||
|
- **V65 비상구**: CAD DOOR 레이어 경계 문 클러스터(±2m 밴드) 변 투영 — H1~H8 실측 34개로 가정
|
||||||
|
비상구(변 중앙 4개소) 대체, H9/H10은 경계 문 신뢰 부족(0~1개)으로 가정 유지.
|
||||||
|
V63/V64 장애물 door 스트립과 좌표 교차 일치(같은 소스) 검증. 생성기 extract_exit_seed.py.
|
||||||
|
- **캔버스 트렌치**: 합성 9m 그리드 → HallInfo.trenches 실데이터 공급 포인트 도트(r0.5m,
|
||||||
|
supplies 툴팁) 표시, 실데이터 없으면 합성 그리드 폴백 유지.
|
||||||
|
- **잔여**: K2 트렌치(전력간선 77MB 도면) — 별도 진행.
|
||||||
|
|||||||
@ -0,0 +1,88 @@
|
|||||||
|
-- V65: 비상구 실좌표 시드 — CAD DOOR 레이어 경계 문 클러스터(±2m 밴드) 변 투영.
|
||||||
|
-- 생성기: tools/cad/extract_exit_seed.py (결정적 재생성). 근거: docs/analysis/cad-extraction.md.
|
||||||
|
-- 실측 확보 홀만 가정(is_assumed) 비상구를 대체. 분리 셔터 등 12m 초과 장스팬 제외.
|
||||||
|
|
||||||
|
-- H1: 실측 비상구 5개
|
||||||
|
DELETE FROM hall_exit WHERE hall_id = 'H1' AND is_assumed;
|
||||||
|
INSERT INTO hall_exit (id, hall_id, geom, clearance_m, is_assumed)
|
||||||
|
VALUES
|
||||||
|
('H1-EXIT-R1', 'H1', ST_SetSRID(ST_MakePoint(5.5, 63.0), 0), 3.0, false),
|
||||||
|
('H1-EXIT-R2', 'H1', ST_SetSRID(ST_MakePoint(104.0, 63.0), 0), 3.0, false),
|
||||||
|
('H1-EXIT-R3', 'H1', ST_SetSRID(ST_MakePoint(106.5, 63.0), 0), 3.0, false),
|
||||||
|
('H1-EXIT-R4', 'H1', ST_SetSRID(ST_MakePoint(171.0, 1.0), 0), 3.0, false),
|
||||||
|
('H1-EXIT-R5', 'H1', ST_SetSRID(ST_MakePoint(171.0, 23.0), 0), 3.0, false)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET geom = EXCLUDED.geom, is_assumed = EXCLUDED.is_assumed;
|
||||||
|
|
||||||
|
-- H2: 실측 비상구 4개
|
||||||
|
DELETE FROM hall_exit WHERE hall_id = 'H2' AND is_assumed;
|
||||||
|
INSERT INTO hall_exit (id, hall_id, geom, clearance_m, is_assumed)
|
||||||
|
VALUES
|
||||||
|
('H2-EXIT-R1', 'H2', ST_SetSRID(ST_MakePoint(171.0, 1.0), 0), 3.0, false),
|
||||||
|
('H2-EXIT-R2', 'H2', ST_SetSRID(ST_MakePoint(171.0, 21.0), 0), 3.0, false),
|
||||||
|
('H2-EXIT-R3', 'H2', ST_SetSRID(ST_MakePoint(171.0, 32.0), 0), 3.0, false),
|
||||||
|
('H2-EXIT-R4', 'H2', ST_SetSRID(ST_MakePoint(171.0, 62.5), 0), 3.0, false)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET geom = EXCLUDED.geom, is_assumed = EXCLUDED.is_assumed;
|
||||||
|
|
||||||
|
-- H3: 실측 비상구 4개
|
||||||
|
DELETE FROM hall_exit WHERE hall_id = 'H3' AND is_assumed;
|
||||||
|
INSERT INTO hall_exit (id, hall_id, geom, clearance_m, is_assumed)
|
||||||
|
VALUES
|
||||||
|
('H3-EXIT-R1', 'H3', ST_SetSRID(ST_MakePoint(171.0, 1.0), 0), 3.0, false),
|
||||||
|
('H3-EXIT-R2', 'H3', ST_SetSRID(ST_MakePoint(171.0, 21.0), 0), 3.0, false),
|
||||||
|
('H3-EXIT-R3', 'H3', ST_SetSRID(ST_MakePoint(171.0, 32.0), 0), 3.0, false),
|
||||||
|
('H3-EXIT-R4', 'H3', ST_SetSRID(ST_MakePoint(171.0, 62.5), 0), 3.0, false)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET geom = EXCLUDED.geom, is_assumed = EXCLUDED.is_assumed;
|
||||||
|
|
||||||
|
-- H4: 실측 비상구 4개
|
||||||
|
DELETE FROM hall_exit WHERE hall_id = 'H4' AND is_assumed;
|
||||||
|
INSERT INTO hall_exit (id, hall_id, geom, clearance_m, is_assumed)
|
||||||
|
VALUES
|
||||||
|
('H4-EXIT-R1', 'H4', ST_SetSRID(ST_MakePoint(171.0, 1.0), 0), 3.0, false),
|
||||||
|
('H4-EXIT-R2', 'H4', ST_SetSRID(ST_MakePoint(171.0, 19.5), 0), 3.0, false),
|
||||||
|
('H4-EXIT-R3', 'H4', ST_SetSRID(ST_MakePoint(171.0, 32.0), 0), 3.0, false),
|
||||||
|
('H4-EXIT-R4', 'H4', ST_SetSRID(ST_MakePoint(171.0, 62.5), 0), 3.0, false)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET geom = EXCLUDED.geom, is_assumed = EXCLUDED.is_assumed;
|
||||||
|
|
||||||
|
-- H5: 실측 비상구 3개
|
||||||
|
DELETE FROM hall_exit WHERE hall_id = 'H5' AND is_assumed;
|
||||||
|
INSERT INTO hall_exit (id, hall_id, geom, clearance_m, is_assumed)
|
||||||
|
VALUES
|
||||||
|
('H5-EXIT-R1', 'H5', ST_SetSRID(ST_MakePoint(171.0, 1.0), 0), 3.0, false),
|
||||||
|
('H5-EXIT-R2', 'H5', ST_SetSRID(ST_MakePoint(171.0, 32.0), 0), 3.0, false),
|
||||||
|
('H5-EXIT-R3', 'H5', ST_SetSRID(ST_MakePoint(171.0, 62.5), 0), 3.0, false)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET geom = EXCLUDED.geom, is_assumed = EXCLUDED.is_assumed;
|
||||||
|
|
||||||
|
-- H6: 실측 비상구 8개
|
||||||
|
DELETE FROM hall_exit WHERE hall_id = 'H6' AND is_assumed;
|
||||||
|
INSERT INTO hall_exit (id, hall_id, geom, clearance_m, is_assumed)
|
||||||
|
VALUES
|
||||||
|
('H6-EXIT-R1', 'H6', ST_SetSRID(ST_MakePoint(0.0, 24.5), 0), 3.0, false),
|
||||||
|
('H6-EXIT-R2', 'H6', ST_SetSRID(ST_MakePoint(0.0, 26.5), 0), 3.0, false),
|
||||||
|
('H6-EXIT-R3', 'H6', ST_SetSRID(ST_MakePoint(0.0, 29.0), 0), 3.0, false),
|
||||||
|
('H6-EXIT-R4', 'H6', ST_SetSRID(ST_MakePoint(0.0, 42.5), 0), 3.0, false),
|
||||||
|
('H6-EXIT-R5', 'H6', ST_SetSRID(ST_MakePoint(0.0, 51.5), 0), 3.0, false),
|
||||||
|
('H6-EXIT-R6', 'H6', ST_SetSRID(ST_MakePoint(0.0, 67.5), 0), 3.0, false),
|
||||||
|
('H6-EXIT-R7', 'H6', ST_SetSRID(ST_MakePoint(0.0, 69.5), 0), 3.0, false),
|
||||||
|
('H6-EXIT-R8', 'H6', ST_SetSRID(ST_MakePoint(1.5, 93.0), 0), 3.0, false)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET geom = EXCLUDED.geom, is_assumed = EXCLUDED.is_assumed;
|
||||||
|
|
||||||
|
-- H7: 실측 비상구 2개
|
||||||
|
DELETE FROM hall_exit WHERE hall_id = 'H7' AND is_assumed;
|
||||||
|
INSERT INTO hall_exit (id, hall_id, geom, clearance_m, is_assumed)
|
||||||
|
VALUES
|
||||||
|
('H7-EXIT-R1', 'H7', ST_SetSRID(ST_MakePoint(90, 31.0), 0), 3.0, false),
|
||||||
|
('H7-EXIT-R2', 'H7', ST_SetSRID(ST_MakePoint(90, 94.5), 0), 3.0, false)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET geom = EXCLUDED.geom, is_assumed = EXCLUDED.is_assumed;
|
||||||
|
|
||||||
|
-- H8: 실측 비상구 4개
|
||||||
|
DELETE FROM hall_exit WHERE hall_id = 'H8' AND is_assumed;
|
||||||
|
INSERT INTO hall_exit (id, hall_id, geom, clearance_m, is_assumed)
|
||||||
|
VALUES
|
||||||
|
('H8-EXIT-R1', 'H8', ST_SetSRID(ST_MakePoint(0.5, 0.0), 0), 3.0, false),
|
||||||
|
('H8-EXIT-R2', 'H8', ST_SetSRID(ST_MakePoint(90, 22.0), 0), 3.0, false),
|
||||||
|
('H8-EXIT-R3', 'H8', ST_SetSRID(ST_MakePoint(90, 31.5), 0), 3.0, false),
|
||||||
|
('H8-EXIT-R4', 'H8', ST_SetSRID(ST_MakePoint(90, 94.5), 0), 3.0, false)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET geom = EXCLUDED.geom, is_assumed = EXCLUDED.is_assumed;
|
||||||
|
|
||||||
|
-- H9: 경계 문 0개 — 신뢰 부족, 가정 비상구 유지
|
||||||
|
-- H10: 경계 문 1개 — 신뢰 부족, 가정 비상구 유지
|
||||||
@ -170,6 +170,14 @@ export interface HallPillarDto {
|
|||||||
y: number;
|
y: number;
|
||||||
radiusM: number;
|
radiusM: number;
|
||||||
}
|
}
|
||||||
|
/** 트렌치 공급 포인트(V61/V65 실측 또는 가정 그리드) — 캔버스 표시·M4 배선 근거. */
|
||||||
|
export interface HallTrenchDto {
|
||||||
|
trenchId: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
assumed: boolean;
|
||||||
|
supplies: string[]; // power | water | air | network | gas
|
||||||
|
}
|
||||||
/** 벽·계단·엘리베이터·문 장애물(V63 실측 스트립) — 자동배치 회피·캔버스 표시. */
|
/** 벽·계단·엘리베이터·문 장애물(V63 실측 스트립) — 자동배치 회피·캔버스 표시. */
|
||||||
export interface HallObstacleDto {
|
export interface HallObstacleDto {
|
||||||
obstacleId: string;
|
obstacleId: string;
|
||||||
@ -188,6 +196,7 @@ export interface HallInfoDto {
|
|||||||
targetBoothCount?: number | null;
|
targetBoothCount?: number | null;
|
||||||
pillars?: HallPillarDto[] | null;
|
pillars?: HallPillarDto[] | null;
|
||||||
obstacles?: HallObstacleDto[] | null;
|
obstacles?: HallObstacleDto[] | null;
|
||||||
|
trenches?: HallTrenchDto[] | null;
|
||||||
}
|
}
|
||||||
export interface LayoutDto {
|
export interface LayoutDto {
|
||||||
layoutId: string;
|
layoutId: string;
|
||||||
|
|||||||
@ -256,6 +256,7 @@ export function BoothLayoutEditorPage() {
|
|||||||
floorRegion={drawingTab === 'dwg' ? hallDwgRegion(hallId) : hallFloorRegion(hallId)}
|
floorRegion={drawingTab === 'dwg' ? hallDwgRegion(hallId) : hallFloorRegion(hallId)}
|
||||||
pillars={layout.hall?.pillars}
|
pillars={layout.hall?.pillars}
|
||||||
obstacles={layout.hall?.obstacles}
|
obstacles={layout.hall?.obstacles}
|
||||||
|
trenches={layout.hall?.trenches}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import type { BoothDto, HallObstacleDto, HallPillarDto } from '../../api/types';
|
import type { BoothDto, HallObstacleDto, HallPillarDto, HallTrenchDto } from '../../api/types';
|
||||||
import './canvas.css';
|
import './canvas.css';
|
||||||
|
|
||||||
const ZOOM_MIN = 0.3;
|
const ZOOM_MIN = 0.3;
|
||||||
@ -41,6 +41,8 @@ interface FloorplanCanvasProps {
|
|||||||
pillars?: HallPillarDto[] | null;
|
pillars?: HallPillarDto[] | null;
|
||||||
/** 벽·계단·엘리베이터·문 장애물(V63 실측) — 구조 레이어에 사각으로 표시. */
|
/** 벽·계단·엘리베이터·문 장애물(V63 실측) — 구조 레이어에 사각으로 표시. */
|
||||||
obstacles?: HallObstacleDto[] | null;
|
obstacles?: HallObstacleDto[] | null;
|
||||||
|
/** 트렌치 공급 포인트(V61/V65) — 있으면 실데이터 도트, 없으면 합성 9m 그리드 폴백. */
|
||||||
|
trenches?: HallTrenchDto[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TRENCH_SPACING_M = 9; // 가정 트렌치 그리드 간격(실측 대체 — R4)
|
const TRENCH_SPACING_M = 9; // 가정 트렌치 그리드 간격(실측 대체 — R4)
|
||||||
@ -69,6 +71,7 @@ export function FloorplanCanvas({
|
|||||||
onZoomChange,
|
onZoomChange,
|
||||||
pillars,
|
pillars,
|
||||||
obstacles,
|
obstacles,
|
||||||
|
trenches,
|
||||||
}: FloorplanCanvasProps) {
|
}: FloorplanCanvasProps) {
|
||||||
const [hw, hh] = hallDims;
|
const [hw, hh] = hallDims;
|
||||||
|
|
||||||
@ -228,14 +231,23 @@ export function FloorplanCanvas({
|
|||||||
</g>
|
</g>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 트렌치 그리드 */}
|
{/* 트렌치 — DB 실데이터(공급 포인트 도트) 우선, 없으면 합성 9m 그리드 폴백 */}
|
||||||
{layers.trench && (
|
{layers.trench &&
|
||||||
<g className="kx-canvas__trench">
|
(trenches && trenches.length > 0 ? (
|
||||||
{trenchLines.map((l, i) => (
|
<g className="kx-canvas__trench-points" aria-hidden="true">
|
||||||
<line key={i} x1={l.x1} y1={l.y1} x2={l.x2} y2={l.y2} />
|
{trenches.map((t) => (
|
||||||
))}
|
<circle key={t.trenchId} cx={t.x} cy={t.y} r={0.5}>
|
||||||
</g>
|
<title>{`${t.trenchId} — ${t.supplies.join('/')}`}</title>
|
||||||
)}
|
</circle>
|
||||||
|
))}
|
||||||
|
</g>
|
||||||
|
) : (
|
||||||
|
<g className="kx-canvas__trench">
|
||||||
|
{trenchLines.map((l, i) => (
|
||||||
|
<line key={i} x1={l.x1} y1={l.y1} x2={l.x2} y2={l.y2} />
|
||||||
|
))}
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
|
|
||||||
{/* 부스 폴리곤 */}
|
{/* 부스 폴리곤 */}
|
||||||
<g className="kx-canvas__booths">
|
<g className="kx-canvas__booths">
|
||||||
|
|||||||
@ -51,6 +51,13 @@
|
|||||||
fill: rgba(122, 92, 60, 0.55);
|
fill: rgba(122, 92, 60, 0.55);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 트렌치 실데이터(V61/V65 공급 포인트) 도트 */
|
||||||
|
.kx-canvas__trench-points circle {
|
||||||
|
fill: rgba(96, 165, 250, 0.55);
|
||||||
|
stroke: #60a5fa;
|
||||||
|
stroke-width: 0.12;
|
||||||
|
}
|
||||||
|
|
||||||
.kx-canvas__trench line {
|
.kx-canvas__trench line {
|
||||||
stroke: var(--color-canvas-line);
|
stroke: var(--color-canvas-line);
|
||||||
stroke-width: 0.15;
|
stroke-width: 0.15;
|
||||||
|
|||||||
188
tools/cad/extract_exit_seed.py
Normal file
188
tools/cad/extract_exit_seed.py
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""비상구 실좌표 시드 생성기 — V65 (hall_exit 실측 대체, DOOR 레이어 기반).
|
||||||
|
|
||||||
|
방식: DOOR 계열 레이어 지오메트리를 홀 로컬로 변환, 홀 외곽 ±2m 밴드 내 포인트를
|
||||||
|
1m 셀 클러스터 → 문 규모(<=12m) 클러스터 중심을 가장 가까운 변에 투영 = 비상구 실좌표.
|
||||||
|
가정(is_assumed) 비상구는 실측 확보 홀에 한해 대체. K1=밴드, K2=회전 프레임.
|
||||||
|
|
||||||
|
사용:
|
||||||
|
python tools/cad/extract_exit_seed.py <k1_trench.dxf> <k2_slim.dxf> <out.sql>
|
||||||
|
"""
|
||||||
|
import io
|
||||||
|
import math
|
||||||
|
import sys
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
import ezdxf
|
||||||
|
|
||||||
|
MM = 1000.0
|
||||||
|
CELL = 1.0
|
||||||
|
EDGE_BAND = 2.0
|
||||||
|
MAX_DOOR_SPAN = 12.0
|
||||||
|
DOOR_KEYS = ("DOOR", "출입구")
|
||||||
|
|
||||||
|
K1 = {"x0": 293.5, "w": 171.0, "y0": 80.1, "h": 63.0, "halls": [1, 2, 3, 4, 5]}
|
||||||
|
K2_FRAMES = {
|
||||||
|
"H6": (-12, (530, 405), 482, 542, 350.5, 443.5),
|
||||||
|
"H7": (0, (0, 0), 439, 529, 215.5, 341.5),
|
||||||
|
"H8": (0, (0, 0), 439, 529, 89.5, 215.5),
|
||||||
|
"H9": (-11, (245, 155), 196, 295, 95.5, 227.5),
|
||||||
|
"H10": (11, (255, 330), 194.5, 293.5, 250, 382),
|
||||||
|
}
|
||||||
|
# A/B 분리 셔터(가동벽) midline y — 셔터 트랙 끝점이 비상구로 오인되지 않게 ±2m 제외
|
||||||
|
SHUTTER_Y = {"H7": 63.0, "H8": 63.0, "H9": 66.0, "H10": 66.0}
|
||||||
|
|
||||||
|
|
||||||
|
def is_door(layer):
|
||||||
|
u = layer.upper()
|
||||||
|
return any(k.upper() in u for k in DOOR_KEYS)
|
||||||
|
|
||||||
|
|
||||||
|
def rot(x, y, deg, c):
|
||||||
|
if deg == 0:
|
||||||
|
return x, y
|
||||||
|
t = math.radians(-deg)
|
||||||
|
dx, dy = x - c[0], y - c[1]
|
||||||
|
return c[0] + dx * math.cos(t) - dy * math.sin(t), c[1] + dx * math.sin(t) + dy * math.cos(t)
|
||||||
|
|
||||||
|
|
||||||
|
def sample_points(e):
|
||||||
|
t = e.dxftype()
|
||||||
|
try:
|
||||||
|
if t == "LINE":
|
||||||
|
a, b = e.dxf.start, e.dxf.end
|
||||||
|
ln = math.hypot(b[0] - a[0], b[1] - a[1]) / MM
|
||||||
|
n = max(2, int(ln / 0.5))
|
||||||
|
return [((a[0] + (b[0] - a[0]) * i / n) / MM, (a[1] + (b[1] - a[1]) * i / n) / MM) for i in range(n + 1)]
|
||||||
|
if t == "LWPOLYLINE":
|
||||||
|
pts = [(p[0] / MM, p[1] / MM) for p in e.get_points()]
|
||||||
|
out = []
|
||||||
|
for a, b in zip(pts, pts[1:]):
|
||||||
|
ln = math.hypot(b[0] - a[0], b[1] - a[1])
|
||||||
|
n = max(1, int(ln / 0.5))
|
||||||
|
out += [(a[0] + (b[0] - a[0]) * i / n, a[1] + (b[1] - a[1]) * i / n) for i in range(n + 1)]
|
||||||
|
return out
|
||||||
|
if t in ("ARC", "CIRCLE"):
|
||||||
|
return [(p[0] / MM, p[1] / MM) for p in e.flattening(200)]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def edge_exits(points, w, h):
|
||||||
|
"""홀 로컬 문 포인트 → 외곽 밴드 클러스터 → 변 투영 비상구 좌표 목록."""
|
||||||
|
cells = set()
|
||||||
|
for lx, ly in points:
|
||||||
|
near = lx <= EDGE_BAND or lx >= w - EDGE_BAND or ly <= EDGE_BAND or ly >= h - EDGE_BAND
|
||||||
|
if near and 0 <= lx < w and 0 <= ly < h:
|
||||||
|
cells.add((int(lx / CELL), int(ly / CELL)))
|
||||||
|
seen = set()
|
||||||
|
exits = []
|
||||||
|
for cell in sorted(cells):
|
||||||
|
if cell in seen:
|
||||||
|
continue
|
||||||
|
q = deque([cell])
|
||||||
|
seen.add(cell)
|
||||||
|
comp = []
|
||||||
|
while q:
|
||||||
|
ci, cj = q.popleft()
|
||||||
|
comp.append((ci, cj))
|
||||||
|
for ni, nj in ((ci + 1, cj), (ci - 1, cj), (ci, cj + 1), (ci, cj - 1),
|
||||||
|
(ci + 1, cj + 1), (ci - 1, cj - 1), (ci + 1, cj - 1), (ci - 1, cj + 1)):
|
||||||
|
if (ni, nj) in cells and (ni, nj) not in seen:
|
||||||
|
seen.add((ni, nj))
|
||||||
|
q.append((ni, nj))
|
||||||
|
xs = [c[0] for c in comp]
|
||||||
|
ys = [c[1] for c in comp]
|
||||||
|
span = max(max(xs) - min(xs), max(ys) - min(ys)) + 1
|
||||||
|
if span > MAX_DOOR_SPAN:
|
||||||
|
continue # 분리 셔터 트랙 등 장스팬 제외
|
||||||
|
cx = (min(xs) + max(xs) + 1) / 2 * CELL
|
||||||
|
cy = (min(ys) + max(ys) + 1) / 2 * CELL
|
||||||
|
# 가장 가까운 변으로 투영
|
||||||
|
d = [(cx, "W"), (w - cx, "E"), (cy, "N"), (h - cy, "S")]
|
||||||
|
dist, side = min(d)
|
||||||
|
if side == "W":
|
||||||
|
cx = 0.0
|
||||||
|
elif side == "E":
|
||||||
|
cx = w
|
||||||
|
elif side == "N":
|
||||||
|
cy = 0.0
|
||||||
|
else:
|
||||||
|
cy = h
|
||||||
|
exits.append((round(cx, 1), round(cy, 1)))
|
||||||
|
# 1.5m 이내 중복 병합
|
||||||
|
merged = []
|
||||||
|
for x, y in sorted(exits):
|
||||||
|
if not any(abs(x - mx) < 1.5 and abs(y - my) < 1.5 for mx, my in merged):
|
||||||
|
merged.append((x, y))
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
k1_src, k2_src, out_sql = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||||
|
per_hall = {}
|
||||||
|
|
||||||
|
doc1 = ezdxf.readfile(k1_src)
|
||||||
|
k1_pts = {n: [] for n in K1["halls"]}
|
||||||
|
for e in doc1.modelspace():
|
||||||
|
if not is_door(e.dxf.layer):
|
||||||
|
continue
|
||||||
|
for x, y in sample_points(e):
|
||||||
|
lx = x - K1["x0"]
|
||||||
|
if not (0 <= lx < K1["w"]):
|
||||||
|
continue
|
||||||
|
for n in K1["halls"]:
|
||||||
|
y0 = K1["y0"] + (n - 1) * K1["h"]
|
||||||
|
if y0 <= y < y0 + K1["h"]:
|
||||||
|
k1_pts[n].append((lx, K1["h"] - (y - y0)))
|
||||||
|
for n in K1["halls"]:
|
||||||
|
per_hall[f"H{n}"] = (edge_exits(k1_pts[n], K1["w"], K1["h"]), K1["w"], K1["h"])
|
||||||
|
|
||||||
|
doc2 = ezdxf.readfile(k2_src)
|
||||||
|
k2_pts = {h: [] for h in K2_FRAMES}
|
||||||
|
for e in doc2.modelspace():
|
||||||
|
if not is_door(e.dxf.layer):
|
||||||
|
continue
|
||||||
|
for x, y in sample_points(e):
|
||||||
|
for hall, (deg, c, u0, u1, v0, v1) in K2_FRAMES.items():
|
||||||
|
u, v = rot(x, y, deg, c)
|
||||||
|
if u0 <= u < u1 and v0 <= v < v1:
|
||||||
|
k2_pts[hall].append((u - u0, v1 - v))
|
||||||
|
for hall, (deg, c, u0, u1, v0, v1) in K2_FRAMES.items():
|
||||||
|
exits = edge_exits(k2_pts[hall], u1 - u0, v1 - v0)
|
||||||
|
sy = SHUTTER_Y.get(hall)
|
||||||
|
if sy is not None:
|
||||||
|
exits = [(x, y) for x, y in exits if abs(y - sy) > 2.0]
|
||||||
|
per_hall[hall] = (exits, u1 - u0, v1 - v0)
|
||||||
|
|
||||||
|
sql = io.StringIO()
|
||||||
|
sql.write(
|
||||||
|
"-- V65: 비상구 실좌표 시드 — CAD DOOR 레이어 경계 문 클러스터(±2m 밴드) 변 투영.\n"
|
||||||
|
"-- 생성기: tools/cad/extract_exit_seed.py (결정적 재생성). 근거: docs/analysis/cad-extraction.md.\n"
|
||||||
|
"-- 실측 확보 홀만 가정(is_assumed) 비상구를 대체. 분리 셔터 등 12m 초과 장스팬 제외.\n\n"
|
||||||
|
)
|
||||||
|
report = []
|
||||||
|
for hall, (exits, w, h) in per_hall.items():
|
||||||
|
report.append(f"{hall}: {len(exits)}개 {exits[:8]}")
|
||||||
|
if len(exits) < 2:
|
||||||
|
sql.write(f"-- {hall}: 경계 문 {len(exits)}개 — 신뢰 부족, 가정 비상구 유지\n")
|
||||||
|
continue
|
||||||
|
sql.write(f"-- {hall}: 실측 비상구 {len(exits)}개\nDELETE FROM hall_exit WHERE hall_id = '{hall}' AND is_assumed;\n")
|
||||||
|
vals = ",\n ".join(
|
||||||
|
f"('{hall}-EXIT-R{i}', '{hall}', ST_SetSRID(ST_MakePoint({x}, {y}), 0), 3.0, false)"
|
||||||
|
for i, (x, y) in enumerate(exits, 1)
|
||||||
|
)
|
||||||
|
sql.write(
|
||||||
|
"INSERT INTO hall_exit (id, hall_id, geom, clearance_m, is_assumed)\nVALUES\n "
|
||||||
|
+ vals + "\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}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Reference in New Issue
Block a user