--- name: 오버 이동은 평행 위치까지 클램핑 (mutation 거부 아님) description: useMovementSetting.handleSave 의 mutation 직전에 OVER_GUARD 를 두어 인접 라인을 0 길이로 흡수하는 평행 위치까지 deltaX/deltaY 를 잘라 적용한다. 사용자가 입력한 큰 값도 평행 한계까지는 그대로 반영되고, 그 이상은 흡수 위치에서 멈춘다. UI 알림은 띄우지 않고 콘솔 경고만. type: feedback --- **현행 정책 (2026-04-24, 최종):** 사용자가 입력한 이동량이 인접 라인을 뒤집어 폴리곤 self-cross 를 일으키는 수준이면, **mutation 을 거부하지 않고 평행 위치(인접 라인이 정확히 0 길이가 되는 한계)까지로 클램핑** 한다. wall.baseLines 는 절대 오버 좌표로 갱신되지 않으므로 SK 빌드도 항상 정상 가정 하에 동작. **정책 전환 flag (2026-04-24 추가):** - 위치: `src/hooks/roofcover/useMovementSetting.js` 파일 상단 `const OVER_MOVE_POLICY = 'clamp'`. - 값: `'clamp'` (기본) / `'reject'` (silent skip) / `'passthrough'` (가드 전면 해제). - 교체 방법: 이 상수 하나만 바꾸면 handleSave 내 OVER_GUARD 분기가 자동 반응. - Why: 사용자가 정책을 과거로 되돌리거나 임시 실험하고 싶을 때 분기 코드 수정 없이 전환. **OVER_GUARD 위치 & 판정:** - 파일: `src/hooks/roofcover/useMovementSetting.js`, handleSave 의 `targetBaseLines.forEach((target) => { ... })` 안. - 위치: deltaX/deltaY 결정 직후, `currentLine.set({...})` 직전. - 클램핑 식 (currentLine 이 수평이면 deltaY, 수직이면 deltaX 가 유일 성분): ``` raw = isHoriz ? deltaY : deltaX sgn = Math.sign(raw) nLimit = isHoriz ? (nextLine.y2 - currentLine.y2) : (nextLine.x2 - currentLine.x2) pLimit = isHoriz ? (prevLine.y1 - currentLine.y1) : (prevLine.x1 - currentLine.x1) // sgn 과 같은 부호 한계만 후보 (반대 부호는 인접 라인이 길어지는 방향이므로 오버 아님) absLimit = min of |nLimit|, |pLimit| (sgn 일치하는 것만) // [EPS 필수] 정확한 평행은 인접 baseLine zero-length → polygon 중복 꼭짓점 → SK 빌더가 헛선 추가. // OVER_EPS 만큼 빼서 인접 baseLine 이 0.5 길이로 남게 → 기존 SHOULDER_ABSORBED 가드가 자연 처리. safeAbsLimit = max(0, absLimit - OVER_EPS) // OVER_EPS = 0.5 limited = sgn * min(|raw|, safeAbsLimit) ``` - `|limited| < |raw|` 이면 클램핑 발생 → 콘솔에 `[handleSave] OVER 클램핑: deltaX|deltaY raw→limited (nLimit=... pLimit=..., EPS=0.5)`. - 토스트/모달 알림 없음. **[필수] roof.moveUpDown 동시 클램핑:** - 클램핑 블록 안에서 `roof.moveUpDown = Math.round(Math.abs(__limited) * 10)` 을 같이 해줘야 한다. - 이유: `movingLineFromSkeleton` (skeleton-utils.js) 은 wall.baseLines 가 아니라 `roof.moveUpDown` + `canvas.skeleton.lastPoints`(또는 `orgRoofPoints`) 로부터 **roof.points 확장량**을 계산한다. wall.baseLines delta 만 줄이고 `roof.moveUpDown` 을 방치하면: - wall.baseLines 는 평행 위치까지만 이동 (클램핑 됨) - roof.points 는 원본 입력값(예: 1525) 기반 확장 적용 - 두 좌표계 충돌 → polygon 자기교차 → `removeNonOrthogonalPoints` 가 꼭짓점 축소 → "SK 가 wall.baseLines 안에서 멈추고 확장 안 됨" 증상. - 단위: `__limited` 는 cm (value 가 mm→cm 변환된 뒤 delta 로 쓰임). `roof.moveUpDown` 은 mm (movingLineFromSkeleton 내부에서 `Big(moveUpDown).div(10)` 으로 cm 환산). 그래서 `*10`. **Why:** - 거부(reject)로 시도했더니 사용자가 평행 위치까지 정확히 가는 정상 케이스에서도 wall 이 안 움직인다고 함 → 거부 정책 폐기. - 클램핑은 사용자 의도에 가장 근접: "큰 값을 입력해도 알아서 평행까지 멈춘다" 는 UX. 사용자 입장에서 다시 정확한 값을 계산할 필요 없음. - 사후 처리(silent skip / 오버 좌표로 SK 재빌드 / 오버 전용 함수) 는 invalid geometry 를 시각화하는 한계 때문에 매번 추가 요구가 발생. - 사전 클램핑으로 wall.baseLines invariant(인접 라인 방향 유지)를 강제하면 하류(SK 빌드, innerLines 생성, lastPoints) 모두 정상 가정 하에 동작. **가정:** - currentLine 은 수평(y1==y2) 또는 수직(x1==x2). 직각 polygon 가정. - nextLine/prevLine 은 currentLine 과 직각이 아니어도 동작은 하지만, 한계 식의 정밀성은 직각일 때 최적. - 대각선 라인 polygon 에서 정밀도 이슈 발생 시 한계 식을 일반 벡터 투영으로 확장 필요. **How to apply:** - handleSave 외 다른 wall.baseLines mutation 경로(드래그 이동 등)에도 동일 OVER_GUARD 추가가 일관됨. 현재는 handleSave 에만 적용. - OVER_GUARD 통과 후 평행이 정확히 일치한 인접 라인은 zero-length baseLine 이 됨 → 기존 `SHOULDER_ABSORBED` 가드(planeSize < 1)로 SK 빌드에서 자연 처리. **Dead code 보존:** - 이전 정책으로 추가된 아래 코드는 남겨둔다 (안전망 + 다른 mutation 경로 추가 가능성): - `skeleton-utils.js: drawSkeletonRidgeRoofFromBaseLines` 함수 (오버 전용 SK 빌드). - `QPolygon.drawHelpLine` 의 verdict==='crossed' → `drawSkeletonRidgeRoofFromBaseLines` 분기. - `skeleton-utils.js: drawSkeletonRidgeRoof` 내부 crossed 경고 로그. - `redrawHelpLineForced` 래퍼. - 현행 정책에서는 OVER_GUARD 가 막아주므로 위 분기들은 호출되지 않음. 사용자가 명시적으로 제거 요청할 때까지 보존. **규칙 교체 이력:** - 2026-04-22: "리셋 없이 조용히 스킵" (silent skip). Why: 사용자 추가 라인 보존, UI 깜빡임 방지. - 2026-04-24 (1차): "crossed 가드 제거 + 오버 좌표 그대로 일반 경로 통과". 결과: 일반 경로 확장/클램핑이 좌표 왜곡. - 2026-04-24 (2차): "crossed 시 오버 전용 함수로 분기". 결과: SK 가 roofLine 바깥으로 삐져나감 → 추가 클립 요구. - 2026-04-24 (3차): "OVER_GUARD 로 mutation 거부". 결과: 평행 위치 정확히 도달하는 정상 케이스도 거부됨 → 폐기. - 2026-04-24 (4차): "OVER_GUARD 로 평행 위치 정확히 도달하게 클램핑". 결과: 정확한 평행이 polygon 중복 꼭짓점을 만들어 SK 빌더가 헛선을 추가. 이전 평행 이동이 멀쩡했던 이유는 fabric floating point 노이즈 덕이었음. - 2026-04-24 (최종 1차): **"OVER_GUARD 클램핑 + OVER_EPS=0.5 여유"**. 평행보다 0.5 모자란 위치까지 이동 → 인접 baseLine 이 0.5 길이로 남고 SHOULDER_ABSORBED(planeSize<1) 가드가 자연 처리. 사용자 체감은 거의 정확한 평행. - 2026-04-24 (최종 2차 버그 fix): 위 상태에서 "SK 가 확장을 안하고 wall.baseLines 안에서 멈춤" 증상 발생. 원인: `roof.moveUpDown` 을 원본 입력값(parseInt) 그대로 두어 movingLineFromSkeleton 이 확장을 원본 크기로 시도 → polygon 자기교차. fix: 클램핑 블록 안에서 `roof.moveUpDown = Math.round(Math.abs(__limited) * 10)` 동시 갱신.