feat(m2): V62 measured pillar seed + auto-layout pillar avoidance
Owner directive 2026-07-15: extract pillars and make auto-placement avoid them. - tools/cad/extract_pillar_seed.py: K1 pillars from CAD layer 4-COL-H (cross-validated against official drawing dims 54/117m south wall pillars); K2 H7-H10 from official drawing dimension annotations (one interior midline pillar each, dia 2.3m); H6 not seeded (no source - recorded as limitation) - V62: hall_pillar table + 74 seeds; also corrects V61 trench y-axis (RT points were seeded in CAD-y without the top-of-drawing flip - now 63-y, generator fixed as the canonical form) - Engine: autoGenerate reserves pillar radius + 0.6m buffer, packing skips overlapping cells; HallInfo.pillars contract extension (additive, NON_NULL) - Canvas: structure layer renders pillar circles (editor passes hall pillars) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
6be091aa7c
commit
6ff692c03f
@ -336,3 +336,7 @@
|
||||
### 세션 5 추가 — V61 트렌치 실측 시드 (2026-07-15, 소유자 지시 "V61 트렌치 실측 시드 진행해")
|
||||
|
||||
CAD 레이어 `5-트랜치`(표기 함정 — 트렌치≠트랜치)에서 K1 홀1~5 트렌치 채널 실측 추출: **홀당 가로 7채널·피치 9.0m·연장 x[20.6..170.2]m** (5개 홀 패턴 완전 일치로 밴드 정렬 교차 검증). V61 = 가정 그리드 삭제 → 채널 따라 6m 간격 실측 포인트 875개(is_assumed=false, 생성기 `tools/cad/extract_trench_seed.py` 결정적 재생성 가능). K2는 트렌치 레이어 부재로 가정 유지(전력간선 도면 분석 후속). 검증: gradle test 통과 → push→자동배포→health·스모크 게이트.
|
||||
|
||||
### 세션 5 추가 — 기둥 추출·자동배치 회피 (2026-07-15, 소유자 지시 "기둥 추출해서 자동배치 회피 반영해")
|
||||
|
||||
V62: hall_pillar 74개 시드 — K1은 CAD 4-COL-H 추출(공식 주석 54/117m와 일치 검증), K2 H7~H10은 공식 도면 치수 주석(midline 내부 기둥, ø2.3m), H6 미시드(근거 없음). 부수 교정: **V61 트렌치 y축 반전 보정**(도면 상단 y=0 기준 정합 — 캔버스/DWG 언더레이와 상하 일치). 자동배치는 기둥 반경+0.6m를 예약 영역으로 회피, 캔버스 구조 레이어에 기둥 원 표시. 게이트: gradle test·tsc·vite·XML 전부 통과 → push→배포.
|
||||
|
||||
@ -95,3 +95,15 @@ H7/H8 블록 252m=126×2·폭 90m 정확 일치) + 회전 프로브 렌더 시
|
||||
(`H{n}-RT-{행}-{k}`, is_assumed=false). 공급 플래그는 V5 정책 유지(위치만 실측 대체).
|
||||
K2(H6~10)는 가정 유지 — K2 도면(원본-1층 평면도)에 트렌치 레이어 부재, 전력간선 평면도(77MB) 분석 후속.
|
||||
- **후속**: FloorplanCanvas의 합성 트렌치 그리드(9m 가정선)를 HallInfo.trenches 실데이터 표시로 교체 검토.
|
||||
|
||||
## V62 기둥 실측 시드 + 자동배치 회피 (2026-07-15)
|
||||
|
||||
- **K1(H1~H5)**: CAD 4-COL-H 사각 중심 추출(0.3~3m 각형·0.8m dedupe) — 공식 주석 교차검증
|
||||
(남측 벽 기둥 x=54/117m ↔ CAD 53.6/116.6m 일치). 홀별 10~29개(벽면·부속실 기둥 포함).
|
||||
- **K2(H7~H10)**: CAD에서 기둥 원 미검출(주심도는 별도 원점·심볼 원 r0.1m뿐, 사이트 평면은 블록 내 부재)
|
||||
→ **공식 홀 도면 치수 주석**으로 시드: midline 내부 기둥 1개(H7/H8 x=40.1·H9/H10 x=48.9, ø2.3m).
|
||||
H6는 근거 없음 — 미시드(한계).
|
||||
- **V62**: hall_pillar 테이블(+GIST 불필요·hall_id 인덱스) + 74개 시드 + **V61 트렌치 y축 보정**
|
||||
(RT 포인트가 CAD y 미반전으로 시드됨 — 도면 상단 y=0 기준으로 63-y 반전, 생성기도 정본 수정).
|
||||
- **엔진**: autoGenerate가 기둥 반경+0.6m 버퍼를 예약 영역으로 — 겹치는 부스 셀 배치 제외.
|
||||
- **캔버스**: 구조 레이어(기둥·비상구 토글)에 기둥 원 표시(HallInfo.pillars 계약 확장, NON_NULL 호환).
|
||||
|
||||
@ -291,11 +291,20 @@ public class FloorplanServiceImpl implements FloorplanService {
|
||||
{3.0, 3.0, 3.5}, // B: 표준 3×3, 통로 3.5m(넓은 피난)
|
||||
{6.0, 3.0, 3.0}, // C: 6×3 프리미엄 지향, 통로 3.0m
|
||||
};
|
||||
// 기둥 회피(V62 실측) — 기둥 반경 + 0.6m 작업 여유를 예약 영역으로(소유자 지시 2026-07-15).
|
||||
List<double[]> pillarZones = new ArrayList<>();
|
||||
if (hall.pillars() != null) {
|
||||
for (HallInfo.Pillar p : hall.pillars()) {
|
||||
double buf = p.radiusM() + 0.6;
|
||||
pillarZones.add(new double[]{p.x() - buf, p.y() - buf, p.x() + buf, p.y() + buf});
|
||||
}
|
||||
}
|
||||
|
||||
List<AutoLayoutOption> options = new ArrayList<>();
|
||||
for (int i = 0; i < optionCount; i++) {
|
||||
double[] vp = variants[i % variants.length];
|
||||
char tag = (char) ('A' + i);
|
||||
List<BoothDto> booths = packBooths(vp[0], vp[1], vp[2], hallW, hallD, bounds, request, tag);
|
||||
List<BoothDto> booths = packBooths(vp[0], vp[1], vp[2], hallW, hallD, bounds, pillarZones, request, tag);
|
||||
|
||||
double salesArea = booths.stream()
|
||||
.mapToDouble(b -> vp[0] * vp[1]).sum();
|
||||
@ -375,7 +384,7 @@ public class FloorplanServiceImpl implements FloorplanService {
|
||||
*/
|
||||
private List<BoothDto> packBooths(double boothW, double boothD, double aisle,
|
||||
double hallW, double hallD, double[] bounds,
|
||||
AutoLayoutRequest req, char tag) {
|
||||
List<double[]> pillarZones, AutoLayoutRequest req, char tag) {
|
||||
List<BoothDto> booths = new ArrayList<>();
|
||||
int target = req.targetBoothCount();
|
||||
int premiumTarget = (int) Math.round(target * clamp01(req.premiumRatio()));
|
||||
@ -388,6 +397,7 @@ public class FloorplanServiceImpl implements FloorplanService {
|
||||
double endY = bounds[3] >= hallD ? hallD - PERIMETER_MARGIN_M : bounds[3] - 1.5;
|
||||
|
||||
List<double[]> reserved = reservedZones(hallW, hallD, aisle, req);
|
||||
reserved.addAll(pillarZones); // 기둥(실측) + 작업 여유 — 겹치는 셀 배치 제외
|
||||
|
||||
int index = 0;
|
||||
for (double y = startY; y + boothD <= endY && index < target; y += stepY) {
|
||||
@ -574,9 +584,17 @@ public class FloorplanServiceImpl implements FloorplanService {
|
||||
nz(dbl(e.get("clearanceM")))));
|
||||
}
|
||||
}
|
||||
List<HallInfo.Pillar> pillars = new ArrayList<>();
|
||||
List<Map<String, Object>> ps = hallMapper.findPillars(hallId);
|
||||
if (ps != null) {
|
||||
for (Map<String, Object> p : ps) {
|
||||
pillars.add(new HallInfo.Pillar(str(p.get("pillarId")),
|
||||
nz(dbl(p.get("x"))), nz(dbl(p.get("y"))), nz(dbl(p.get("radiusM")))));
|
||||
}
|
||||
}
|
||||
return new HallInfo(str(h.get("hallId")), str(h.get("label")), dims, dbl(h.get("heightM")),
|
||||
intOrNull(h.get("boothCapacity")), dbl(h.get("floorLoadTPerM2")),
|
||||
Boolean.TRUE.equals(bool(h.get("assumedTrench"))), trenches, exits);
|
||||
Boolean.TRUE.equals(bool(h.get("assumedTrench"))), trenches, exits, pillars);
|
||||
}
|
||||
|
||||
private int nextVersion(String eventId, String hallId) {
|
||||
|
||||
@ -28,9 +28,21 @@ public record HallInfo(
|
||||
Double floorLoadTPerM2,
|
||||
boolean assumedTrench,
|
||||
List<Trench> trenches,
|
||||
List<Exit> exits
|
||||
List<Exit> exits,
|
||||
List<Pillar> pillars
|
||||
) {
|
||||
|
||||
/**
|
||||
* 기둥(구조 장애물) — 자동배치 회피·캔버스 표시 근거(V62 hall_pillar, CAD/공식 도면 실측).
|
||||
*
|
||||
* @param pillarId 기둥 식별자
|
||||
* @param x 홀 로컬 X(m)
|
||||
* @param y 홀 로컬 Y(m, 도면 상단=0)
|
||||
* @param radiusM 기둥 반경(m)
|
||||
*/
|
||||
public record Pillar(String pillarId, double x, double y, double radiusM) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 트렌치 포인트(홀 로컬 미터) + 공급 매트릭스.
|
||||
*
|
||||
|
||||
@ -32,4 +32,7 @@ public interface HallMapper {
|
||||
|
||||
/** 구역 단건(경계 좌표) — 구역 제한 자동배치 근거. 없으면 null. */
|
||||
Map<String, Object> findZone(@Param("zoneId") String zoneId);
|
||||
|
||||
/** 홀 기둥(V62 hall_pillar, 실측) — 자동배치 회피·캔버스 표시. */
|
||||
List<Map<String, Object>> findPillars(@Param("hallId") String hallId);
|
||||
}
|
||||
|
||||
@ -0,0 +1,95 @@
|
||||
-- V62: 기둥 실측 시드(hall_pillar) + V61 트렌치 y축 보정 — 자동배치 기둥 회피 근거.
|
||||
-- 생성기: tools/cad/extract_pillar_seed.py (결정적 재생성). 근거: docs/analysis/cad-extraction.md.
|
||||
-- K1=CAD 4-COL-H 추출(주석 교차검증), K2 H7~H10=공식 도면 치수 주석(midline 내부 기둥), H6=근거 없음(미시드).
|
||||
|
||||
CREATE TABLE IF NOT EXISTS hall_pillar (
|
||||
id varchar(60) PRIMARY KEY, -- 예: H1-P-1
|
||||
hall_id varchar(20) NOT NULL REFERENCES hall(id) ON DELETE CASCADE,
|
||||
geom geometry(Point,0) NOT NULL, -- 홀 로컬 m(y=0 도면 상단)
|
||||
radius_m numeric(4,2) NOT NULL,
|
||||
is_assumed boolean NOT NULL DEFAULT false
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_hall_pillar_hall ON hall_pillar (hall_id);
|
||||
|
||||
-- V61 트렌치 y축 보정: RT 포인트가 CAD y(상하 미반전)로 시드됨 → 도면 상단 기준으로 반전(멱등:
|
||||
-- 재실행 시 다시 뒤집히지 않도록 마이그레이션 1회 실행 특성(flyway)에 의존 — 값 자체는 63-y 대칭 변환).
|
||||
UPDATE trench SET geom = ST_SetSRID(ST_MakePoint(ST_X(geom), 63.0 - ST_Y(geom)), 0)
|
||||
WHERE id LIKE '%-RT-%';
|
||||
|
||||
INSERT INTO hall_pillar (id, hall_id, geom, radius_m, is_assumed)
|
||||
VALUES
|
||||
('H1-P-1', 'H1', ST_SetSRID(ST_MakePoint(8.64, 52.67), 0), 1.25, false),
|
||||
('H1-P-2', 'H1', ST_SetSRID(ST_MakePoint(8.64, 61.67), 0), 1.25, false),
|
||||
('H1-P-3', 'H1', ST_SetSRID(ST_MakePoint(17.64, 52.67), 0), 1.25, false),
|
||||
('H1-P-4', 'H1', ST_SetSRID(ST_MakePoint(17.64, 61.67), 0), 1.25, false),
|
||||
('H1-P-5', 'H1', ST_SetSRID(ST_MakePoint(26.64, 61.67), 0), 1.25, false),
|
||||
('H1-P-6', 'H1', ST_SetSRID(ST_MakePoint(35.64, 61.67), 0), 1.25, false),
|
||||
('H1-P-7', 'H1', ST_SetSRID(ST_MakePoint(44.64, 61.67), 0), 1.25, false),
|
||||
('H1-P-8', 'H1', ST_SetSRID(ST_MakePoint(53.64, 61.67), 0), 1.25, false),
|
||||
('H1-P-9', 'H1', ST_SetSRID(ST_MakePoint(62.64, 61.67), 0), 1.25, false),
|
||||
('H1-P-10', 'H1', ST_SetSRID(ST_MakePoint(71.64, 61.67), 0), 1.25, false),
|
||||
('H1-P-11', 'H1', ST_SetSRID(ST_MakePoint(76.14, 61.67), 0), 1.25, false),
|
||||
('H1-P-12', 'H1', ST_SetSRID(ST_MakePoint(80.64, 61.67), 0), 1.25, false),
|
||||
('H1-P-13', 'H1', ST_SetSRID(ST_MakePoint(89.64, 61.67), 0), 1.25, false),
|
||||
('H1-P-14', 'H1', ST_SetSRID(ST_MakePoint(98.64, 61.67), 0), 1.25, false),
|
||||
('H1-P-15', 'H1', ST_SetSRID(ST_MakePoint(107.64, 61.67), 0), 1.25, false),
|
||||
('H1-P-16', 'H1', ST_SetSRID(ST_MakePoint(116.64, 62.32), 0), 1.25, false),
|
||||
('H1-P-17', 'H1', ST_SetSRID(ST_MakePoint(125.64, 61.67), 0), 1.25, false),
|
||||
('H1-P-18', 'H1', ST_SetSRID(ST_MakePoint(134.64, 61.67), 0), 1.25, false),
|
||||
('H1-P-19', 'H1', ST_SetSRID(ST_MakePoint(143.64, 61.67), 0), 1.25, false),
|
||||
('H1-P-20', 'H1', ST_SetSRID(ST_MakePoint(152.64, 61.67), 0), 1.25, false),
|
||||
('H1-P-21', 'H1', ST_SetSRID(ST_MakePoint(161.64, 61.67), 0), 1.25, false),
|
||||
('H1-P-22', 'H1', ST_SetSRID(ST_MakePoint(170.64, 0.77), 0), 1.25, false),
|
||||
('H1-P-23', 'H1', ST_SetSRID(ST_MakePoint(170.64, 7.67), 0), 1.25, false),
|
||||
('H1-P-24', 'H1', ST_SetSRID(ST_MakePoint(170.64, 16.67), 0), 1.25, false),
|
||||
('H1-P-25', 'H1', ST_SetSRID(ST_MakePoint(170.64, 25.67), 0), 1.25, false),
|
||||
('H1-P-26', 'H1', ST_SetSRID(ST_MakePoint(170.64, 34.67), 0), 1.25, false),
|
||||
('H1-P-27', 'H1', ST_SetSRID(ST_MakePoint(170.64, 43.67), 0), 1.25, false),
|
||||
('H1-P-28', 'H1', ST_SetSRID(ST_MakePoint(170.64, 52.67), 0), 1.25, false),
|
||||
('H1-P-29', 'H1', ST_SetSRID(ST_MakePoint(170.64, 61.67), 0), 1.25, false),
|
||||
('H2-P-1', 'H2', ST_SetSRID(ST_MakePoint(53.64, 61.98), 0), 1.25, false),
|
||||
('H2-P-2', 'H2', ST_SetSRID(ST_MakePoint(116.64, 61.98), 0), 1.25, false),
|
||||
('H2-P-3', 'H2', ST_SetSRID(ST_MakePoint(170.64, 0.77), 0), 1.25, false),
|
||||
('H2-P-4', 'H2', ST_SetSRID(ST_MakePoint(170.64, 7.67), 0), 1.25, false),
|
||||
('H2-P-5', 'H2', ST_SetSRID(ST_MakePoint(170.64, 16.67), 0), 1.25, false),
|
||||
('H2-P-6', 'H2', ST_SetSRID(ST_MakePoint(170.64, 25.67), 0), 1.25, false),
|
||||
('H2-P-7', 'H2', ST_SetSRID(ST_MakePoint(170.64, 34.67), 0), 1.25, false),
|
||||
('H2-P-8', 'H2', ST_SetSRID(ST_MakePoint(170.64, 43.67), 0), 1.25, false),
|
||||
('H2-P-9', 'H2', ST_SetSRID(ST_MakePoint(170.64, 52.67), 0), 1.25, false),
|
||||
('H2-P-10', 'H2', ST_SetSRID(ST_MakePoint(170.64, 61.67), 0), 1.25, false),
|
||||
('H3-P-1', 'H3', ST_SetSRID(ST_MakePoint(53.64, 61.98), 0), 1.25, false),
|
||||
('H3-P-2', 'H3', ST_SetSRID(ST_MakePoint(116.64, 61.98), 0), 1.25, false),
|
||||
('H3-P-3', 'H3', ST_SetSRID(ST_MakePoint(170.64, 0.77), 0), 1.25, false),
|
||||
('H3-P-4', 'H3', ST_SetSRID(ST_MakePoint(170.64, 7.67), 0), 1.25, false),
|
||||
('H3-P-5', 'H3', ST_SetSRID(ST_MakePoint(170.64, 16.67), 0), 1.25, false),
|
||||
('H3-P-6', 'H3', ST_SetSRID(ST_MakePoint(170.64, 25.67), 0), 1.25, false),
|
||||
('H3-P-7', 'H3', ST_SetSRID(ST_MakePoint(170.64, 34.67), 0), 1.25, false),
|
||||
('H3-P-8', 'H3', ST_SetSRID(ST_MakePoint(170.64, 43.67), 0), 1.25, false),
|
||||
('H3-P-9', 'H3', ST_SetSRID(ST_MakePoint(170.64, 52.67), 0), 1.25, false),
|
||||
('H3-P-10', 'H3', ST_SetSRID(ST_MakePoint(170.64, 61.67), 0), 1.25, false),
|
||||
('H4-P-1', 'H4', ST_SetSRID(ST_MakePoint(53.64, 61.98), 0), 1.25, false),
|
||||
('H4-P-2', 'H4', ST_SetSRID(ST_MakePoint(116.64, 61.98), 0), 1.25, false),
|
||||
('H4-P-3', 'H4', ST_SetSRID(ST_MakePoint(170.64, 0.77), 0), 1.25, false),
|
||||
('H4-P-4', 'H4', ST_SetSRID(ST_MakePoint(170.64, 7.67), 0), 1.25, false),
|
||||
('H4-P-5', 'H4', ST_SetSRID(ST_MakePoint(170.64, 16.67), 0), 1.25, false),
|
||||
('H4-P-6', 'H4', ST_SetSRID(ST_MakePoint(170.64, 25.67), 0), 1.25, false),
|
||||
('H4-P-7', 'H4', ST_SetSRID(ST_MakePoint(170.64, 34.67), 0), 1.25, false),
|
||||
('H4-P-8', 'H4', ST_SetSRID(ST_MakePoint(170.64, 43.67), 0), 1.25, false),
|
||||
('H4-P-9', 'H4', ST_SetSRID(ST_MakePoint(170.64, 52.67), 0), 1.25, false),
|
||||
('H4-P-10', 'H4', ST_SetSRID(ST_MakePoint(170.64, 61.67), 0), 1.25, false),
|
||||
('H5-P-1', 'H5', ST_SetSRID(ST_MakePoint(8.64, 7.02), 0), 1.25, false),
|
||||
('H5-P-2', 'H5', ST_SetSRID(ST_MakePoint(16.99, 7.02), 0), 1.25, false),
|
||||
('H5-P-3', 'H5', ST_SetSRID(ST_MakePoint(53.64, 61.98), 0), 1.25, false),
|
||||
('H5-P-4', 'H5', ST_SetSRID(ST_MakePoint(116.64, 61.98), 0), 1.25, false),
|
||||
('H5-P-5', 'H5', ST_SetSRID(ST_MakePoint(170.64, 7.67), 0), 1.25, false),
|
||||
('H5-P-6', 'H5', ST_SetSRID(ST_MakePoint(170.64, 16.67), 0), 1.25, false),
|
||||
('H5-P-7', 'H5', ST_SetSRID(ST_MakePoint(170.64, 25.67), 0), 1.25, false),
|
||||
('H5-P-8', 'H5', ST_SetSRID(ST_MakePoint(170.64, 34.67), 0), 1.25, false),
|
||||
('H5-P-9', 'H5', ST_SetSRID(ST_MakePoint(170.64, 43.67), 0), 1.25, false),
|
||||
('H5-P-10', 'H5', ST_SetSRID(ST_MakePoint(170.64, 52.67), 0), 1.25, false),
|
||||
('H5-P-11', 'H5', ST_SetSRID(ST_MakePoint(170.64, 61.67), 0), 1.25, false),
|
||||
('H7-P-1', 'H7', ST_SetSRID(ST_MakePoint(40.1, 63.0), 0), 1.15, false),
|
||||
('H8-P-1', 'H8', ST_SetSRID(ST_MakePoint(40.1, 63.0), 0), 1.15, false),
|
||||
('H9-P-1', 'H9', ST_SetSRID(ST_MakePoint(48.9, 66.0), 0), 1.15, false),
|
||||
('H10-P-1', 'H10', ST_SetSRID(ST_MakePoint(48.9, 66.0), 0), 1.15, false)
|
||||
ON CONFLICT (id) DO UPDATE SET geom = EXCLUDED.geom, radius_m = EXCLUDED.radius_m;
|
||||
@ -82,4 +82,15 @@
|
||||
WHERE id = #{zoneId}
|
||||
</select>
|
||||
|
||||
<!-- 홀 기둥(V62 실측) — 자동배치 회피·캔버스 표시. -->
|
||||
<select id="findPillars" resultType="map">
|
||||
SELECT id AS "pillarId",
|
||||
ST_X(geom) AS "x",
|
||||
ST_Y(geom) AS "y",
|
||||
radius_m AS "radiusM"
|
||||
FROM hall_pillar
|
||||
WHERE hall_id = #{hallId}
|
||||
ORDER BY id
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
@ -163,6 +163,13 @@ export interface LayoutSummary {
|
||||
violationBlock: number;
|
||||
violationWarn: number;
|
||||
}
|
||||
/** 홀 기둥(V62 실측) — 자동배치 회피·캔버스 표시. 좌표는 홀 로컬 m(도면 상단 y=0). */
|
||||
export interface HallPillarDto {
|
||||
pillarId: string;
|
||||
x: number;
|
||||
y: number;
|
||||
radiusM: number;
|
||||
}
|
||||
/** 홀 마스터 정보(M2 LayoutDto.hall) — 캔버스 축척·경계 원천. dimsM=[폭, 깊이](m). */
|
||||
export interface HallInfoDto {
|
||||
hallId: string;
|
||||
@ -170,6 +177,7 @@ export interface HallInfoDto {
|
||||
dimsM?: number[] | null;
|
||||
ceilingM?: number | null;
|
||||
targetBoothCount?: number | null;
|
||||
pillars?: HallPillarDto[] | null;
|
||||
}
|
||||
export interface LayoutDto {
|
||||
layoutId: string;
|
||||
|
||||
@ -254,6 +254,7 @@ export function BoothLayoutEditorPage() {
|
||||
onZoomChange={setZoom}
|
||||
floorplanUrl={drawingTab === 'dwg' ? hallDwgUrl(hallId) : hallFloorplanUrl(hallId)}
|
||||
floorRegion={drawingTab === 'dwg' ? hallDwgRegion(hallId) : hallFloorRegion(hallId)}
|
||||
pillars={layout.hall?.pillars}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { BoothDto } from '../../api/types';
|
||||
import type { BoothDto, HallPillarDto } from '../../api/types';
|
||||
import './canvas.css';
|
||||
|
||||
const ZOOM_MIN = 0.3;
|
||||
@ -37,6 +37,8 @@ interface FloorplanCanvasProps {
|
||||
floorRegion?: [number, number, number, number] | null;
|
||||
/** 마우스휠 확대/축소 시 페이지 zoom 상태 동기화(소유자 지시 2026-07-14). 미지정 시 휠 줌 비활성. */
|
||||
onZoomChange?: (zoom: number) => void;
|
||||
/** 홀 기둥(V62 실측) — 구조 레이어에 원으로 표시(자동배치는 서버가 회피). */
|
||||
pillars?: HallPillarDto[] | null;
|
||||
}
|
||||
|
||||
const TRENCH_SPACING_M = 9; // 가정 트렌치 그리드 간격(실측 대체 — R4)
|
||||
@ -63,6 +65,7 @@ export function FloorplanCanvas({
|
||||
floorplanUrl,
|
||||
floorRegion,
|
||||
onZoomChange,
|
||||
pillars,
|
||||
}: FloorplanCanvasProps) {
|
||||
const [hw, hh] = hallDims;
|
||||
|
||||
@ -197,6 +200,15 @@ export function FloorplanCanvas({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 기둥(V62 실측) — 구조 레이어 */}
|
||||
{layers.structure && pillars && pillars.length > 0 && (
|
||||
<g className="kx-canvas__pillars" aria-hidden="true">
|
||||
{pillars.map((p) => (
|
||||
<circle key={p.pillarId} cx={p.x} cy={p.y} r={Math.max(p.radiusM, 0.6)} />
|
||||
))}
|
||||
</g>
|
||||
)}
|
||||
|
||||
{/* 트렌치 그리드 */}
|
||||
{layers.trench && (
|
||||
<g className="kx-canvas__trench">
|
||||
|
||||
@ -30,6 +30,12 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.kx-canvas__pillars circle {
|
||||
fill: rgba(122, 92, 60, 0.55);
|
||||
stroke: #a8875f;
|
||||
stroke-width: 0.2;
|
||||
}
|
||||
|
||||
.kx-canvas__trench line {
|
||||
stroke: var(--color-canvas-line);
|
||||
stroke-width: 0.15;
|
||||
|
||||
108
tools/cad/extract_pillar_seed.py
Normal file
108
tools/cad/extract_pillar_seed.py
Normal file
@ -0,0 +1,108 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""기둥 실측 시드 생성기 — V62 (hall_pillar 테이블 + K1/K2 기둥 + V61 트렌치 y축 보정).
|
||||
|
||||
근거:
|
||||
K1(H1~H5): CAD `전기,설비 트렌치.dwg` 레이어 4-COL-H 사각 폴리라인 중심(0.3~3m 각형).
|
||||
공식 도면 주석(hall1.jpg: 기둥 Pillar ø2.5m, x=54m 치수)과 CAD 추출값(53.6m) 일치 검증.
|
||||
K2(H7~H10): 공식 홀 도면 치수 주석 — midline(A/B 경계) 내부 기둥 1개.
|
||||
H7/H8: 40m|44.5m 분할 → x=40.1m, y=63m(ø2.3m). H9/H10: 48.9m|44.6m → x=48.9m, y=66m.
|
||||
H6: 도면에 내부 기둥 주석 없음 — 미시드(한계 기록).
|
||||
|
||||
좌표 규약: 홀 로컬 m, y=0은 도면 상단(DWG 언더레이 기준) — CAD y와 상하 반전:
|
||||
local_y = BAND_H - (cad_y - band_y0). ★V61 트렌치가 이 반전을 누락 → 본 마이그레이션에서 보정.
|
||||
|
||||
사용:
|
||||
python tools/cad/extract_pillar_seed.py <k1_trench.dxf> <out.sql>
|
||||
"""
|
||||
import io
|
||||
import sys
|
||||
|
||||
import ezdxf
|
||||
|
||||
MM = 1000.0
|
||||
FLOOR_X0, FLOOR_W = 293.5, 171.0
|
||||
BAND_Y0, BAND_H = 80.1, 63.0
|
||||
K1_HALLS = [1, 2, 3, 4, 5]
|
||||
K1_RADIUS = 1.25 # ø2.5m (공식 주석)
|
||||
|
||||
# K2 내부 기둥 — 공식 도면 치수 주석(홀 로컬 m, y=0 상단)
|
||||
K2_PILLARS = {
|
||||
"H7": [(40.1, 63.0, 1.15)],
|
||||
"H8": [(40.1, 63.0, 1.15)],
|
||||
"H9": [(48.9, 66.0, 1.15)],
|
||||
"H10": [(48.9, 66.0, 1.15)],
|
||||
}
|
||||
|
||||
|
||||
def k1_pillars(doc):
|
||||
"""4-COL-H 사각 중심 → 홀 로컬(0.3~3m 각형, 0.8m 스냅 dedupe)."""
|
||||
msp = doc.modelspace()
|
||||
per_hall = {n: [] for n in K1_HALLS}
|
||||
for e in msp:
|
||||
if e.dxftype() != "LWPOLYLINE" or "COL" not in e.dxf.layer.upper():
|
||||
continue
|
||||
pts = [(p[0] / MM, p[1] / MM) for p in e.get_points()]
|
||||
if len(pts) < 3:
|
||||
continue
|
||||
xs = [p[0] for p in pts]
|
||||
ys = [p[1] for p in pts]
|
||||
w, h = max(xs) - min(xs), max(ys) - min(ys)
|
||||
if not (0.3 <= w <= 3.0 and 0.3 <= h <= 3.0):
|
||||
continue
|
||||
cx, cy = (min(xs) + max(xs)) / 2, (min(ys) + max(ys)) / 2
|
||||
lx = cx - FLOOR_X0
|
||||
if not (0 <= lx <= FLOOR_W):
|
||||
continue
|
||||
for n in K1_HALLS:
|
||||
y0 = BAND_Y0 + (n - 1) * BAND_H
|
||||
if y0 <= cy < y0 + BAND_H:
|
||||
ly = BAND_H - (cy - y0) # 상하 반전(도면 상단=0)
|
||||
if not any(abs(px - lx) < 0.8 and abs(py - ly) < 0.8 for px, py, _ in per_hall[n]):
|
||||
per_hall[n].append((round(lx, 2), round(ly, 2), K1_RADIUS))
|
||||
return per_hall
|
||||
|
||||
|
||||
def main():
|
||||
src, out_sql = sys.argv[1], sys.argv[2]
|
||||
doc = ezdxf.readfile(src)
|
||||
per_hall = k1_pillars(doc)
|
||||
|
||||
sql = io.StringIO()
|
||||
sql.write(
|
||||
"-- V62: 기둥 실측 시드(hall_pillar) + V61 트렌치 y축 보정 — 자동배치 기둥 회피 근거.\n"
|
||||
"-- 생성기: tools/cad/extract_pillar_seed.py (결정적 재생성). 근거: docs/analysis/cad-extraction.md.\n"
|
||||
"-- K1=CAD 4-COL-H 추출(주석 교차검증), K2 H7~H10=공식 도면 치수 주석(midline 내부 기둥), H6=근거 없음(미시드).\n\n"
|
||||
"CREATE TABLE IF NOT EXISTS hall_pillar (\n"
|
||||
" id varchar(60) PRIMARY KEY, -- 예: H1-P-1\n"
|
||||
" hall_id varchar(20) NOT NULL REFERENCES hall(id) ON DELETE CASCADE,\n"
|
||||
" geom geometry(Point,0) NOT NULL, -- 홀 로컬 m(y=0 도면 상단)\n"
|
||||
" radius_m numeric(4,2) NOT NULL,\n"
|
||||
" is_assumed boolean NOT NULL DEFAULT false\n"
|
||||
");\n"
|
||||
"CREATE INDEX IF NOT EXISTS idx_hall_pillar_hall ON hall_pillar (hall_id);\n\n"
|
||||
"-- V61 트렌치 y축 보정: RT 포인트가 CAD y(상하 미반전)로 시드됨 → 도면 상단 기준으로 반전(멱등:\n"
|
||||
"-- 재실행 시 다시 뒤집히지 않도록 마이그레이션 1회 실행 특성(flyway)에 의존 — 값 자체는 63-y 대칭 변환).\n"
|
||||
"UPDATE trench SET geom = ST_SetSRID(ST_MakePoint(ST_X(geom), 63.0 - ST_Y(geom)), 0)\n"
|
||||
"WHERE id LIKE '%-RT-%';\n\n"
|
||||
)
|
||||
rows = []
|
||||
for n in K1_HALLS:
|
||||
for i, (x, y, r) in enumerate(sorted(per_hall[n]), 1):
|
||||
rows.append(f"('H{n}-P-{i}', 'H{n}', ST_SetSRID(ST_MakePoint({x}, {y}), 0), {r}, false)")
|
||||
for hall, plist in K2_PILLARS.items():
|
||||
for i, (x, y, r) in enumerate(plist, 1):
|
||||
rows.append(f"('{hall}-P-{i}', '{hall}', ST_SetSRID(ST_MakePoint({x}, {y}), 0), {r}, false)")
|
||||
sql.write(
|
||||
"INSERT INTO hall_pillar (id, hall_id, geom, radius_m, is_assumed)\nVALUES\n "
|
||||
+ ",\n ".join(rows)
|
||||
+ "\nON CONFLICT (id) DO UPDATE SET geom = EXCLUDED.geom, radius_m = EXCLUDED.radius_m;\n"
|
||||
)
|
||||
with open(out_sql, "w", encoding="utf-8") as f:
|
||||
f.write(sql.getvalue())
|
||||
for n in K1_HALLS:
|
||||
print(f"H{n}: {len(per_hall[n])} pillars {sorted(per_hall[n])[:6]}")
|
||||
print(f"OK -> {out_sql} (총 {len(rows)}개)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -59,7 +59,9 @@ def main():
|
||||
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))
|
||||
# 홀 로컬 y=0은 도면 상단(DWG 언더레이 기준) — CAD y 상하 반전.
|
||||
# (V61은 미반전 시드 — V62에서 63-y 보정 완료. 재생성 시 본 반전이 정본.)
|
||||
runs[n][round(BAND_H - (my - y0), 1)].append((lx0, lx1))
|
||||
|
||||
sql = io.StringIO()
|
||||
sql.write(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user