qcast-front/.claude/memory/project_parallel_collapse_fix.md

5.3 KiB
Raw Blame History

name description type
평행 이동 SK 붕괴 근본 원인 및 수정 (2차) 평행 이동 시 roof.basePoints[index] 좌표 매칭이 wall 오프셋 붕괴(degenerate baseLines)·좌표 우연 일치에 취약해 엉뚱한 꼭짓점에 dy 적용 → SK 축직교 깨짐 → SkeletonBuilder 보조선 난립/붕괴. movingLineFromSkeleton 평행 분기와 buildRawMovedPoints를 인덱스 직접 매핑 + Set 합집합으로 전환. 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.jscorrectedPoints 교정 블록(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 로그로 확인.