From b8db9355181afb46b86d7b45f670c2a747dfefac Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 23 Apr 2026 12:24:21 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B3=B4=EC=A1=B0=EB=9D=BC=EC=9D=B8=20?= =?UTF-8?q?=EB=A7=9E=EC=B6=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/skeleton-utils.js | 245 +++++++++++++++++++++++++++++-------- 1 file changed, 194 insertions(+), 51 deletions(-) diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index 56606bde..a882f630 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -72,6 +72,13 @@ const movingLineFromSkeleton = (roofId, canvas) => { const endPoint = selectLine.endPoint const orgRoofPoints = roof.points; // orgPoint를 orgPoints로 변경 const oldPoints = canvas?.skeleton.lastPoints ?? orgRoofPoints // 여기도 변경 + // [LP-TRACE] movingLineFromSkeleton 진입 시점 oldPoints[7] 값 — lastPoints 유입값 확인용 + try { + const _lp = canvas?.skeleton?.lastPoints + const _src = _lp ? 'lastPoints' : 'orgRoofPoints' + const _p7 = oldPoints?.[7] + console.log(`[LP-TRACE] movingLineFromSkeleton.in src=${_src} oldPoints[7]=(${_p7?.x?.toFixed(1)},${_p7?.y?.toFixed(1)}) pos=${movePosition} dir=${moveDirection}`) + } catch (_e) {} const oppositeLine = findOppositeLine(canvas.skeleton.Edges, startPoint, endPoint, oldPoints); const wall = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.WALL && obj.attributes.roofId === roofId) @@ -419,6 +426,11 @@ const buildRawMovedPoints = (roofId, canvas) => { const src = (prevLast && prevLast[i]) ? prevLast[i] : orgRoofPoints[i] oldPoints.push({ x: src.x, y: src.y }) } + // [LP-TRACE] buildRawMovedPoints 진입 — prevLast 유무 및 [7] 현재 값 + try { + const _pl7 = prevLast?.[7] + console.log(`[LP-TRACE] buildRaw.in prevLast=${prevLast ? 'Y' : 'N'} prevLast[7]=(${_pl7?.x?.toFixed(1)},${_pl7?.y?.toFixed(1)}) oldPoints[7]=(${oldPoints[7]?.x?.toFixed(1)},${oldPoints[7]?.y?.toFixed(1)})`) + } catch (_e) {} const selectLine = roof.moveSelectLine if (!selectLine) return oldPoints @@ -465,6 +477,14 @@ const buildRawMovedPoints = (roofId, canvas) => { }) }) + // [LP-TRACE] buildRawMovedPoints 결과 — matchingLines 수와 결과 [7] 값 + try { + const _sel = selectLine + const _sp = _sel?.startPoint + const _ep = _sel?.endPoint + console.log(`[LP-TRACE] buildRaw.out matchingLines=${matchingLines.length} selSP=(${_sp?.x?.toFixed(1)},${_sp?.y?.toFixed(1)}) selEP=(${_ep?.x?.toFixed(1)},${_ep?.y?.toFixed(1)}) bp7=(${basePoints[7]?.x?.toFixed(1)},${basePoints[7]?.y?.toFixed(1)}) newPoints[7]=(${newPoints[7]?.x?.toFixed(1)},${newPoints[7]?.y?.toFixed(1)})`) + } catch (_e) {} + return newPoints } catch (e) { console.warn('[buildRawMovedPoints] 실패 → null fallback:', e) @@ -495,26 +515,44 @@ const buildRawMovedPoints = (roofId, canvas) => { export const verifyMoveBoundary = (roofId, canvas) => { try { const roof = canvas?.getObjects().find((o) => o.id === roofId) - if (!roof || !Array.isArray(roof.points)) return 'unknown' + if (!roof || !Array.isArray(roof.points)) { + console.log('[verifyMoveBoundary] roof/points 없음 → unknown') + return 'unknown' + } const moveFlowLine = roof.moveFlowLine ?? 0 const moveUpDown = roof.moveUpDown ?? 0 - if (moveFlowLine === 0 && moveUpDown === 0) return 'ok' + const position = roof.movePosition + const direction = roof.moveDirect + console.log( + `[verifyMoveBoundary] 진입 position=${position} direction=${direction} moveUpDown=${moveUpDown} moveFlowLine=${moveFlowLine}` + ) + if (moveFlowLine === 0 && moveUpDown === 0) { + console.log('[verifyMoveBoundary] 이동 없음 → ok') + return 'ok' + } const proposed = buildRawMovedPoints(roofId, canvas) const orig = roof.points - if (!Array.isArray(proposed) || proposed.length !== orig.length) return 'unknown' + if (!Array.isArray(proposed) || proposed.length !== orig.length) { + console.log( + `[verifyMoveBoundary] proposed 비정상 (len=${proposed?.length}, orig.len=${orig.length}) → unknown` + ) + return 'unknown' + } const n = orig.length if (n < 3) return 'unknown' const posTol = 0.5 let worst = 'ok' + const movedIdx = [] for (let i = 0; i < n; i++) { const moved = Math.abs(proposed[i].x - orig[i].x) > posTol || Math.abs(proposed[i].y - orig[i].y) > posTol if (!moved) continue + movedIdx.push(i) const prev = (i - 1 + n) % n const next = (i + 1) % n @@ -522,6 +560,10 @@ export const verifyMoveBoundary = (roofId, canvas) => { const crossOrig = getTurnDirection(orig[prev], orig[i], orig[next]) const crossNew = getTurnDirection(proposed[prev], proposed[i], proposed[next]) + console.log( + `[verifyMoveBoundary] [${i}] orig=(${Math.round(orig[i].x)},${Math.round(orig[i].y)}) → proposed=(${Math.round(proposed[i].x)},${Math.round(proposed[i].y)}) crossOrig=${crossOrig.toFixed(1)} crossNew=${crossNew.toFixed(1)}` + ) + // 원래 골짜기(>0) 였던 꼭짓점만 경계 판정 대상 if (crossOrig > 0) { // 상대 허용오차: 원본 cross 크기 5% 이내면 "0 근처" 로 간주 (on-boundary) @@ -542,6 +584,10 @@ export const verifyMoveBoundary = (roofId, canvas) => { } } + if (movedIdx.length === 0) { + console.log('[verifyMoveBoundary] 이동된 꼭짓점 0개 (buildRawMovedPoints 매칭 실패 가능성)') + } + console.log(`[verifyMoveBoundary] 최종 판정: ${worst} (moved indices=[${movedIdx.join(',')}])`) return worst } catch (e) { console.warn('[verifyMoveBoundary] 판정 실패 → unknown:', e) @@ -795,31 +841,34 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { // movedPoints를 roof 경계로 확장 // 이번 이동에서 실제로 변경된 포인트만 구분하여, 변경되지 않은 축만 roof 경계로 확장 - const oldPoints = canvas?.skeleton?.lastPoints ?? roofLinePoints - const tolerance = 0.1 - const correctedPoints = movedPoints.map((mp, i) => { - const rp = roofLinePoints[i] - const op = oldPoints[i] - if (!rp || !op) return mp - const xMatch = Math.abs(mp.x - rp.x) < tolerance - const yMatch = Math.abs(mp.y - rp.y) < tolerance - if (xMatch && yMatch) return mp - // 이번 이동에서 실제로 변경된 축 판별 (oldPoints vs movedPoints) - const xMoved = Math.abs(mp.x - op.x) >= tolerance - const yMoved = Math.abs(mp.y - op.y) >= tolerance - let newX = mp.x - let newY = mp.y - // 이번에 이동되지 않았는데 roof 경계와 다른 축만 교정 - if (!xMoved && !xMatch) newX = rp.x - if (!yMoved && !yMatch) newY = rp.y - if (newX !== mp.x || newY !== mp.y) { - console.log(`point[${i}] 교정: mp=(${mp.x}, ${mp.y}) → (${newX}, ${newY}), op=(${op.x}, ${op.y}), xMoved=${xMoved}, yMoved=${yMoved}`) - } - return { x: newX, y: newY } - }) - - // roofLineContactPoints = correctedPoints - // changRoofLinePoints = correctedPoints + /* + * [DEAD CODE — 비활성 보존] + * 의도: "이번 이동에서 바뀌지 않은 축은 roof 경계로 교정" 하려던 블록. + * 비활성화 이유: correctedPoints 결과가 아래 대입(roofLineContactPoints/changRoofLinePoints)에서 + * movedPoints로 대체되어 실사용되지 않음. 또한 누적 이동 상태에서 "이번에 안 움직인 y축"을 + * roof 경계로 되돌려 이전 이동(예: 1차 top)의 y값을 지우는 부작용 위험이 있어 이전 작업자가 + * 대입을 주석 처리한 것으로 보임. 구조/의도 흔적만 남겨 두고 실행은 하지 않는다. + * + * const oldPoints = canvas?.skeleton?.lastPoints ?? roofLinePoints + * const tolerance = 0.1 + * const correctedPoints = movedPoints.map((mp, i) => { + * const rp = roofLinePoints[i] + * const op = oldPoints[i] + * if (!rp || !op) return mp + * const xMatch = Math.abs(mp.x - rp.x) < tolerance + * const yMatch = Math.abs(mp.y - rp.y) < tolerance + * if (xMatch && yMatch) return mp + * const xMoved = Math.abs(mp.x - op.x) >= tolerance + * const yMoved = Math.abs(mp.y - op.y) >= tolerance + * let newX = mp.x + * let newY = mp.y + * if (!xMoved && !xMatch) newX = rp.x + * if (!yMoved && !yMatch) newY = rp.y + * return { x: newX, y: newY } + * }) + * roofLineContactPoints = correctedPoints + * changRoofLinePoints = correctedPoints + */ roofLineContactPoints = movedPoints changRoofLinePoints = movedPoints @@ -959,13 +1008,26 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { roofId: roofId, // Add other necessary top-level properties } + // [누적상태 보존] + // canvas.skeleton = cleanSkeleton 으로 교체하면 기존 lastPoints 가 사라져, + // 직후 호출되는 buildRawMovedPoints 의 prevLast 가 undefined 로 떨어진다. + // 그 결과 save 경로가 매번 "orig + 이번 세션 delta" 만 기록하여 + // 이전 세션들의 누적 이동(y-shift 등)이 drop → 3~4차 이동에서 SK 붕괴. + // 교체 전에 lastPoints 를 캡처해 새 skeleton 에 이어붙여 누적을 유지한다. + const __preservedLastPoints = canvas.skeleton?.lastPoints ?? null canvas.skeleton = [] canvas.skeleton = cleanSkeleton + if (__preservedLastPoints) canvas.skeleton.lastPoints = __preservedLastPoints // lastPoints 는 축소되지 않은 풀 길이(roof.points.length) 버전으로 저장해야 // 다음 이동 시 인덱스 기반 매칭이 유지됨. raw 재구성 실패 시에만 기존 값 사용. const rawMovedFull = buildRawMovedPoints(roofId, canvas) if (rawMovedFull && rawMovedFull.length === (roof.points?.length ?? 0)) { canvas.skeleton.lastPoints = rawMovedFull + // [LP-TRACE] 실제 저장되는 lastPoints[7] — 이 값이 다음 이동의 prevLast[7] + try { + const _p7 = rawMovedFull[7] + console.log(`[LP-TRACE] lastPoints.save [7]=(${_p7?.x?.toFixed(1)},${_p7?.y?.toFixed(1)}) len=${rawMovedFull.length}`) + } catch (_e) {} console.log('[skeleton] lastPoints 저장: raw 풀 길이', rawMovedFull.length, '점') } else { canvas.skeleton.lastPoints = roofLineContactPoints @@ -1226,9 +1288,36 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver // 각 라인 집합 정렬 const sortWallLines = ensureCounterClockwiseLines(wallLines) - const sortWallBaseLines = ensureCounterClockwiseLines(wall.baseLines) + // wall.baseLines를 독립적으로 CCW 정렬하면 ㅗ→붕괴 등으로 꼭짓점 집합이 달라질 때 + // 시작점(min-Y-then-min-X)이 wallLines와 어긋나 배열이 shift된다. + // wallLines[i]와 wall.baseLines[i]는 초기 생성 시부터 raw-index 1:1 페어링이 유지되므로, + // sortWallLines의 CCW 순서에 대응하는 원 인덱스를 복원하여 같은 순서로 wall.baseLines를 매핑한다. + 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) => { + 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 ===', + // 'moveUpDown=', roof.moveUpDown, + // 'moveFlowLine=', roof.moveFlowLine) + // sortWallBaseLines.forEach((bl, i) => { + // console.log(`[MOVE-TRACE] sortWallBaseLines[${i}]`, + // `(${Math.round(bl?.x1)},${Math.round(bl?.y1)})→(${Math.round(bl?.x2)},${Math.round(bl?.y2)})`, + // 'vs sortWallLines', + // `(${Math.round(sortWallLines[i]?.x1)},${Math.round(sortWallLines[i]?.y1)})→(${Math.round(sortWallLines[i]?.x2)},${Math.round(sortWallLines[i]?.y2)})`, + // 'vs sortRoofLines', + // `(${Math.round(sortRoofLines[i]?.x1)},${Math.round(sortRoofLines[i]?.y1)})→(${Math.round(sortRoofLines[i]?.x2)},${Math.round(sortRoofLines[i]?.y2)})`) + // }) + // ===== 공통 헬퍼 함수 ===== // axis config: vertical(left/right) → moveAxis='x', lineAxis='y' // horizontal(top/bottom) → moveAxis='y', lineAxis='x' @@ -1248,6 +1337,19 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver console.log(`${condition}::::isStartEnd:::::`) + // [MOVE-TRACE] index 0 전용 (필요 시 주석 해제) + // if (index === 0) { + // console.log('[MOVE-TRACE] processInStartEnd idx=0', + // 'condition=', condition, + // 'isStart=', isStart, + // 'inSign=', inSign, + // 'moveAxis=', moveAxis, + // 'lineAxis=', lineAxis, + // 'wallLine=', `(${Math.round(wallLine.x1)},${Math.round(wallLine.y1)})→(${Math.round(wallLine.x2)},${Math.round(wallLine.y2)})`, + // 'wallBaseLine=', `(${Math.round(wallBaseLine.x1)},${Math.round(wallBaseLine.y1)})→(${Math.round(wallBaseLine.x2)},${Math.round(wallBaseLine.y2)})`, + // 'roofLine=', `(${Math.round(roofLine.x1)},${Math.round(roofLine.y1)})→(${Math.round(roofLine.x2)},${Math.round(roofLine.y2)})`) + // } + // 고정 끝점 설정 if (isStart) { newPEnd[lineAxis] = roofLine[`${lineAxis}2`] @@ -1257,7 +1359,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver newPStart[moveAxis] = roofLine[`${moveAxis}1`] } - const moveDist = Big(wallBaseLine[m]).minus(wallLine[m]).toNumber() + const moveDist = Big(wallBaseLine[m]).minus(wallLine[m]).abs().toNumber() const ePoint = { [moveAxis]: wallBaseLine[m], [lineAxis]: wallBaseLine[l] } if (isStart) { newPStart[lineAxis] = wallBaseLine[l] @@ -1267,7 +1369,9 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver findPoints.push({ ...ePoint, position: `${condition}_${isStart ? 'start' : 'end'}` }) - let newPointM = Big(roofLine[`${moveAxis}1`]).plus(moveDist).toNumber() + let newPointM = Big(roofLine[`${moveAxis}1`]) + [inSign > 0 ? 'plus' : 'minus'](moveDist) + .toNumber() const pLineL = roofLine[l] const prevIndex = (index - 1 + sortRoofLines.length) % sortRoofLines.length @@ -1423,6 +1527,18 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver const roofLine = sortRoofLines[index] const wallBaseLine = sortWallBaseLines[index] + // [진단] sortWallLines ↔ sortWallBaseLines 페어링 검증 + // collapse 시 ensureCounterClockwiseLines 시작점이 달라지면 인덱스가 어긋남 + // 정상: 같은 물리 벽 → 적어도 한쪽 끝점 공유 또는 방향 동일(수직/수평) + { + const wlDir = Math.abs(wallLine.x2 - wallLine.x1) < 0.5 ? 'V' : (Math.abs(wallLine.y2 - wallLine.y1) < 0.5 ? 'H' : 'D') + const wbDir = Math.abs(wallBaseLine.x2 - wallBaseLine.x1) < 0.5 ? 'V' : (Math.abs(wallBaseLine.y2 - wallBaseLine.y1) < 0.5 ? 'H' : 'D') + const wallIdWl = wallLine.attributes?.wallId ?? wallLine.attributes?.idx ?? '?' + const wallIdWb = wallBaseLine.attributes?.wallId ?? wallBaseLine.attributes?.idx ?? '?' + const dirMatch = wlDir === wbDir + console.log(`🔗 [pair#${index}] wallId wl=${wallIdWl} wb=${wallIdWb} ${dirMatch ? '' : '⚠️DIR_MISMATCH '}wlDir=${wlDir} wbDir=${wbDir} wl=(${Math.round(wallLine.x1)},${Math.round(wallLine.y1)})→(${Math.round(wallLine.x2)},${Math.round(wallLine.y2)}) wb=(${Math.round(wallBaseLine.x1)},${Math.round(wallBaseLine.y1)})→(${Math.round(wallBaseLine.x2)},${Math.round(wallBaseLine.y2)})`) + } + //roofline 외곽선 설정 // console.log('index::::', index) @@ -1469,6 +1585,17 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver const getAddLine = (p1, p2, stroke = '', lineType = 'eaveHelpLine') => { movedLines.push({ index, p1, p2 }) + // [진단] eaveHelpLine 생성 추적: 어느 index/어느 wallBaseLine-wallLine 쌍에서 만들어지는지 + console.log('🎯 [getAddLine]', { + index, + lineType, + p1: { x: Math.round(p1.x), y: Math.round(p1.y) }, + p2: { x: Math.round(p2.x), y: Math.round(p2.y) }, + wallLine: `(${Math.round(wallLine.x1)},${Math.round(wallLine.y1)})→(${Math.round(wallLine.x2)},${Math.round(wallLine.y2)})`, + wallBaseLine: `(${Math.round(wallBaseLine.x1)},${Math.round(wallBaseLine.y1)})→(${Math.round(wallBaseLine.x2)},${Math.round(wallBaseLine.y2)})`, + wallBaseLen: Math.round(Math.hypot(wallBaseLine.x2 - wallBaseLine.x1, wallBaseLine.y2 - wallBaseLine.y1)), + }) + const dx = Math.abs(p2.x - p1.x); const dy = Math.abs(p2.y - p1.y); const isDiagonal = dx > 0.5 && dy > 0.5; // x, y 변화가 모두 있으면 대각선 @@ -1530,6 +1657,16 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver const mLine = getSelectLinePosition(wall, wallBaseLine) + // [진단] 왜 bottom/top/left/right로 잡혔는지 추적 + // - wallBaseLine이 실제로는 어떤 방향인지, midpoint, 위/아래 안팎 결과 확인 + { + const midX = (wallBaseLine.x1 + wallBaseLine.x2) / 2 + const midY = (wallBaseLine.y1 + wallBaseLine.y2) / 2 + const isH = Math.abs(wallBaseLine.y1 - wallBaseLine.y2) < 0.5 + const isV = Math.abs(wallBaseLine.x1 - wallBaseLine.x2) < 0.5 + console.log(`🧭 [getSelectLinePosition 결과] idx=${index} position=${mLine.position} orient=${mLine.orientation} mid=(${Math.round(midX)},${Math.round(midY)}) isH=${isH} isV=${isV} wallLine=(${Math.round(wallLine.x1)},${Math.round(wallLine.y1)})→(${Math.round(wallLine.x2)},${Math.round(wallLine.y2)}) wallBaseLine=(${Math.round(wallBaseLine.x1)},${Math.round(wallBaseLine.y1)})→(${Math.round(wallBaseLine.x2)},${Math.round(wallBaseLine.y2)})`) + } + if (getOrientation(roofLine) === 'vertical') { if (['left', 'right'].includes(mLine.position)) { if (Math.abs(wallLine.x1 - wallBaseLine.x1) < 0.1 || Math.abs(wallLine.x2 - wallBaseLine.x2) < 0.1) { @@ -3682,22 +3819,22 @@ function getOrderedBasePoints(baseLines) { function createOrderedBasePoints(roofPoints, baseLines) { const basePoints = []; - // baseLines에서 연결된 순서대로 점들을 추출 - const orderedBasePoints = getOrderedBasePoints(baseLines); - - // roofPoints의 개수와 맞추기 - if (orderedBasePoints.length >= roofPoints.length) { - return orderedBasePoints.slice(0, roofPoints.length); - } - - // 부족한 경우 roofPoints 기반으로 보완 - roofPoints.forEach((roofPoint, index) => { - if (index < orderedBasePoints.length) { - basePoints.push(orderedBasePoints[index]); + // wall 생성 시 baseLines[i]는 polygon edge i (roofPoints[i]→roofPoints[i+1])에 대응해 push되므로 + // baseLines[i].startPoint 는 roofPoints[i]의 wall 좌표이다(= 인덱스 직접 정합). + // + // 기존 구현은 getOrderedBasePoints(graph traversal)을 사용해 baseLines[0] 시작점과 + // 체인 isSamePoint 성공에 결과 순서가 의존했고, 평행 이동으로 좌표가 mutate되면 + // 체인이 어긋나 roof.points 인덱스와 정합이 깨져 엉뚱한 꼭짓점에 이동이 적용되는 + // SK 붕괴 원인이 됐다. 인덱스 직접 매핑으로 불변식을 확보한다. + for (let i = 0; i < roofPoints.length; i++) { + const bl = baseLines[i]; + if (bl && bl.startPoint) { + basePoints.push({ ...bl.startPoint }); } else { - basePoints.push({ ...roofPoint }); // fallback + // baseLines 수가 부족한 예외 상황에만 roof 좌표로 fallback + basePoints.push({ ...roofPoints[i] }); } - }); + } return basePoints; } @@ -4122,14 +4259,20 @@ function isPointOnLineSegment(point, lineStart, lineEnd, tolerance = 0.1) { */ function updateAndAddLine(innerLines, targetPoint) { - // 1. Find the line containing the target point (HIP 라인 제외 - HIP 위의 점이 잘못 매칭되는 것 방지) + // 1. Find the line containing the target point. + // 우선 non-HIP에서 찾고, 없을 때만 HIP 포함 fallback — target이 HIP 꼭짓점에 정확히 + // 놓이는 케이스(wallBaseLine 꼭짓점이 HIP 위)를 복구하기 위함. non-HIP 우선 순서로 + // 과거 "HIP 잘못 매칭" 회피 의도도 보존. const nonHipLines = innerLines.filter(line => line.attributes?.type !== LINE_TYPE.SUBLINE.HIP) - const foundLine = findLineContainingPoint(nonHipLines, targetPoint); - console.log(`[updateAndAddLine] position=${targetPoint.position} point=(${targetPoint.x},${targetPoint.y})`, - foundLine ? `foundLine: (${foundLine.x1},${foundLine.y1})→(${foundLine.x2},${foundLine.y2}) name=${foundLine.name||foundLine.lineName||'?'} type=${foundLine.attributes?.type||'?'}` : 'NOT FOUND', - `innerLines count=${innerLines.length}, nonHip=${nonHipLines.length}`) + let foundLine = findLineContainingPoint(nonHipLines, targetPoint); if (!foundLine) { - console.warn('No line found containing the target point'); + foundLine = findLineContainingPoint(innerLines, targetPoint); + if (foundLine) { + console.log(`[MOVE-TRACE] HIP fallback matched: position=${targetPoint.position} target=(${targetPoint.x.toFixed(2)},${targetPoint.y.toFixed(2)}) → (${foundLine.x1.toFixed(1)},${foundLine.y1.toFixed(1)})→(${foundLine.x2.toFixed(1)},${foundLine.y2.toFixed(1)}) type=${foundLine.attributes?.type||'?'}`) + } + } + if (!foundLine) { + console.warn(`[updateAndAddLine] NOT FOUND position=${targetPoint.position} point=(${targetPoint.x.toFixed(2)},${targetPoint.y.toFixed(2)})`); return [...innerLines]; }