--- name: skeleton-utils 용마루 지붕 두 경로 차이 description: src/util/skeleton-utils.js 의 drawSkeletonRidgeRoof(일반) vs drawSkeletonRidgeRoofFromBaseLines(오버 전용) 차이 + skeletonBuilder 전처리 파이프라인 + SK_INPUT_USE_WALL_BASELINE_DIRECT 플래그. 용마루(전체 EAVES) 케이스 수정 전 reference. type: project --- # skeleton-utils 용마루 지붕 — 일반 vs 오버 두 경로 `src/util/skeleton-utils.js`. **용마루 지붕(라인 type 이 전부 EAVES)** 케이스 전용. straight-skeleton(`SkeletonBuilder.BuildFromGeoJSON`)으로 용마루/추녀 내부선 생성. 박공/한쪽흐름/변별은 `qpolygon-utils.js` 가 담당 (별도 메모리 참조: [[qpolygon-utils 지붕 생성 구조]]). ## 1. 분기 (QPolygon.js:677-684) `drawHelpLine` 에서 `types.every(EAVES)` 일 때: ``` verdict = verifyMoveBoundary(this.id, this.canvas) if verdict === 'crossed': drawSkeletonRidgeRoofFromBaseLines ← 오버(경계 넘음) 전용 else: drawSkeletonRidgeRoof ← 일반 ``` `'crossed'` = 마루/벽 이동이 roof 경계를 넘어선 "오버" 상태. wall.baseLines 가 이미 오버된 좌표로 mutate 됨. ## 2. drawSkeletonRidgeRoof (일반, :71-75) ```js export const drawSkeletonRidgeRoof = (roofId, canvas, textMode) => { skeletonBuilder(roofId, canvas, textMode) // 얇은 래퍼 } ``` 실체는 `skeletonBuilder` (:922). 전처리 파이프라인: 1. roof.lines ↔ wall.lines index 매칭으로 `wallLine` id 주입 2. `baseLines = wall.baseLines.filter(planeSize>0)`, `createOrderedBasePoints` 로 roof.points 순서 정렬 3. **45° 대각 확장 계산** (:1011-1066): contactData(baseLine→roofLine 방향벡터) → `maxStep`(전체 꼭짓점 중 min(|dx|,|dy|) 의 max) → centroid 보정 → `changRoofLinePoints` (maxContactDistance=√2·maxStep 확장) 4. 마루이동(`moveFlowLine`/`moveUpDown` ≠ 0) 시 `safeMovedPointsWithFallback` 의 `movedPoints` 로 대체 (:1071-1121) 5. **⚠️ `SK_INPUT_USE_WALL_BASELINE_DIRECT = true` (:36, :1128)** → 위 3·4 결과를 **전부 무시**하고 `orderedBaseLinePoints`(=wall.baseLines corner) 직접 사용. + 평행오버 corner 흡수(DUP_EPS=1.0, COLLINEAR_EPS=50.0, 최대 5패스) 6. 좌표 유효성/중복점/일직선/**방향 뒤집힘(isOverDetected, cross product 부호 반전)** 검출 (:1175-1236) 7. `roof.skeletonPoints` 갱신 → SK 빌드 → `createInnerLinesFromSkeleton` 즉 45° 확장 코드는 **계산만 하고 버려지는** 상태(점진 리팩토링 흔적). 실입력은 baseLines corner. ## 3. drawSkeletonRidgeRoofFromBaseLines (오버 전용, :1363-1444) 헤더 주석(:1350-1362) 의 존재 이유: 일반 경로의 45° 확장/roof 경계 클램핑이 오버 상태에선 **폴리곤 자가교차/경계 되돌림**을 일으켜 엉뚱한 SK 를 만든다 → 전처리 다 건너뛰고 baseLines 그대로 입력. 처리: 1. `wall.baseLines` 순차 끝점(`{x:bl.x1, y:bl.y1}`) 수집, **확장·offset 없이 그대로**. `planeSize<1`(SHOULDER_ABSORBED) 스킵 2. rawPoints < 3 이면 중단 3. `roof.skeletonPoints = rawPoints` (오버 좌표 그대로) 4. `perturbPolygonPoints` → `toGeoJSON` → `SkeletonBuilder.BuildFromGeoJSON` 5. `createInnerLinesFromSkeleton(..., isOverDetected=true, rawPoints)` 6. `canvas.skeletonStates[roofId]=true`, `canvas.skeletonLines` 갱신 7. **`canvas.skeleton.lastPoints` 는 기존 값 보존** (`__preservedLastPoints`) — 오버 상태에서 새 누적 기준점 안 만듦 **건드리지 않는 것**: `roof.points` / `roof.lines` / `outerLine` / `lengthText` (주석 :1358 명시). ## 4. 핵심 차이 요약 | 항목 | drawSkeletonRidgeRoof (일반) | ...FromBaseLines (오버) | |---|---|---| | 트리거 | EAVES 전부 + verdict ≠ crossed | EAVES 전부 + verdict === crossed | | 실체 | skeletonBuilder 래퍼 | 자체 SK 빌드 | | SK 입력 | 전처리 파이프라인(but 플래그로 baseLine 직접) | baseLines 끝점 그대로 | | roof.points/lines/outerLine/lengthText | 마루이동 시 갱신·검증 | **불변 (side-effect free)** | | lastPoints | 새로 계산/누적 | **보존** | | isOverDetected | cross 부호 반전 검출로 산출 | 무조건 true 마킹 | | 공통 | createInnerLinesFromSkeleton, planeSize<1 스킵 | 동일 | **현 시점 진짜 차이**: 플래그(`SK_INPUT_USE_WALL_BASELINE_DIRECT=true`) 때문에 두 함수의 SK 입력 좌표는 거의 같아짐. 실질 차이는 **주변 상태(roof.points/lastPoints 등) 를 mutate 하느냐 보존하느냐**. 오버를 undo/redo 단축수단으로 쓸 때 history snapshot(wall.baseLines/roof.lines) 오염 없이 SK 만 재그리기 위함 (현재 `feature/redo-undo` 브랜치 맥락). ## 5. 수정 시 주의 1. **45° 확장 코드(:1011-1066) 는 dead 입력** — `SK_INPUT_USE_WALL_BASELINE_DIRECT` 끄지 않는 한 결과 미사용. 만지기 전 플래그 확인. 2. `createInnerLinesFromSkeleton` (:1455) 은 두 경로 공유 — 여기 바꾸면 양쪽 영향. `processEavesEdge`(처마) + `processGableEdge`(케라바 후처리) 호출. 3. 오버 경로는 `lastPoints` 보존이 의도 — 무심코 갱신하면 누적 마루이동 기준 붕괴. 4. 예외 시 보호 정책(:1341-1346): SK/innerLines 유지, `skeletonStates[roofId]=false` 만, `skeletonLines` 비우지 않음 (이전 성공 상태 보존). 5. `verifyMoveBoundary` 는 QPolygon.drawHelpLine 과 skeletonBuilder(:1077) **두 곳에서** 호출 — 'crossed' 시 경고 로그만, 흐름은 진행.