feat(m2): V63 wall/stair/elevator/door obstacle recognition + avoidance
Owner directive 2026-07-15: recognize walls, stairs, elevators, doors, drywall. - tools/cad/extract_obstacle_seed.py: rasterize hall floor into 1m cells over obstacle layers (WALL/DRYWALL/masonry/panel, STAIR, ELEVATOR, DOOR), connected components, row-strip decomposition (prevents bbox inflation of L-shaped thin wall runs) -> 60 measured rects for K1 halls 1-5 - V63: hall_obstacle table (kind: stair|elevator|door|wall) + seeds - Engine: auto-layout reserves obstacles with kind-based buffer (door 1.0m approach clearance, others 0.3m) - Canvas: structure layer renders obstacle rects (door tinted green); HallInfo.obstacles contract extension (additive) - Limits recorded: wall strips not filled room interiors (perimeter margin + wall strips still block booth intrusion); K2 deferred (different layer set) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
6ff692c03f
commit
ffa133aee5
@ -340,3 +340,7 @@ CAD 레이어 `5-트랜치`(표기 함정 — 트렌치≠트랜치)에서 K1
|
|||||||
### 세션 5 추가 — 기둥 추출·자동배치 회피 (2026-07-15, 소유자 지시 "기둥 추출해서 자동배치 회피 반영해")
|
### 세션 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→배포.
|
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→배포.
|
||||||
|
|
||||||
|
### 세션 5 추가 — V63 벽·계단·엘리베이터·문 인식 (2026-07-15, 소유자 지시 "V63 벽·계단 인식 진행해" + "엘리베이터,문,DRYWALL 까지 함께")
|
||||||
|
|
||||||
|
hall_obstacle 60개 시드(K1 H1~H5) — 1m 셀 래스터·연결성분·행 스트립 분해로 벽/계단/엘리베이터/문 실측 추출. 자동배치가 문 1.0m·기타 0.3m 버퍼로 회피, 캔버스 구조 레이어 사각 표시. K2는 레이어 구조 상이로 보류(한계 기록). 게이트: gradle test·tsc·vite·XML 전부 통과 → push→배포.
|
||||||
|
|||||||
@ -107,3 +107,13 @@ H7/H8 블록 252m=126×2·폭 90m 정확 일치) + 회전 프로브 렌더 시
|
|||||||
(RT 포인트가 CAD y 미반전으로 시드됨 — 도면 상단 y=0 기준으로 63-y 반전, 생성기도 정본 수정).
|
(RT 포인트가 CAD y 미반전으로 시드됨 — 도면 상단 y=0 기준으로 63-y 반전, 생성기도 정본 수정).
|
||||||
- **엔진**: autoGenerate가 기둥 반경+0.6m 버퍼를 예약 영역으로 — 겹치는 부스 셀 배치 제외.
|
- **엔진**: autoGenerate가 기둥 반경+0.6m 버퍼를 예약 영역으로 — 겹치는 부스 셀 배치 제외.
|
||||||
- **캔버스**: 구조 레이어(기둥·비상구 토글)에 기둥 원 표시(HallInfo.pillars 계약 확장, NON_NULL 호환).
|
- **캔버스**: 구조 레이어(기둥·비상구 토글)에 기둥 원 표시(HallInfo.pillars 계약 확장, NON_NULL 호환).
|
||||||
|
|
||||||
|
## V63 벽·계단·엘리베이터·문 장애물 인식 (2026-07-15)
|
||||||
|
|
||||||
|
- **추출(K1 H1~H5)**: 장애물 레이어(WALL/DRYWALL/조적/석고/판넬·STAIR/계단·ELEVATOR·DOOR/출입구)를
|
||||||
|
1m 셀 래스터 → 연결 성분 → **행 스트립 분해**(L자 벽 런의 bbox 뻥튀기 방지) → 60개 사각 영역.
|
||||||
|
좌측 벽선(x0~1m)·우측 에지 문(x169~171)·H1 하단 부속실·H5 상단 부속실 벽 등.
|
||||||
|
- **V63**: hall_obstacle(kind: stair|elevator|door|wall) + 시드. 자동배치 회피 버퍼: 문 1.0m(접근
|
||||||
|
클리어)·기타 0.3m. 캔버스 구조 레이어에 사각 표시(문=녹색톤 구분).
|
||||||
|
- **한계**: ①벽선 기반 — 밀폐 실내 채움 아님(단, 외곽 여유 5m+벽 스트립 차단으로 실배치 유입 방지)
|
||||||
|
②K2(H6~10)는 장애물 레이어 구조 상이(석고보드·경량철골 등 xref 평탄화)로 신뢰 추출 보류.
|
||||||
|
|||||||
@ -299,6 +299,13 @@ public class FloorplanServiceImpl implements FloorplanService {
|
|||||||
pillarZones.add(new double[]{p.x() - buf, p.y() - buf, p.x() + buf, p.y() + buf});
|
pillarZones.add(new double[]{p.x() - buf, p.y() - buf, p.x() + buf, p.y() + buf});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 벽·계단·엘리베이터·문 회피(V63 실측) — 문은 접근 클리어 1.0m, 그 외 0.3m 여유.
|
||||||
|
if (hall.obstacles() != null) {
|
||||||
|
for (HallInfo.Obstacle o : hall.obstacles()) {
|
||||||
|
double buf = "door".equals(o.kind()) ? 1.0 : 0.3;
|
||||||
|
pillarZones.add(new double[]{o.x0() - buf, o.y0() - buf, o.x1() + buf, o.y1() + buf});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
List<AutoLayoutOption> options = new ArrayList<>();
|
List<AutoLayoutOption> options = new ArrayList<>();
|
||||||
for (int i = 0; i < optionCount; i++) {
|
for (int i = 0; i < optionCount; i++) {
|
||||||
@ -592,9 +599,18 @@ public class FloorplanServiceImpl implements FloorplanService {
|
|||||||
nz(dbl(p.get("x"))), nz(dbl(p.get("y"))), nz(dbl(p.get("radiusM")))));
|
nz(dbl(p.get("x"))), nz(dbl(p.get("y"))), nz(dbl(p.get("radiusM")))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
List<HallInfo.Obstacle> obstacles = new ArrayList<>();
|
||||||
|
List<Map<String, Object>> obs = hallMapper.findObstacles(hallId);
|
||||||
|
if (obs != null) {
|
||||||
|
for (Map<String, Object> o : obs) {
|
||||||
|
obstacles.add(new HallInfo.Obstacle(str(o.get("obstacleId")), str(o.get("kind")),
|
||||||
|
nz(dbl(o.get("x0"))), nz(dbl(o.get("y0"))),
|
||||||
|
nz(dbl(o.get("x1"))), nz(dbl(o.get("y1")))));
|
||||||
|
}
|
||||||
|
}
|
||||||
return new HallInfo(str(h.get("hallId")), str(h.get("label")), dims, dbl(h.get("heightM")),
|
return new HallInfo(str(h.get("hallId")), str(h.get("label")), dims, dbl(h.get("heightM")),
|
||||||
intOrNull(h.get("boothCapacity")), dbl(h.get("floorLoadTPerM2")),
|
intOrNull(h.get("boothCapacity")), dbl(h.get("floorLoadTPerM2")),
|
||||||
Boolean.TRUE.equals(bool(h.get("assumedTrench"))), trenches, exits, pillars);
|
Boolean.TRUE.equals(bool(h.get("assumedTrench"))), trenches, exits, pillars, obstacles);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int nextVersion(String eventId, String hallId) {
|
private int nextVersion(String eventId, String hallId) {
|
||||||
|
|||||||
@ -29,9 +29,20 @@ public record HallInfo(
|
|||||||
boolean assumedTrench,
|
boolean assumedTrench,
|
||||||
List<Trench> trenches,
|
List<Trench> trenches,
|
||||||
List<Exit> exits,
|
List<Exit> exits,
|
||||||
List<Pillar> pillars
|
List<Pillar> pillars,
|
||||||
|
List<Obstacle> obstacles
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 벽·계단·엘리베이터·문 장애물(V63 hall_obstacle, CAD 실측 스트립) — 자동배치 회피·캔버스 표시.
|
||||||
|
*
|
||||||
|
* @param obstacleId 장애물 식별자
|
||||||
|
* @param kind stair | elevator | door | wall
|
||||||
|
* @param x0,y0,x1,y1 홀 로컬 사각 영역(m, y=0 도면 상단)
|
||||||
|
*/
|
||||||
|
public record Obstacle(String obstacleId, String kind, double x0, double y0, double x1, double y1) {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 기둥(구조 장애물) — 자동배치 회피·캔버스 표시 근거(V62 hall_pillar, CAD/공식 도면 실측).
|
* 기둥(구조 장애물) — 자동배치 회피·캔버스 표시 근거(V62 hall_pillar, CAD/공식 도면 실측).
|
||||||
*
|
*
|
||||||
|
|||||||
@ -35,4 +35,7 @@ public interface HallMapper {
|
|||||||
|
|
||||||
/** 홀 기둥(V62 hall_pillar, 실측) — 자동배치 회피·캔버스 표시. */
|
/** 홀 기둥(V62 hall_pillar, 실측) — 자동배치 회피·캔버스 표시. */
|
||||||
List<Map<String, Object>> findPillars(@Param("hallId") String hallId);
|
List<Map<String, Object>> findPillars(@Param("hallId") String hallId);
|
||||||
|
|
||||||
|
/** 벽·계단·엘리베이터·문 장애물(V63 hall_obstacle, 실측) — 자동배치 회피·캔버스 표시. */
|
||||||
|
List<Map<String, Object>> findObstacles(@Param("hallId") String hallId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,78 @@
|
|||||||
|
-- V63: 벽·계단·엘리베이터 장애물 실측 시드(hall_obstacle) — 자동배치 회피 근거(소유자 지시 2026-07-15).
|
||||||
|
-- 생성기: tools/cad/extract_obstacle_seed.py (1m 셀 래스터 연결성분 bbox, 결정적 재생성).
|
||||||
|
-- K1 H1~H5 = CAD 추출. K2 = 장애물 레이어 신뢰 추출 불가(도면 구조 상이) — 미시드(한계, cad-extraction.md).
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS hall_obstacle (
|
||||||
|
id varchar(60) PRIMARY KEY, -- 예: H1-OB-1
|
||||||
|
hall_id varchar(20) NOT NULL REFERENCES hall(id) ON DELETE CASCADE,
|
||||||
|
kind varchar(20) NOT NULL, -- stair | elevator | door | wall(부속실·DRYWALL 포함)
|
||||||
|
x0 numeric(8,2) NOT NULL, y0 numeric(8,2) NOT NULL,
|
||||||
|
x1 numeric(8,2) NOT NULL, y1 numeric(8,2) NOT NULL -- 홀 로컬 m(y=0 도면 상단)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_hall_obstacle_hall ON hall_obstacle (hall_id);
|
||||||
|
|
||||||
|
DELETE FROM hall_obstacle WHERE id LIKE 'H%-OB-%';
|
||||||
|
INSERT INTO hall_obstacle (id, hall_id, kind, x0, y0, x1, y1)
|
||||||
|
VALUES
|
||||||
|
('H1-OB-1', 'H1', 'wall', 0.0, 0.0, 1.0, 12.0),
|
||||||
|
('H1-OB-2', 'H1', 'wall', 0.0, 14.0, 1.0, 27.0),
|
||||||
|
('H1-OB-3', 'H1', 'door', 0.0, 34.0, 1.0, 52.0),
|
||||||
|
('H1-OB-4', 'H1', 'door', 0.0, 52.0, 19.0, 54.0),
|
||||||
|
('H1-OB-5', 'H1', 'door', 2.0, 61.0, 9.0, 62.0),
|
||||||
|
('H1-OB-6', 'H1', 'door', 3.0, 62.0, 4.0, 63.0),
|
||||||
|
('H1-OB-7', 'H1', 'door', 5.0, 62.0, 7.0, 63.0),
|
||||||
|
('H1-OB-8', 'H1', 'door', 8.0, 54.0, 9.0, 61.0),
|
||||||
|
('H1-OB-9', 'H1', 'door', 8.0, 62.0, 9.0, 63.0),
|
||||||
|
('H1-OB-10', 'H1', 'door', 17.0, 54.0, 19.0, 61.0),
|
||||||
|
('H1-OB-11', 'H1', 'door', 17.0, 61.0, 65.0, 62.0),
|
||||||
|
('H1-OB-12', 'H1', 'door', 17.0, 62.0, 67.0, 63.0),
|
||||||
|
('H1-OB-13', 'H1', 'wall', 70.0, 61.0, 84.0, 62.0),
|
||||||
|
('H1-OB-14', 'H1', 'wall', 70.0, 62.0, 88.0, 63.0),
|
||||||
|
('H1-OB-15', 'H1', 'wall', 89.0, 61.0, 91.0, 63.0),
|
||||||
|
('H1-OB-16', 'H1', 'door', 96.0, 61.0, 104.0, 62.0),
|
||||||
|
('H1-OB-17', 'H1', 'door', 96.0, 62.0, 105.0, 63.0),
|
||||||
|
('H1-OB-18', 'H1', 'door', 106.0, 61.0, 137.0, 63.0),
|
||||||
|
('H1-OB-19', 'H1', 'wall', 142.0, 61.0, 171.0, 63.0),
|
||||||
|
('H1-OB-20', 'H1', 'door', 169.0, 0.0, 171.0, 1.0),
|
||||||
|
('H1-OB-21', 'H1', 'door', 170.0, 1.0, 171.0, 2.0),
|
||||||
|
('H1-OB-22', 'H1', 'wall', 170.0, 7.0, 171.0, 19.0),
|
||||||
|
('H1-OB-23', 'H1', 'door', 170.0, 22.0, 171.0, 40.0),
|
||||||
|
('H1-OB-24', 'H1', 'wall', 170.0, 41.0, 171.0, 55.0),
|
||||||
|
('H2-OB-1', 'H2', 'wall', 0.0, 0.0, 1.0, 21.0),
|
||||||
|
('H2-OB-2', 'H2', 'wall', 0.0, 39.0, 1.0, 63.0),
|
||||||
|
('H2-OB-3', 'H2', 'door', 169.0, 0.0, 171.0, 1.0),
|
||||||
|
('H2-OB-4', 'H2', 'door', 170.0, 1.0, 171.0, 2.0),
|
||||||
|
('H2-OB-5', 'H2', 'door', 170.0, 7.0, 171.0, 28.0),
|
||||||
|
('H2-OB-6', 'H2', 'door', 170.0, 31.0, 171.0, 49.0),
|
||||||
|
('H2-OB-7', 'H2', 'wall', 170.0, 50.0, 171.0, 54.0),
|
||||||
|
('H2-OB-8', 'H2', 'door', 170.0, 59.0, 171.0, 63.0),
|
||||||
|
('H3-OB-1', 'H3', 'wall', 0.0, 0.0, 1.0, 21.0),
|
||||||
|
('H3-OB-2', 'H3', 'wall', 0.0, 39.0, 1.0, 63.0),
|
||||||
|
('H3-OB-3', 'H3', 'door', 169.0, 0.0, 171.0, 1.0),
|
||||||
|
('H3-OB-4', 'H3', 'door', 170.0, 1.0, 171.0, 2.0),
|
||||||
|
('H3-OB-5', 'H3', 'door', 170.0, 7.0, 171.0, 28.0),
|
||||||
|
('H3-OB-6', 'H3', 'door', 170.0, 31.0, 171.0, 49.0),
|
||||||
|
('H3-OB-7', 'H3', 'wall', 170.0, 50.0, 171.0, 54.0),
|
||||||
|
('H3-OB-8', 'H3', 'door', 170.0, 59.0, 171.0, 63.0),
|
||||||
|
('H4-OB-1', 'H4', 'wall', 0.0, 0.0, 1.0, 21.0),
|
||||||
|
('H4-OB-2', 'H4', 'wall', 0.0, 39.0, 1.0, 63.0),
|
||||||
|
('H4-OB-3', 'H4', 'door', 169.0, 0.0, 171.0, 1.0),
|
||||||
|
('H4-OB-4', 'H4', 'door', 170.0, 1.0, 171.0, 2.0),
|
||||||
|
('H4-OB-5', 'H4', 'door', 170.0, 7.0, 171.0, 20.0),
|
||||||
|
('H4-OB-6', 'H4', 'wall', 170.0, 22.0, 171.0, 28.0),
|
||||||
|
('H4-OB-7', 'H4', 'door', 170.0, 31.0, 171.0, 49.0),
|
||||||
|
('H4-OB-8', 'H4', 'wall', 170.0, 50.0, 171.0, 54.0),
|
||||||
|
('H4-OB-9', 'H4', 'door', 170.0, 59.0, 171.0, 63.0),
|
||||||
|
('H5-OB-1', 'H5', 'wall', 0.0, 7.0, 19.0, 9.0),
|
||||||
|
('H5-OB-2', 'H5', 'wall', 0.0, 9.0, 1.0, 21.0),
|
||||||
|
('H5-OB-3', 'H5', 'wall', 0.0, 24.0, 1.0, 27.0),
|
||||||
|
('H5-OB-4', 'H5', 'wall', 0.0, 34.0, 1.0, 63.0),
|
||||||
|
('H5-OB-5', 'H5', 'wall', 17.0, 0.0, 19.0, 7.0),
|
||||||
|
('H5-OB-6', 'H5', 'door', 170.0, 0.0, 171.0, 3.0),
|
||||||
|
('H5-OB-7', 'H5', 'wall', 170.0, 6.0, 171.0, 20.0),
|
||||||
|
('H5-OB-8', 'H5', 'wall', 170.0, 22.0, 171.0, 28.0),
|
||||||
|
('H5-OB-9', 'H5', 'door', 170.0, 31.0, 171.0, 49.0),
|
||||||
|
('H5-OB-10', 'H5', 'wall', 170.0, 50.0, 171.0, 54.0),
|
||||||
|
('H5-OB-11', 'H5', 'door', 170.0, 59.0, 171.0, 63.0)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET kind = EXCLUDED.kind,
|
||||||
|
x0 = EXCLUDED.x0, y0 = EXCLUDED.y0, x1 = EXCLUDED.x1, y1 = EXCLUDED.y1;
|
||||||
@ -93,4 +93,14 @@
|
|||||||
ORDER BY id
|
ORDER BY id
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<!-- 벽·계단·엘리베이터·문 장애물(V63 실측) — 자동배치 회피·캔버스 표시. -->
|
||||||
|
<select id="findObstacles" resultType="map">
|
||||||
|
SELECT id AS "obstacleId",
|
||||||
|
kind,
|
||||||
|
x0, y0, x1, y1
|
||||||
|
FROM hall_obstacle
|
||||||
|
WHERE hall_id = #{hallId}
|
||||||
|
ORDER BY id
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@ -170,6 +170,15 @@ export interface HallPillarDto {
|
|||||||
y: number;
|
y: number;
|
||||||
radiusM: number;
|
radiusM: number;
|
||||||
}
|
}
|
||||||
|
/** 벽·계단·엘리베이터·문 장애물(V63 실측 스트립) — 자동배치 회피·캔버스 표시. */
|
||||||
|
export interface HallObstacleDto {
|
||||||
|
obstacleId: string;
|
||||||
|
kind: 'stair' | 'elevator' | 'door' | 'wall';
|
||||||
|
x0: number;
|
||||||
|
y0: number;
|
||||||
|
x1: number;
|
||||||
|
y1: number;
|
||||||
|
}
|
||||||
/** 홀 마스터 정보(M2 LayoutDto.hall) — 캔버스 축척·경계 원천. dimsM=[폭, 깊이](m). */
|
/** 홀 마스터 정보(M2 LayoutDto.hall) — 캔버스 축척·경계 원천. dimsM=[폭, 깊이](m). */
|
||||||
export interface HallInfoDto {
|
export interface HallInfoDto {
|
||||||
hallId: string;
|
hallId: string;
|
||||||
@ -178,6 +187,7 @@ export interface HallInfoDto {
|
|||||||
ceilingM?: number | null;
|
ceilingM?: number | null;
|
||||||
targetBoothCount?: number | null;
|
targetBoothCount?: number | null;
|
||||||
pillars?: HallPillarDto[] | null;
|
pillars?: HallPillarDto[] | null;
|
||||||
|
obstacles?: HallObstacleDto[] | null;
|
||||||
}
|
}
|
||||||
export interface LayoutDto {
|
export interface LayoutDto {
|
||||||
layoutId: string;
|
layoutId: string;
|
||||||
|
|||||||
@ -255,6 +255,7 @@ export function BoothLayoutEditorPage() {
|
|||||||
floorplanUrl={drawingTab === 'dwg' ? hallDwgUrl(hallId) : hallFloorplanUrl(hallId)}
|
floorplanUrl={drawingTab === 'dwg' ? hallDwgUrl(hallId) : hallFloorplanUrl(hallId)}
|
||||||
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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import type { BoothDto, HallPillarDto } from '../../api/types';
|
import type { BoothDto, HallObstacleDto, HallPillarDto } from '../../api/types';
|
||||||
import './canvas.css';
|
import './canvas.css';
|
||||||
|
|
||||||
const ZOOM_MIN = 0.3;
|
const ZOOM_MIN = 0.3;
|
||||||
@ -39,6 +39,8 @@ interface FloorplanCanvasProps {
|
|||||||
onZoomChange?: (zoom: number) => void;
|
onZoomChange?: (zoom: number) => void;
|
||||||
/** 홀 기둥(V62 실측) — 구조 레이어에 원으로 표시(자동배치는 서버가 회피). */
|
/** 홀 기둥(V62 실측) — 구조 레이어에 원으로 표시(자동배치는 서버가 회피). */
|
||||||
pillars?: HallPillarDto[] | null;
|
pillars?: HallPillarDto[] | null;
|
||||||
|
/** 벽·계단·엘리베이터·문 장애물(V63 실측) — 구조 레이어에 사각으로 표시. */
|
||||||
|
obstacles?: HallObstacleDto[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TRENCH_SPACING_M = 9; // 가정 트렌치 그리드 간격(실측 대체 — R4)
|
const TRENCH_SPACING_M = 9; // 가정 트렌치 그리드 간격(실측 대체 — R4)
|
||||||
@ -66,6 +68,7 @@ export function FloorplanCanvas({
|
|||||||
floorRegion,
|
floorRegion,
|
||||||
onZoomChange,
|
onZoomChange,
|
||||||
pillars,
|
pillars,
|
||||||
|
obstacles,
|
||||||
}: FloorplanCanvasProps) {
|
}: FloorplanCanvasProps) {
|
||||||
const [hw, hh] = hallDims;
|
const [hw, hh] = hallDims;
|
||||||
|
|
||||||
@ -200,6 +203,22 @@ export function FloorplanCanvas({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 벽·계단·엘리베이터·문 장애물(V63 실측) — 구조 레이어 */}
|
||||||
|
{layers.structure && obstacles && obstacles.length > 0 && (
|
||||||
|
<g className="kx-canvas__obstacles" aria-hidden="true">
|
||||||
|
{obstacles.map((o) => (
|
||||||
|
<rect
|
||||||
|
key={o.obstacleId}
|
||||||
|
className={`is-${o.kind}`}
|
||||||
|
x={o.x0}
|
||||||
|
y={o.y0}
|
||||||
|
width={o.x1 - o.x0}
|
||||||
|
height={o.y1 - o.y0}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</g>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 기둥(V62 실측) — 구조 레이어 */}
|
{/* 기둥(V62 실측) — 구조 레이어 */}
|
||||||
{layers.structure && pillars && pillars.length > 0 && (
|
{layers.structure && pillars && pillars.length > 0 && (
|
||||||
<g className="kx-canvas__pillars" aria-hidden="true">
|
<g className="kx-canvas__pillars" aria-hidden="true">
|
||||||
|
|||||||
@ -36,6 +36,21 @@
|
|||||||
stroke-width: 0.2;
|
stroke-width: 0.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 장애물(V63 실측): 벽/계단/엘리베이터=차단 톤, 문=접근 클리어 톤 */
|
||||||
|
.kx-canvas__obstacles rect {
|
||||||
|
fill: rgba(122, 92, 60, 0.35);
|
||||||
|
stroke: #8a7355;
|
||||||
|
stroke-width: 0.12;
|
||||||
|
}
|
||||||
|
.kx-canvas__obstacles rect.is-door {
|
||||||
|
fill: rgba(64, 145, 108, 0.3);
|
||||||
|
stroke: #40916c;
|
||||||
|
}
|
||||||
|
.kx-canvas__obstacles rect.is-stair,
|
||||||
|
.kx-canvas__obstacles rect.is-elevator {
|
||||||
|
fill: rgba(122, 92, 60, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
.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;
|
||||||
|
|||||||
200
tools/cad/extract_obstacle_seed.py
Normal file
200
tools/cad/extract_obstacle_seed.py
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""벽·계단·엘리베이터 장애물 실측 시드 생성기 — V63 (hall_obstacle).
|
||||||
|
|
||||||
|
방식: 홀 바닥면(로컬 m)을 1m 셀로 래스터화, 장애물 레이어(WALL/STAIR/ELEVATOR/DRYWALL 계열)
|
||||||
|
지오메트리가 지나는 셀을 마킹 → 연결 성분 → bbox 를 장애 영역으로 시드.
|
||||||
|
자동배치는 이 영역(+여유)을 예약 영역으로 회피한다.
|
||||||
|
|
||||||
|
필터: 면적 3m² 미만(파편)·최소변 1.2m 미만(단순 벽선)·폭/깊이 80% 이상 관통 박판(외곽 벽선) 제외.
|
||||||
|
|
||||||
|
사용:
|
||||||
|
python tools/cad/extract_obstacle_seed.py <k1_trench.dxf> <out.sql>
|
||||||
|
|
||||||
|
좌표 규약: 홀 로컬 m, y=0 도면 상단(= CAD y 상하 반전, V62와 동일).
|
||||||
|
"""
|
||||||
|
import io
|
||||||
|
import math
|
||||||
|
import sys
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
KIND_LAYERS = {
|
||||||
|
"stair": ("STAIR", "계단"),
|
||||||
|
"elevator": ("ELEVATOR", "ELV", "승강"),
|
||||||
|
"door": ("DOOR", "출입구", "4-창호"),
|
||||||
|
"wall": ("WALL", "DRYWALL", "조적", "석고", "판넬"),
|
||||||
|
}
|
||||||
|
CELL = 1.0 # m
|
||||||
|
MIN_AREA = 3.0 # wall/stair/elevator — 파편 제거
|
||||||
|
MIN_DIM = 1.2
|
||||||
|
DOOR_MIN_AREA = 0.5 # 문 심볼은 소형 — 별도 기준(문 전면 클리어 확보 목적)
|
||||||
|
|
||||||
|
|
||||||
|
def kind_of(layer):
|
||||||
|
u = layer.upper()
|
||||||
|
for kind, keys in KIND_LAYERS.items():
|
||||||
|
if any(k.upper() in u for k in keys):
|
||||||
|
return kind
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
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 main():
|
||||||
|
src, out_sql = sys.argv[1], sys.argv[2]
|
||||||
|
doc = ezdxf.readfile(src)
|
||||||
|
msp = doc.modelspace()
|
||||||
|
|
||||||
|
W, H = int(FLOOR_W / CELL), int(BAND_H / CELL)
|
||||||
|
grids = {n: [[None] * W for _ in range(H)] for n in K1_HALLS} # kind 마킹
|
||||||
|
|
||||||
|
for e in msp:
|
||||||
|
kind = kind_of(e.dxf.layer)
|
||||||
|
if not kind:
|
||||||
|
continue
|
||||||
|
for x, y in sample_points(e):
|
||||||
|
lx = x - FLOOR_X0
|
||||||
|
if not (0 <= lx < FLOOR_W):
|
||||||
|
continue
|
||||||
|
for n in K1_HALLS:
|
||||||
|
y0 = BAND_Y0 + (n - 1) * BAND_H
|
||||||
|
if y0 <= y < y0 + BAND_H:
|
||||||
|
ly = BAND_H - (y - y0) # 상하 반전
|
||||||
|
ci, cj = int(lx / CELL), min(H - 1, int(ly / CELL))
|
||||||
|
cur = grids[n][cj][ci]
|
||||||
|
# stair/elevator 가 wall 보다 우선(용도 표기)
|
||||||
|
if cur is None or (cur == "wall" and kind != "wall"):
|
||||||
|
grids[n][cj][ci] = kind
|
||||||
|
|
||||||
|
sql = io.StringIO()
|
||||||
|
sql.write(
|
||||||
|
"-- V63: 벽·계단·엘리베이터 장애물 실측 시드(hall_obstacle) — 자동배치 회피 근거(소유자 지시 2026-07-15).\n"
|
||||||
|
"-- 생성기: tools/cad/extract_obstacle_seed.py (1m 셀 래스터 연결성분 bbox, 결정적 재생성).\n"
|
||||||
|
"-- K1 H1~H5 = CAD 추출. K2 = 장애물 레이어 신뢰 추출 불가(도면 구조 상이) — 미시드(한계, cad-extraction.md).\n\n"
|
||||||
|
"CREATE TABLE IF NOT EXISTS hall_obstacle (\n"
|
||||||
|
" id varchar(60) PRIMARY KEY, -- 예: H1-OB-1\n"
|
||||||
|
" hall_id varchar(20) NOT NULL REFERENCES hall(id) ON DELETE CASCADE,\n"
|
||||||
|
" kind varchar(20) NOT NULL, -- stair | elevator | door | wall(부속실·DRYWALL 포함)\n"
|
||||||
|
" x0 numeric(8,2) NOT NULL, y0 numeric(8,2) NOT NULL,\n"
|
||||||
|
" x1 numeric(8,2) NOT NULL, y1 numeric(8,2) NOT NULL -- 홀 로컬 m(y=0 도면 상단)\n"
|
||||||
|
");\n"
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_hall_obstacle_hall ON hall_obstacle (hall_id);\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
report = []
|
||||||
|
for n in K1_HALLS:
|
||||||
|
g = grids[n]
|
||||||
|
seen = [[False] * W for _ in range(H)]
|
||||||
|
comps = []
|
||||||
|
for j in range(H):
|
||||||
|
for i in range(W):
|
||||||
|
if g[j][i] is None or seen[j][i]:
|
||||||
|
continue
|
||||||
|
q = deque([(i, j)])
|
||||||
|
seen[j][i] = True
|
||||||
|
cells = []
|
||||||
|
kinds = {}
|
||||||
|
while q:
|
||||||
|
ci, cj = q.popleft()
|
||||||
|
cells.append((ci, cj))
|
||||||
|
k = g[cj][ci]
|
||||||
|
kinds[k] = kinds.get(k, 0) + 1
|
||||||
|
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 0 <= ni < W and 0 <= nj < H and g[nj][ni] is not None and not seen[nj][ni]:
|
||||||
|
seen[nj][ni] = True
|
||||||
|
q.append((ni, nj))
|
||||||
|
area = len(cells) * CELL * CELL
|
||||||
|
# 용도 우선순위: stair > elevator > door > wall
|
||||||
|
kind = ("stair" if kinds.get("stair")
|
||||||
|
else "elevator" if kinds.get("elevator")
|
||||||
|
else "door" if kinds.get("door")
|
||||||
|
else "wall")
|
||||||
|
min_area = DOOR_MIN_AREA if kind == "door" else MIN_AREA
|
||||||
|
if area < min_area:
|
||||||
|
continue
|
||||||
|
# ★bbox 뻥튀기 방지(L자형 얇은 벽 런): 행 스트립 분해 — 행별 연속 구간을
|
||||||
|
# rect 로 만들고 동일 x구간 인접 행을 세로 병합(실 커버리지 근사).
|
||||||
|
by_row = {}
|
||||||
|
for ci, cj in cells:
|
||||||
|
by_row.setdefault(cj, []).append(ci)
|
||||||
|
strips = [] # (j0, j1, i0, i1)
|
||||||
|
for j in sorted(by_row):
|
||||||
|
cols = sorted(by_row[j])
|
||||||
|
seg = [cols[0], cols[0]]
|
||||||
|
segs = []
|
||||||
|
for c in cols[1:]:
|
||||||
|
if c == seg[1] + 1:
|
||||||
|
seg[1] = c
|
||||||
|
else:
|
||||||
|
segs.append(tuple(seg))
|
||||||
|
seg = [c, c]
|
||||||
|
segs.append(tuple(seg))
|
||||||
|
for i0, i1 in segs:
|
||||||
|
merged_flag = False
|
||||||
|
for s in strips:
|
||||||
|
if s[1] == j - 1 and s[2] == i0 and s[3] == i1:
|
||||||
|
s[1] = j
|
||||||
|
merged_flag = True
|
||||||
|
break
|
||||||
|
if not merged_flag:
|
||||||
|
strips.append([j, j, i0, i1])
|
||||||
|
for j0, j1, i0, i1 in strips:
|
||||||
|
x0, x1 = i0 * CELL, (i1 + 1) * CELL
|
||||||
|
y0, y1 = j0 * CELL, (j1 + 1) * CELL
|
||||||
|
w, h = x1 - x0, y1 - y0
|
||||||
|
if w * h < min_area and not (kind == "door" and w * h >= DOOR_MIN_AREA):
|
||||||
|
continue
|
||||||
|
if (w >= FLOOR_W * 0.8 and h <= 2.5) or (h >= BAND_H * 0.8 and w <= 2.5):
|
||||||
|
continue # 외곽 벽선 관통 박판
|
||||||
|
comps.append((kind, x0, y0, x1, y1, w * h))
|
||||||
|
comps.sort(key=lambda c: (c[1], c[2]))
|
||||||
|
report.append(f"H{n}: {len(comps)}개 " + ", ".join(f"{c[0]}({c[1]:.0f},{c[2]:.0f})-({c[3]:.0f},{c[4]:.0f})" for c in comps[:8]))
|
||||||
|
for i, (kind, x0, y0, x1, y1, _a) in enumerate(comps, 1):
|
||||||
|
rows.append(f"('H{n}-OB-{i}', 'H{n}', '{kind}', {x0:.1f}, {y0:.1f}, {x1:.1f}, {y1:.1f})")
|
||||||
|
|
||||||
|
if rows:
|
||||||
|
sql.write("DELETE FROM hall_obstacle WHERE id LIKE 'H%-OB-%';\n")
|
||||||
|
sql.write(
|
||||||
|
"INSERT INTO hall_obstacle (id, hall_id, kind, x0, y0, x1, y1)\nVALUES\n "
|
||||||
|
+ ",\n ".join(rows)
|
||||||
|
+ "\nON CONFLICT (id) DO UPDATE SET kind = EXCLUDED.kind,\n"
|
||||||
|
" x0 = EXCLUDED.x0, y0 = EXCLUDED.y0, x1 = EXCLUDED.x1, y1 = EXCLUDED.y1;\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} (장애물 {len(rows)}개)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Reference in New Issue
Block a user