From 01b9625a1357b98d87f99d4aab7c1713a481e350 Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 23 Apr 2026 15:24:21 +0900 Subject: [PATCH] =?UTF-8?q?[1806]=EC=84=9C=EA=B9=8C=EB=9E=98=20=EB=86=92?= =?UTF-8?q?=EC=9D=B4=20=EC=A1=B0=EC=A0=95=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?=EC=A7=80=EB=B6=95=20=ED=98=95=EC=83=81=20=EB=B6=88=EC=9D=BC?= =?UTF-8?q?=EC=B9=98=20-=20=EB=9D=BC=EC=9D=B8=EC=98=A4=EB=B2=84=EC=B2=B4?= =?UTF-8?q?=ED=81=AC2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/skeleton-utils.js | 219 +++++++++++++++++-------------------- 1 file changed, 100 insertions(+), 119 deletions(-) diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index a882f630..88096345 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -205,104 +205,55 @@ const movingLineFromSkeleton = (roofId, canvas) => { let newPoints = oldPoints.map(point => ({...point})); // selectLine과 일치하는 baseLines 찾기 + // tolerance=0.5 사용 이유: selectLine 은 UI 클릭 시점 좌표(마우스 hover 수치와 + // 클릭 시점 값 사이에 미세 차이 발생), wall.baseLines 는 Big.js 누적 연산 결과. + // 2자리 소수 입력 + /10 + offset 연산 + wall 재생성 누적으로 실측 drift 상한 ~0.3~0.4. + // 기본 0.1 은 빡빡해 filter 0개 매칭으로 dy 미적용 → "못 미침" 발생. + // 0.5 는 드리프트 상한 여유 포용 + 이웃 edge 오매칭 위험 사실상 0 의 균형점. const matchingLines = baseLines .map((line, index) => ({ ...line, findIndex: index })) .filter(line => - (isSamePoint(line.startPoint, selectLine.startPoint) && - isSamePoint(line.endPoint, selectLine.endPoint)) || - (isSamePoint(line.startPoint, selectLine.endPoint) && - isSamePoint(line.endPoint, selectLine.startPoint)) + (isSamePoint(line.startPoint, selectLine.startPoint, 0.5) && + isSamePoint(line.endPoint, selectLine.endPoint, 0.5)) || + (isSamePoint(line.startPoint, selectLine.endPoint, 0.5) && + isSamePoint(line.endPoint, selectLine.startPoint, 0.5)) ); + // 평행 이동 영향 꼭짓점 인덱스 집합 계산 (index-direct + Set dedup) + // baseLines[k] 는 polygon edge k (roof.points[k] → roof.points[(k+1)%n]) 에 대응하므로 + // 매칭된 baseLine 의 인덱스 k 가 곧 이동 영향을 받는 두 꼭짓점 [k, (k+1)%n] 을 결정한다. + // 기존 구현의 좌표 기반 매칭(roof.basePoints[index] vs originalStart/End)은 wall 오프셋 + // 붕괴로 degenerate baseLines 가 생기거나 basePoints[i] 가 선택된 edge 의 꼭짓점과 좌표 + // 우연 일치하는 경우, 의도치 않은 꼭짓점에 dy 가 적용되어 축직교가 깨지고 SkeletonBuilder + // 가 추가 보조선을 생성하며 SK 붕괴로 이어지던 경로였음. + // matchingLines 가 2개 이상인 케이스(보강 라인 중복 push, 좌표 동일 edge 등)에서도 + // Set 으로 합집합 처리해 같은 꼭짓점에 dy 가 2번 적용되는 것을 막는다. + const nPts = newPoints.length; + const affected = new Set(); matchingLines.forEach(line => { - const originalStartPoint = line.startPoint; - const originalEndPoint = line.endPoint; - const offset = line.attributes.offset - // 새로운 좌표 계산 - let newStartPoint = {...originalStartPoint}; - let newEndPoint = {...originalEndPoint} -// 원본 라인 업데이트 - // newPoints 배열에서 일치하는 포인트들을 찾아서 업데이트 - console.log('absMove::', moveUpDownLength); - newPoints.forEach((point, index) => { - if(position === 'bottom'){ - if (moveDirection === 'in') { - if(isSamePoint(roof.basePoints[index], originalStartPoint) || isSamePoint(roof.basePoints[index], originalEndPoint)) { - point.y = Big(point.y).minus(moveUpDownLength).toNumber(); - } - // if (isSamePoint(roof.basePoints[index], originalEndPoint)) { - // point.y = Big(point.y).minus(absMove).toNumber(); - // } - }else if (moveDirection === 'out'){ - if(isSamePoint(roof.basePoints[index], originalStartPoint) || isSamePoint(roof.basePoints[index], originalEndPoint)) { - point.y = Big(point.y).plus(moveUpDownLength).toNumber(); - } - // if (isSamePoint(roof.basePoints[index], originalEndPoint)) { - // point.y = Big(point.y).plus(absMove).toNumber(); - // } - } - - }else if (position === 'top'){ - if(moveDirection === 'in'){ - if(isSamePoint(roof.basePoints[index], originalStartPoint)) { - point.y = Big(point.y).plus(moveUpDownLength).toNumber(); - } - if (isSamePoint(roof.basePoints[index], originalEndPoint)) { - point.y = Big(point.y).plus(moveUpDownLength).toNumber(); - } - }else if(moveDirection === 'out'){ - if(isSamePoint(roof.basePoints[index], originalStartPoint) || isSamePoint(roof.basePoints[index], originalEndPoint)) { - point.y = Big(point.y).minus(moveUpDownLength).toNumber(); - // console.log('roof.basePoints[index]', roof.basePoints[index]) - // console.log('point.x::::', point) - // console.log('originalStartPoint', originalStartPoint) - // console.log('originalEndPoint', originalEndPoint) - } - - } - - }else if(position === 'left'){ - if(moveDirection === 'in'){ - if(isSamePoint(roof.basePoints[index], originalStartPoint) || isSamePoint(roof.basePoints[index], originalEndPoint)) { - point.x = Big(point.x).plus(moveUpDownLength).toNumber(); - } - // if (isSamePoint(roof.basePoints[index], originalEndPoint)) { - // point.x = Big(point.x).plus(absMove).toNumber(); - // } - }else if(moveDirection === 'out'){ - if(isSamePoint(roof.basePoints[index], originalStartPoint) || isSamePoint(roof.basePoints[index], originalEndPoint)) { - point.x = Big(point.x).minus(moveUpDownLength).toNumber(); - } - // if (isSamePoint(roof.basePoints[index], originalEndPoint)) { - // point.x = Big(point.x).minus(absMove).toNumber(); - // } - } - - }else if(position === 'right'){ - if(moveDirection === 'in'){ - if(isSamePoint(roof.basePoints[index], originalStartPoint)) { - point.x = Big(point.x).minus(moveUpDownLength).toNumber(); - } - if (isSamePoint(roof.basePoints[index], originalEndPoint)) { - point.x = Big(point.x).minus(moveUpDownLength).toNumber(); - } - }else if(moveDirection === 'out'){ - if(isSamePoint(roof.basePoints[index], originalStartPoint)) { - point.x = Big(point.x).plus(moveUpDownLength).toNumber(); - } - if (isSamePoint(roof.basePoints[index], originalEndPoint)) { - point.x = Big(point.x).plus(moveUpDownLength).toNumber(); - } - } - - } - - }); - - // 원본 baseLine도 업데이트 - line.startPoint = newStartPoint; - line.endPoint = newEndPoint; + const k = line.findIndex; + if (typeof k !== 'number' || k < 0) return; + affected.add(k); + affected.add((k + 1) % nPts); + }); + console.log('absMove::', moveUpDownLength); + affected.forEach((i) => { + const point = newPoints[i]; + if (!point) return; + if (position === 'bottom') { + if (moveDirection === 'in') point.y = Big(point.y).minus(moveUpDownLength).toNumber(); + else if (moveDirection === 'out') point.y = Big(point.y).plus(moveUpDownLength).toNumber(); + } else if (position === 'top') { + if (moveDirection === 'in') point.y = Big(point.y).plus(moveUpDownLength).toNumber(); + else if (moveDirection === 'out') point.y = Big(point.y).minus(moveUpDownLength).toNumber(); + } else if (position === 'left') { + if (moveDirection === 'in') point.x = Big(point.x).plus(moveUpDownLength).toNumber(); + else if (moveDirection === 'out') point.x = Big(point.x).minus(moveUpDownLength).toNumber(); + } else if (position === 'right') { + if (moveDirection === 'in') point.x = Big(point.x).minus(moveUpDownLength).toNumber(); + else if (moveDirection === 'out') point.x = Big(point.x).plus(moveUpDownLength).toNumber(); + } }); /** @@ -446,35 +397,41 @@ const buildRawMovedPoints = (roofId, canvas) => { const position = roof.movePosition const moveDirection = roof.moveDirect - const matchingLines = baseLines.filter((line) => - (isSamePoint(line.startPoint, selectLine.startPoint) && isSamePoint(line.endPoint, selectLine.endPoint)) || - (isSamePoint(line.startPoint, selectLine.endPoint) && isSamePoint(line.endPoint, selectLine.startPoint)) - ) + // matchingLines 는 LP-TRACE 로그 / 외부 참조용으로 count 유지. + // 실제 dy 적용은 인덱스 직접 매핑 + Set 합집합 (movingLineFromSkeleton 과 동일 규칙)으로 + // baseLines[k] = edge k = roof.points[k] → roof.points[(k+1)%n] + // 좌표 우연 일치 / degenerate baseLines 로부터의 오염을 차단. + // tolerance=0.5 : movingLineFromSkeleton 과 동일. UI 클릭 drift + Big.js 누적 연산 + // drift 포용. 두 경로(save/verify)가 동일 임계값을 사용해야 보정 결과가 일관됨. + const matchingLines = baseLines + .map((line, idx) => ({ line, idx })) + .filter(({ line }) => + (isSamePoint(line.startPoint, selectLine.startPoint, 0.5) && isSamePoint(line.endPoint, selectLine.endPoint, 0.5)) || + (isSamePoint(line.startPoint, selectLine.endPoint, 0.5) && isSamePoint(line.endPoint, selectLine.startPoint, 0.5)) + ) - matchingLines.forEach((line) => { - const s = line.startPoint - const e = line.endPoint - newPoints.forEach((point, index) => { - const bp = basePoints[index] - if (!bp) return - const matchS = isSamePoint(bp, s) - const matchE = isSamePoint(bp, e) - if (!matchS && !matchE) return - - if (position === 'bottom') { - if (moveDirection === 'out') point.y = Big(point.y).plus(moveUpDownLength).toNumber() - else if (moveDirection === 'in') point.y = Big(point.y).minus(moveUpDownLength).toNumber() - } else if (position === 'top') { - if (moveDirection === 'out') point.y = Big(point.y).minus(moveUpDownLength).toNumber() - else if (moveDirection === 'in') point.y = Big(point.y).plus(moveUpDownLength).toNumber() - } else if (position === 'left') { - if (moveDirection === 'out') point.x = Big(point.x).minus(moveUpDownLength).toNumber() - else if (moveDirection === 'in') point.x = Big(point.x).plus(moveUpDownLength).toNumber() - } else if (position === 'right') { - if (moveDirection === 'out') point.x = Big(point.x).plus(moveUpDownLength).toNumber() - else if (moveDirection === 'in') point.x = Big(point.x).minus(moveUpDownLength).toNumber() - } - }) + const nPts = newPoints.length + const affected = new Set() + matchingLines.forEach(({ idx }) => { + affected.add(idx) + affected.add((idx + 1) % nPts) + }) + affected.forEach((i) => { + const point = newPoints[i] + if (!point) return + if (position === 'bottom') { + if (moveDirection === 'out') point.y = Big(point.y).plus(moveUpDownLength).toNumber() + else if (moveDirection === 'in') point.y = Big(point.y).minus(moveUpDownLength).toNumber() + } else if (position === 'top') { + if (moveDirection === 'out') point.y = Big(point.y).minus(moveUpDownLength).toNumber() + else if (moveDirection === 'in') point.y = Big(point.y).plus(moveUpDownLength).toNumber() + } else if (position === 'left') { + if (moveDirection === 'out') point.x = Big(point.x).minus(moveUpDownLength).toNumber() + else if (moveDirection === 'in') point.x = Big(point.x).plus(moveUpDownLength).toNumber() + } else if (position === 'right') { + if (moveDirection === 'out') point.x = Big(point.x).plus(moveUpDownLength).toNumber() + else if (moveDirection === 'in') point.x = Big(point.x).minus(moveUpDownLength).toNumber() + } }) // [LP-TRACE] buildRawMovedPoints 결과 — matchingLines 수와 결과 [7] 값 @@ -1406,6 +1363,22 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver getAddLine, sortRoofLines, findPoints, innerLines, moveAxis, lineAxis, inSign }) => { + // [SHOULDER-ADJACENT] 인접 pair 가 SHOULDER_ABSORBED 된 경우 HIP 빗변은 orphan 이 됨. + // 이유: processInBoth 는 roofLine(원본 roof.points 기반) 꼭짓점을 HIP 끝점으로 씀. + // 인접 pair 흡수 = 해당 꼭짓점이 현재 wallBaseLine 토폴로지에 없음 → 공중에 뜬 대각선. + // SHOULDER_ABSORBED 가드(line 1482 인근)와 같은 원인·패턴의 후속 증상. + { + const n = sortWallBaseLines.length + const prevBL = sortWallBaseLines[(index - 1 + n) % n] + const nextBL = sortWallBaseLines[(index + 1) % n] + const prevAbsorbed = (prevBL?.attributes?.planeSize ?? Infinity) < 1 + const nextAbsorbed = (nextBL?.attributes?.planeSize ?? Infinity) < 1 + if (prevAbsorbed || nextAbsorbed) { + console.log(`⏭️ [pair#${index}] ${condition} HIP_SKIP adj_absorbed prev=${prevAbsorbed} next=${nextAbsorbed}`) + return + } + } + console.log(`${condition}::::isStartEnd (both):::::`) // 원본 기준: moveDistY = abs(roofLine.y - wallBaseLine.y), moveDistX = abs(roofLine.x - wallBaseLine.x) @@ -1527,6 +1500,14 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver const roofLine = sortRoofLines[index] const wallBaseLine = sortWallBaseLines[index] + // [SHOULDER-ABSORBED] wall.baseLines[k] 가 이동 흡수로 zero-length(planeSize≈0)면 + // 해당 edge 는 흡수되어 사라진 상태. pair 생성 자체가 불필요(DIR_MISMATCH + eaveHelpLine 노이즈 유발). + // 참고: 같은 파일 line 666 의 filter(planeSize > 0) 선례와 동일 패턴. + if (!wallBaseLine || (wallBaseLine.attributes?.planeSize ?? Infinity) < 1) { + console.log(`⏭️ [pair#${index}] SHOULDER_ABSORBED skip: wb=(${Math.round(wallBaseLine?.x1)},${Math.round(wallBaseLine?.y1)})→(${Math.round(wallBaseLine?.x2)},${Math.round(wallBaseLine?.y2)}) planeSize=${wallBaseLine?.attributes?.planeSize}`) + return + } + // [진단] sortWallLines ↔ sortWallBaseLines 페어링 검증 // collapse 시 ensureCounterClockwiseLines 시작점이 달라지면 인덱스가 어긋남 // 정상: 같은 물리 벽 → 적어도 한쪽 끝점 공유 또는 방향 동일(수직/수평) -- 2.47.2