[1806]서까래 높이 조정에 따른 지붕 형상 불일치 - 라인오버체크2 #792

Merged
ysCha merged 1 commits from dev into dev-deploy 2026-04-23 15:25:46 +09:00

View File

@ -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 시작점이 달라지면 인덱스가 어긋남
// 정상: 같은 물리 벽 → 적어도 한쪽 끝점 공유 또는 방향 동일(수직/수평)