From d810b89658535c74cd342a8cacb229daadc5a138 Mon Sep 17 00:00:00 2001 From: ysCha Date: Mon, 27 Apr 2026 13:09:28 +0900 Subject: [PATCH] =?UTF-8?q?=EB=9D=BC=EC=9D=B8=EC=A4=84=ED=91=9C=EC=8B=9C,?= =?UTF-8?q?=20=EC=9D=B8=EB=8D=B1=EC=8A=A4=20=EC=9E=AC=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/fabric/QPolygon.js | 4 ++++ src/util/skeleton-utils.js | 32 ++++++++++++++++++------------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index c3b330c2..3cdd644b 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -7,6 +7,7 @@ import * as turf from '@turf/turf' import { LINE_TYPE, POLYGON_TYPE } from '@/common/common' import Big from 'big.js' import { drawSkeletonRidgeRoof, drawSkeletonRidgeRoofFromBaseLines, verifyMoveBoundary } from '@/util/skeleton-utils' +import { attachDebugLabels } from '@/util/debugLabels' export const QPolygon = fabric.util.createClass(fabric.Polygon, { type: 'QPolygon', @@ -514,6 +515,9 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { // console.log('변별로 설정') drawRoofByAttribute(this.id, this.canvas, textMode) } + + // [DEBUG] URL ?debug=labels 일 때만 라벨 부착. 운영 빌드/평소엔 noop. + attachDebugLabels(this.canvas, this.id) }, /** diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index fdf4ddb7..958eb53e 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -1339,24 +1339,29 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver }) } - // 각 라인 집합 정렬 - const sortWallLines = ensureCounterClockwiseLines(wallLines) - // wall.baseLines를 독립적으로 CCW 정렬하면 ㅗ→붕괴 등으로 꼭짓점 집합이 달라질 때 - // 시작점(min-Y-then-min-X)이 wallLines와 어긋나 배열이 shift된다. - // wallLines[i]와 wall.baseLines[i]는 초기 생성 시부터 raw-index 1:1 페어링이 유지되므로, - // sortWallLines의 CCW 순서에 대응하는 원 인덱스를 복원하여 같은 순서로 wall.baseLines를 매핑한다. + // [정렬 정책 — 인덱스가 생명] (2026-04-27) + // 1) master = roofLines. roofLine 은 외곽(처마) 경계라 사용자 mutation 영향 없음 → 시작점 안정. + // 2) ensureCounterClockwiseLines 가 master 의 leftTop(min-Y → min-X) 꼭짓점에서 시작해 CCW 정렬. + // 3) wall.lines, wall.baseLines 는 raw 인덱스 1:1 페어링 유지된다는 전제로 + // master 의 sorted 순서 → 원 인덱스 → wall.lines[idx] / wall.baseLines[idx] 매핑. + // wall 쪽을 master 로 쓰던 구현은 OVER 흡수/usePropertiesSetting 등에서 wall.lines 가 + // 재할당되며 시작점이 어긋나 페어 인덱스가 회전되던 문제(2026-04-27 ㅗ 좌측오버 케이스) 가 있어 변경. const _keyOfEdge = (l) => { const a = `${Math.round(l.x1 * 10) / 10},${Math.round(l.y1 * 10) / 10}` const b = `${Math.round(l.x2 * 10) / 10},${Math.round(l.y2 * 10) / 10}` return a < b ? `${a}|${b}` : `${b}|${a}` } const _rawIdxByKey = new Map() - wallLines.forEach((l, i) => _rawIdxByKey.set(_keyOfEdge(l), i)) - const sortWallBaseLines = sortWallLines.map((sl) => { + roofLines.forEach((l, i) => _rawIdxByKey.set(_keyOfEdge(l), i)) + const sortRoofLines = ensureCounterClockwiseLines(roofLines) + const sortWallLines = sortRoofLines.map((sl) => { + const origIdx = _rawIdxByKey.get(_keyOfEdge(sl)) + return origIdx != null && wallLines[origIdx] ? wallLines[origIdx] : sl + }) + const sortWallBaseLines = sortRoofLines.map((sl) => { const origIdx = _rawIdxByKey.get(_keyOfEdge(sl)) return origIdx != null && wall.baseLines[origIdx] ? wall.baseLines[origIdx] : sl }) - const sortRoofLines = ensureCounterClockwiseLines(roofLines) // [MOVE-TRACE] 진입 스냅샷 (필요 시 주석 해제) // console.log('[MOVE-TRACE] === getMoveUpDownLine entry ===', @@ -1596,10 +1601,11 @@ 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) { + // [SHOULDER-ABSORBED] wall.baseLines[k] 가 이동 흡수/OVER_EPS 클램핑으로 거의 zero-length 이면 + // 해당 edge 는 흡수된 상태. pair 생성 자체가 불필요(DIR_MISMATCH + eaveHelpLine 노이즈 유발). + // 임계 = 10: useMovementSetting OVER_EPS=0.5 (canvas unit) 시 잔여 길이=0.5 → planeSize=5 (calcLinePlaneSize × 10). + // 안전마진 두고 < 10 으로 차단. 정상 baseLine 은 보통 50+ 이므로 false positive 없음. + if (!wallBaseLine || (wallBaseLine.attributes?.planeSize ?? Infinity) < 10) { 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 }