--- name: 평행 이동 SK 붕괴 근본 원인 및 수정 (2차) description: 평행 이동 시 roof.basePoints[index] 좌표 매칭이 wall 오프셋 붕괴(degenerate baseLines)·좌표 우연 일치에 취약해 엉뚱한 꼭짓점에 dy 적용 → SK 축직교 깨짐 → SkeletonBuilder 보조선 난립/붕괴. movingLineFromSkeleton 평행 분기와 buildRawMovedPoints를 인덱스 직접 매핑 + Set 합집합으로 전환. type: project --- **사실:** `skeleton-utils.js`의 평행 이동 dy 적용 경로 2곳을 좌표 매칭 → 인덱스 직접 매핑으로 전환 (2026-04-23 오후). - `movingLineFromSkeleton` 평행 분기: `matchingLines.forEach + newPoints.forEach((point, index) => isSamePoint(roof.basePoints[index], originalStart/End))` 제거. `line.findIndex = k` 기반 `affected = {k, (k+1)%n}` Set 합집합 → `affected.forEach(i => apply dy)` 로 교체. - `buildRawMovedPoints`: 동일 패턴으로 교체. `matchingLines` 는 LP-TRACE 로그용으로 `{line, idx}` 쌍 형태 유지. **Why:** `createOrderedBasePoints`가 오전 커밋(`0d3ede96 보조라인 맞춤`)에서 `baseLines[i].startPoint` 직접 인덱스 매핑으로 고쳐졌지만, **wall 오프셋 계산 단계에서 이미 `baseLines[k]`가 degenerate(zero-length 또는 인접 edge와 start 좌표 공유)** 가 된 형상(L자 shoulder, ㅗ의 shoulder 등)에서는 `roof.basePoints[k]` 가 다른 edge 꼭짓점과 좌표 일치하게 되어 좌표 매칭이 오염됨. polygon topology 로 정해지는 인덱스 `k` 는 이런 좌표 붕괴에 독립적. **Why (Set 합집합):** `matchingLines ≥ 2` 케이스(보강 라인 중복 push, coincident edges)에서 같은 꼭짓점에 dy 가 중복 적용되는 잠재 버그도 동시에 제거. **How to apply:** - `moveFlowLine`(단일 라인 이동) 경로와 무관. 오전에 안정화된 단일 라인 이동 로직은 건드리지 않음. - `createOrderedBasePoints` / `roof.basePoints` 는 다른 경로가 참조할 수 있어 유지. 이 두 지점만 우회. - 평행 누적/붕괴 이동 증상(예: 첫 top_out 에서 `verifyMoveBoundary moved=[0, k-1, k]` 처럼 의도하지 않은 인접 꼭짓점이 함께 이동) 재현 시 이 패치가 적용되어 있는지 먼저 확인. **관련 커밋 히스토리:** - `4d9e2fc0 top 평행수정` (오전): `verifyMoveBoundary` 로깅만 추가. - `0d3ede96 보조라인 맞춤` (오전): `createOrderedBasePoints` graph traversal → 인덱스 직접 매핑. 단, dy 적용 경로는 여전히 좌표 매칭이라 이 패치(2차)로 보완. **죽은 코드 주의:** `skeleton-utils.js`의 `correctedPoints` 교정 블록(skeletonBuilder 내)은 `0d3ede96`에서 블록 주석 처리된 죽은 코드. 건드리지 않는다. --- **3차 후속 수정 (2026-04-23 저녁): SHOULDER_ABSORBED guard** **사실:** `skeleton-utils.js:1482` pair forEach 엔트리에 zero-length `wallBaseLine` 스킵 가드 추가 (`planeSize < 1` 체크). **Why:** `useMovementSetting.js:handleSave` 의 이웃 흡수 로직 (line 766-782) 이 shoulder 가 이동량에 의해 흡수되는 경우 `nextLine`/`prevLine` 을 `(x,y)→(x,y)` zero-length 로 mutate 하고 `planeSize=0` 으로 저장. 이는 의도된 geometry 표현 (흡수된 edge 는 "없어진" 상태). 다만 downstream `skeleton-utils.js` pair 매칭에서 이를 그대로 받아 `⚠️DIR_MISMATCH` 로그 + 잘못된 eaveHelpLine 생성 → SK 보조선 난립. **How to apply:** - `skeleton-utils.js` pair 생성부 `sortWallLines.forEach((wallLine, index) => { ... })` 최상단에 `if ((wallBaseLine.attributes?.planeSize ?? Infinity) < 1) return` 가드. - 같은 파일 line 666 의 `wall.baseLines.filter(line => line.attributes.planeSize > 0)` 선례와 동일 패턴. - 생산지(`useMovementSetting.js`) 는 의도대로 동작 중이라 건드리지 않음 (잘 되는 코드 건들지 않기 원칙). - 대안이었던 "sortWallBaseLines 배열 자체 필터" 는 `sortRoofLines[index]`/`sortWallLines[index]` 와 인덱스 align 이 깨져 불가. per-iteration guard 가 최소 변경. **재현 체크:** `⏭️ [pair#N] SHOULDER_ABSORBED skip` 로그가 찍히면 가드가 걸린 것. --- **4차 후속 수정 (2026-04-23 저녁): HIP_SKIP adj_absorbed guard** **사실:** `skeleton-utils.js:processInBoth` 진입부에 인접 pair 흡수 감지 시 HIP extensionLine 생성 스킵 가드 추가. **Why:** SHOULDER_ABSORBED 가드로 pair forEach 의 DIR_MISMATCH 는 해결됐지만, 남은 인접 pair 의 `processInBoth` 가 원본 `roof.points` 기반 꼭짓점 (흡수로 사라진 shoulder 위치) 으로 HIP 빗변 끝점을 설정해 **"공중에 뜬 orange 대각선"** 을 만들어냄. 화면상 blue 사각형(simplified ridge) 외곽 코너에 길이 70.7 짜리 빗변이 orphan 으로 남는 증상. **How to apply:** - `processInBoth` 함수 최상단에 `sortWallBaseLines[(index±1)%n].attributes.planeSize < 1` 체크, 인접 중 하나라도 흡수된 경우 early return. - processInBoth 는 getMoveUpDownLine 스코프 내부 정의라 sortWallBaseLines 클로저 접근 가능. - 호출처 2곳 (line 1675, 1930) 에 중복 가드 안 넣고 함수 진입부 한 곳에서 처리. - processInStartEnd (blue/green/pink/red 보조선) 은 건드리지 않음 — HIP 빗변 전용 문제. **재현 체크:** `⏭️ [pair#N] X_in HIP_SKIP adj_absorbed prev=T/F next=T/F` 로그로 확인.