sk라인 기록 보존
This commit is contained in:
parent
a3e608f059
commit
3d142cd95c
@ -364,6 +364,264 @@ const movingLineFromSkeleton = (roofId, canvas) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* movingLineFromSkeleton 결과를 안전하게 얻기 위한 래퍼.
|
||||||
|
* - 정상이면 movedPoints 반환
|
||||||
|
* - 골짜기 라인 out 등으로 옆 라인을 넘어가 붕괴/자기교차가 감지되면 null 반환
|
||||||
|
* (호출부에서 null이면 skeletonBuilder 전체를 bail out 시킬 것)
|
||||||
|
*
|
||||||
|
* 기존 movingLineFromSkeleton 로직은 건드리지 않는다.
|
||||||
|
*
|
||||||
|
* @param {string} roofId
|
||||||
|
* @param {fabric.Canvas} canvas
|
||||||
|
* @returns {Array<{x:number,y:number}> | null}
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* canvas.skeleton.lastPoints 에 저장할 "축소되지 않은(raw) 풀 길이(roof.points.length) 8점 버전" 을 생성.
|
||||||
|
*
|
||||||
|
* 문제:
|
||||||
|
* movingLineFromSkeleton 은 마지막에 removeNonOrthogonalPoints 를 거치며 collinear/중복 점을
|
||||||
|
* 제거하므로, 반환 길이가 roof.points.length 보다 작을 수 있다 (예: 8 → 6).
|
||||||
|
* 이 축소된 값을 lastPoints 에 저장하면, 다음 이동 때 인덱스 기반 매칭이
|
||||||
|
* 전부 깨져서 1차 이동 결과가 사라진다.
|
||||||
|
*
|
||||||
|
* 처리:
|
||||||
|
* movingLineFromSkeleton 의 "shift 부분" 만 재현하여 removeNonOrthogonalPoints 생략.
|
||||||
|
* roof.points.length 길이를 정확히 유지한 "원시 이동 결과" 를 리턴.
|
||||||
|
* 실패/미지원 시 null 리턴 → 호출부는 기존 roofLineContactPoints 사용 (기존 동작 보장).
|
||||||
|
*
|
||||||
|
* 기존 movingLineFromSkeleton 은 수정하지 않는다.
|
||||||
|
*
|
||||||
|
* @param {string} roofId
|
||||||
|
* @param {fabric.Canvas} canvas
|
||||||
|
* @returns {Array<{x:number,y:number}> | null}
|
||||||
|
*/
|
||||||
|
const buildRawMovedPoints = (roofId, canvas) => {
|
||||||
|
try {
|
||||||
|
const roof = canvas?.getObjects().find((o) => o.id === roofId)
|
||||||
|
if (!roof || !Array.isArray(roof.points)) return null
|
||||||
|
|
||||||
|
const moveFlowLine = roof.moveFlowLine ?? 0
|
||||||
|
const moveUpDown = roof.moveUpDown ?? 0
|
||||||
|
if (moveFlowLine === 0 && moveUpDown === 0) return null
|
||||||
|
|
||||||
|
// moveFlowLine 의 경우 기존 movingLineFromSkeleton 이 길이 축소를 하지 않으므로
|
||||||
|
// 여기서 raw 재구성 불필요 → null (fallback 으로 기존 값 사용).
|
||||||
|
if (moveFlowLine !== 0 && moveUpDown === 0) return null
|
||||||
|
|
||||||
|
const orgRoofPoints = roof.points
|
||||||
|
const prevLast = canvas?.skeleton?.lastPoints
|
||||||
|
|
||||||
|
// 풀 길이(roof.points.length) oldPoints 구성
|
||||||
|
// prevLast 가 더 짧으면(이미 축소되었던 상태) 부족분은 orgRoofPoints 로 채움.
|
||||||
|
const oldPoints = []
|
||||||
|
for (let i = 0; i < orgRoofPoints.length; i++) {
|
||||||
|
const src = (prevLast && prevLast[i]) ? prevLast[i] : orgRoofPoints[i]
|
||||||
|
oldPoints.push({ x: src.x, y: src.y })
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectLine = roof.moveSelectLine
|
||||||
|
if (!selectLine) return oldPoints
|
||||||
|
|
||||||
|
const wall = canvas.getObjects().find((o) => o.name === POLYGON_TYPE.WALL && o.attributes.roofId === roofId)
|
||||||
|
if (!wall || !Array.isArray(wall.baseLines)) return oldPoints
|
||||||
|
const baseLines = wall.baseLines
|
||||||
|
const basePoints = createOrderedBasePoints(roof.points, baseLines)
|
||||||
|
|
||||||
|
const newPoints = oldPoints.map((p) => ({ x: p.x, y: p.y }))
|
||||||
|
|
||||||
|
const moveUpDownLength = Big(moveUpDown).times(1).div(10)
|
||||||
|
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.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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return newPoints
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[buildRawMovedPoints] 실패 → null fallback:', e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이번 이동이 "경계" 상태에 대해 어떤 위치에 있는지 판정.
|
||||||
|
*
|
||||||
|
* 'ok' : 골짜기(valley) 유지 — 정상 이동
|
||||||
|
* 'on-boundary' : 골짜기가 정확히 외곽과 평행/일치 상태 — 허용되는 마지막 상태
|
||||||
|
* 'crossed' : 골짜기가 볼록으로 뒤집힘 — 옆 라인을 넘어감 (거부 대상)
|
||||||
|
* 'unknown' : 판정 불가 (데이터 부족 등) — 기존 흐름 그대로 진행
|
||||||
|
*
|
||||||
|
* 재료:
|
||||||
|
* - 기존 getTurnDirection() (CCW 외적) 재사용
|
||||||
|
* - 이번 이동 후 가상 폴리곤은 buildRawMovedPoints() 로 획득
|
||||||
|
* - 원본 roof.points 의 cross 부호와 비교
|
||||||
|
*
|
||||||
|
* 기존 함수(getTurnDirection, buildRawMovedPoints 등) 는 수정하지 않는다.
|
||||||
|
*
|
||||||
|
* @param {string} roofId
|
||||||
|
* @param {fabric.Canvas} canvas
|
||||||
|
* @returns {'ok'|'on-boundary'|'crossed'|'unknown'}
|
||||||
|
*/
|
||||||
|
export const verifyMoveBoundary = (roofId, canvas) => {
|
||||||
|
try {
|
||||||
|
const roof = canvas?.getObjects().find((o) => o.id === roofId)
|
||||||
|
if (!roof || !Array.isArray(roof.points)) return 'unknown'
|
||||||
|
|
||||||
|
const moveFlowLine = roof.moveFlowLine ?? 0
|
||||||
|
const moveUpDown = roof.moveUpDown ?? 0
|
||||||
|
if (moveFlowLine === 0 && moveUpDown === 0) return 'ok'
|
||||||
|
|
||||||
|
const proposed = buildRawMovedPoints(roofId, canvas)
|
||||||
|
const orig = roof.points
|
||||||
|
if (!Array.isArray(proposed) || proposed.length !== orig.length) return 'unknown'
|
||||||
|
|
||||||
|
const n = orig.length
|
||||||
|
if (n < 3) return 'unknown'
|
||||||
|
|
||||||
|
const posTol = 0.5
|
||||||
|
let worst = 'ok'
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
const prev = (i - 1 + n) % n
|
||||||
|
const next = (i + 1) % n
|
||||||
|
|
||||||
|
const crossOrig = getTurnDirection(orig[prev], orig[i], orig[next])
|
||||||
|
const crossNew = getTurnDirection(proposed[prev], proposed[i], proposed[next])
|
||||||
|
|
||||||
|
// 원래 골짜기(>0) 였던 꼭짓점만 경계 판정 대상
|
||||||
|
if (crossOrig > 0) {
|
||||||
|
// 상대 허용오차: 원본 cross 크기 5% 이내면 "0 근처" 로 간주 (on-boundary)
|
||||||
|
const boundaryTol = Math.max(1.0, Math.abs(crossOrig) * 0.05)
|
||||||
|
|
||||||
|
if (crossNew < -boundaryTol) {
|
||||||
|
console.warn(
|
||||||
|
`[verifyMoveBoundary] 꼭짓점[${i}] 골짜기 → 볼록 뒤집힘 (CROSSED): ${crossOrig.toFixed(1)} → ${crossNew.toFixed(1)}`
|
||||||
|
)
|
||||||
|
return 'crossed'
|
||||||
|
}
|
||||||
|
if (Math.abs(crossNew) <= boundaryTol) {
|
||||||
|
console.log(
|
||||||
|
`[verifyMoveBoundary] 꼭짓점[${i}] 경계 도달 (on-boundary): cross ≈ 0 (${crossNew.toFixed(1)})`
|
||||||
|
)
|
||||||
|
if (worst === 'ok') worst = 'on-boundary'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return worst
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[verifyMoveBoundary] 판정 실패 → unknown:', e)
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const safeMovedPointsWithFallback = (roofId, canvas) => {
|
||||||
|
const roof = canvas?.getObjects().find((object) => object.id === roofId)
|
||||||
|
const orgRoofPoints = roof?.points ?? []
|
||||||
|
const lastPoints = canvas?.skeleton?.lastPoints ?? null
|
||||||
|
const oldPoints = lastPoints ?? orgRoofPoints
|
||||||
|
|
||||||
|
const movedPoints = movingLineFromSkeleton(roofId, canvas)
|
||||||
|
|
||||||
|
if (!Array.isArray(movedPoints) || movedPoints.length === 0) {
|
||||||
|
console.warn('[safeMovedPointsWithFallback] movedPoints 비정상 → bail out')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const tolerance = 0.5
|
||||||
|
|
||||||
|
// 1) 다각형 붕괴/자기교차 감지
|
||||||
|
// (a) zero-length edge (연속된 점이 동일 좌표)
|
||||||
|
// (b) 점 개수가 oldPoints 대비 절반 이하로 급감
|
||||||
|
// (c) 세그먼트 자기교차 (옆 라인을 넘어가는 케이스)
|
||||||
|
let zeroLenEdges = 0
|
||||||
|
for (let i = 0; i < movedPoints.length; i++) {
|
||||||
|
const a = movedPoints[i]
|
||||||
|
const b = movedPoints[(i + 1) % movedPoints.length]
|
||||||
|
if (!a || !b) continue
|
||||||
|
if (Math.abs(a.x - b.x) < tolerance && Math.abs(a.y - b.y) < tolerance) {
|
||||||
|
zeroLenEdges++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const severeReduction =
|
||||||
|
oldPoints.length >= 6 && movedPoints.length > 0 && movedPoints.length <= Math.floor(oldPoints.length / 2)
|
||||||
|
|
||||||
|
// 자기교차: 인접하지 않은 두 세그먼트가 교차하는지 검사
|
||||||
|
const segIntersect = (p1, p2, p3, p4) => {
|
||||||
|
const d = (p2.x - p1.x) * (p4.y - p3.y) - (p2.y - p1.y) * (p4.x - p3.x)
|
||||||
|
if (Math.abs(d) < 1e-9) return false
|
||||||
|
const t = ((p3.x - p1.x) * (p4.y - p3.y) - (p3.y - p1.y) * (p4.x - p3.x)) / d
|
||||||
|
const u = ((p3.x - p1.x) * (p2.y - p1.y) - (p3.y - p1.y) * (p2.x - p1.x)) / d
|
||||||
|
return t > 1e-6 && t < 1 - 1e-6 && u > 1e-6 && u < 1 - 1e-6
|
||||||
|
}
|
||||||
|
let selfIntersect = false
|
||||||
|
const n = movedPoints.length
|
||||||
|
outer: for (let i = 0; i < n; i++) {
|
||||||
|
const a1 = movedPoints[i]
|
||||||
|
const a2 = movedPoints[(i + 1) % n]
|
||||||
|
for (let j = i + 2; j < n; j++) {
|
||||||
|
// 마지막-첫번째 인접 세그먼트는 스킵
|
||||||
|
if (i === 0 && j === n - 1) continue
|
||||||
|
const b1 = movedPoints[j]
|
||||||
|
const b2 = movedPoints[(j + 1) % n]
|
||||||
|
if (!a1 || !a2 || !b1 || !b2) continue
|
||||||
|
if (segIntersect(a1, a2, b1, b2)) {
|
||||||
|
selfIntersect = true
|
||||||
|
break outer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zeroLenEdges > 0 || severeReduction || selfIntersect) {
|
||||||
|
console.warn('[safeMovedPointsWithFallback] 붕괴/교차 감지 (로그만)', {
|
||||||
|
movedLen: movedPoints.length,
|
||||||
|
oldLen: oldPoints.length,
|
||||||
|
zeroLenEdges,
|
||||||
|
severeReduction,
|
||||||
|
selfIntersect,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return movedPoints
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SkeletonBuilder를 사용하여 스켈레톤을 생성하고 내부선을 그립니다.
|
* SkeletonBuilder를 사용하여 스켈레톤을 생성하고 내부선을 그립니다.
|
||||||
* @param {string} roofId - 지붕 ID
|
* @param {string} roofId - 지붕 ID
|
||||||
@ -522,7 +780,17 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
|
|||||||
// 6. 마루이동(용마루 위치 수동 조정)이 설정된 경우 movingLineFromSkeleton 결과로 대체
|
// 6. 마루이동(용마루 위치 수동 조정)이 설정된 경우 movingLineFromSkeleton 결과로 대체
|
||||||
|
|
||||||
if (moveFlowLine !== 0 || moveUpDown !== 0) {
|
if (moveFlowLine !== 0 || moveUpDown !== 0) {
|
||||||
const movedPoints = movingLineFromSkeleton(roofId, canvas)
|
// [보호 가드] 이동이 골짜기 경계를 넘어가는지 사전 판정
|
||||||
|
// 'crossed' → 외곽선을 뒤집을 만큼 과한 이동 → 기존 SK/innerLines/skeletonLines 전부 보존 후 조용히 종료
|
||||||
|
// 'ok' | 'on-boundary' | 'unknown' → 기존 흐름 그대로 진행
|
||||||
|
// ※ 기존 로직은 전혀 수정하지 않고, 진입부에 한 블록만 추가
|
||||||
|
const __moveVerdict = verifyMoveBoundary(roofId, canvas)
|
||||||
|
if (__moveVerdict === 'crossed') {
|
||||||
|
console.warn('[skeleton] 경계 넘음(crossed) → 재빌드 스킵 (기존 SK/innerLines/skeletonLines 유지)')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const movedPoints = safeMovedPointsWithFallback(roofId, canvas)
|
||||||
// console.log("movedPoints:::", movedPoints);
|
// console.log("movedPoints:::", movedPoints);
|
||||||
|
|
||||||
// movedPoints를 roof 경계로 확장
|
// movedPoints를 roof 경계로 확장
|
||||||
@ -637,7 +905,13 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
|
|||||||
if (isOverDetected) {
|
if (isOverDetected) {
|
||||||
// [예외] 오버 감지 시 → 별도 함수에서 스켈레톤 전용 보정 포인트 계산
|
// [예외] 오버 감지 시 → 별도 함수에서 스켈레톤 전용 보정 포인트 계산
|
||||||
try {
|
try {
|
||||||
const corrected = calcOverCorrectedPoints(changRoofLinePoints, origPoints)
|
// 누적 이동 보호: 이전 SK 확정 상태(lastPoints)를 전달하여
|
||||||
|
// 1차 이동에서 이미 확정된 점은 보정 대상에서 제외한다.
|
||||||
|
const corrected = calcOverCorrectedPointsSafe(
|
||||||
|
changRoofLinePoints,
|
||||||
|
origPoints,
|
||||||
|
canvas?.skeleton?.lastPoints ?? null
|
||||||
|
)
|
||||||
if (corrected && corrected.length >= 3) {
|
if (corrected && corrected.length >= 3) {
|
||||||
skeletonInputPoints = corrected
|
skeletonInputPoints = corrected
|
||||||
console.log('[SK_OVER] 보정 포인트 적용:', skeletonInputPoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`))
|
console.log('[SK_OVER] 보정 포인트 적용:', skeletonInputPoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`))
|
||||||
@ -687,7 +961,15 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
|
|||||||
}
|
}
|
||||||
canvas.skeleton = []
|
canvas.skeleton = []
|
||||||
canvas.skeleton = cleanSkeleton
|
canvas.skeleton = cleanSkeleton
|
||||||
|
// lastPoints 는 축소되지 않은 풀 길이(roof.points.length) 버전으로 저장해야
|
||||||
|
// 다음 이동 시 인덱스 기반 매칭이 유지됨. raw 재구성 실패 시에만 기존 값 사용.
|
||||||
|
const rawMovedFull = buildRawMovedPoints(roofId, canvas)
|
||||||
|
if (rawMovedFull && rawMovedFull.length === (roof.points?.length ?? 0)) {
|
||||||
|
canvas.skeleton.lastPoints = rawMovedFull
|
||||||
|
console.log('[skeleton] lastPoints 저장: raw 풀 길이', rawMovedFull.length, '점')
|
||||||
|
} else {
|
||||||
canvas.skeleton.lastPoints = roofLineContactPoints
|
canvas.skeleton.lastPoints = roofLineContactPoints
|
||||||
|
}
|
||||||
canvas.set('skeleton', cleanSkeleton)
|
canvas.set('skeleton', cleanSkeleton)
|
||||||
canvas.renderAll()
|
canvas.renderAll()
|
||||||
|
|
||||||
@ -696,11 +978,12 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
|
|||||||
console.error('[SK_ERROR] 스켈레톤 생성 중 오류 발생:', e)
|
console.error('[SK_ERROR] 스켈레톤 생성 중 오류 발생:', e)
|
||||||
console.error('[SK_ERROR] 입력 좌표:', changRoofLinePoints.map((p, i) => `[${i}] (${p.x.toFixed(2)},${p.y.toFixed(2)})`))
|
console.error('[SK_ERROR] 입력 좌표:', changRoofLinePoints.map((p, i) => `[${i}] (${p.x.toFixed(2)},${p.y.toFixed(2)})`))
|
||||||
console.error('[SK_ERROR] geoJSON:', JSON.stringify(geoJSONPolygon))
|
console.error('[SK_ERROR] geoJSON:', JSON.stringify(geoJSONPolygon))
|
||||||
|
// [보호] 예외 시 상태 플래그만 복구하고, 기존 SK/innerLines는 유지한다.
|
||||||
|
// → 사용자가 힘들게 추가한 라인들이 순간적으로 사라지는 현상 방지.
|
||||||
if (canvas.skeletonStates) {
|
if (canvas.skeletonStates) {
|
||||||
canvas.skeletonStates[roofId] = false
|
canvas.skeletonStates[roofId] = false
|
||||||
canvas.skeletonStates = {}
|
|
||||||
canvas.skeletonLines = []
|
|
||||||
}
|
}
|
||||||
|
// 주의: canvas.skeletonLines 는 의도적으로 비우지 않음 (이전 성공 상태 보존)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1866,17 +2149,17 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('📐 [processEavesEdge] face 분석:', {
|
// console.log('📐 [processEavesEdge] face 분석:', {
|
||||||
hasOuterLine: !!outerLine,
|
// hasOuterLine: !!outerLine,
|
||||||
outerLineType: outerLine?.attributes?.type,
|
// outerLineType: outerLine?.attributes?.type,
|
||||||
pitch,
|
// pitch,
|
||||||
roofLineIndex,
|
// roofLineIndex,
|
||||||
edgeBegin: { x: Math.round(Begin.X), y: Math.round(Begin.Y) },
|
// edgeBegin: { x: Math.round(Begin.X), y: Math.round(Begin.Y) },
|
||||||
edgeEnd: { x: Math.round(End.X), y: Math.round(End.Y) },
|
// edgeEnd: { x: Math.round(End.X), y: Math.round(End.Y) },
|
||||||
polygonPointCount: polygonPoints.length,
|
// polygonPointCount: polygonPoints.length,
|
||||||
polygonPoints: polygonPoints.map(p => ({ x: Math.round(p.x), y: Math.round(p.y) })),
|
// polygonPoints: polygonPoints.map(p => ({ x: Math.round(p.x), y: Math.round(p.y) })),
|
||||||
skPtsCount: skPts.length,
|
// skPtsCount: skPts.length,
|
||||||
})
|
// })
|
||||||
|
|
||||||
for (let i = 0; i < polygonPoints.length; i++) {
|
for (let i = 0; i < polygonPoints.length; i++) {
|
||||||
const p1 = polygonPoints[i];
|
const p1 = polygonPoints[i];
|
||||||
@ -1886,7 +2169,7 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) {
|
|||||||
|
|
||||||
// 확장된 외곽선에 해당하는 edge는 스킵
|
// 확장된 외곽선에 해당하는 edge는 스킵
|
||||||
if (_isSkipOuter) {
|
if (_isSkipOuter) {
|
||||||
console.log(' [edge', i, '] SKIP(outerEdge)', { p1: {x: Math.round(p1.x), y: Math.round(p1.y)}, p2: {x: Math.round(p2.x), y: Math.round(p2.y)} })
|
// console.log(' [edge', i, '] SKIP(outerEdge)', { p1: {x: Math.round(p1.x), y: Math.round(p1.y)}, p2: {x: Math.round(p2.x), y: Math.round(p2.y)} })
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1895,10 +2178,10 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) {
|
|||||||
//console.log('clipped line', clippedLine.p1, clippedLine.p2);
|
//console.log('clipped line', clippedLine.p1, clippedLine.p2);
|
||||||
const isOuterLine = isOuterEdge(clippedLine.p1, clippedLine.p2, [edgeResult.Edge])
|
const isOuterLine = isOuterEdge(clippedLine.p1, clippedLine.p2, [edgeResult.Edge])
|
||||||
|
|
||||||
const _dx = Math.abs(clippedLine.p2.x - clippedLine.p1.x)
|
// const _dx = Math.abs(clippedLine.p2.x - clippedLine.p1.x)
|
||||||
const _dy = Math.abs(clippedLine.p2.y - clippedLine.p1.y)
|
// const _dy = Math.abs(clippedLine.p2.y - clippedLine.p1.y)
|
||||||
const _lineType = (_dx < 0.5 && _dy > 0.5) ? '⚠️수직' : (_dy < 0.5 && _dx > 0.5) ? '수평' : '대각'
|
// const _lineType = (_dx < 0.5 && _dy > 0.5) ? '⚠️수직' : (_dy < 0.5 && _dx > 0.5) ? '수평' : '대각'
|
||||||
console.log(` [edge ${i}] ${_lineType} ${isOuterLine ? 'OUTER' : 'INNER'} (${Math.round(clippedLine.p1.x)},${Math.round(clippedLine.p1.y)})→(${Math.round(clippedLine.p2.x)},${Math.round(clippedLine.p2.y)}) dx:${Math.round(_dx)} dy:${Math.round(_dy)}`)
|
// console.log(` [edge ${i}] ${_lineType} ${isOuterLine ? 'OUTER' : 'INNER'} (${Math.round(clippedLine.p1.x)},${Math.round(clippedLine.p1.y)})→(${Math.round(clippedLine.p2.x)},${Math.round(clippedLine.p2.y)}) dx:${Math.round(_dx)} dy:${Math.round(_dy)}`)
|
||||||
|
|
||||||
addRawLine(roof.id, skeletonLines, clippedLine.p1, clippedLine.p2, 'ridge', '#1083E3', 4, pitch, isOuterLine, targetWallId);
|
addRawLine(roof.id, skeletonLines, clippedLine.p1, clippedLine.p2, 'ridge', '#1083E3', 4, pitch, isOuterLine, targetWallId);
|
||||||
// }
|
// }
|
||||||
@ -1978,7 +2261,7 @@ function logDeadEndLines(skeletonLines, roof) {
|
|||||||
const dy = Math.abs(line.p2.y - line.p1.y);
|
const dy = Math.abs(line.p2.y - line.p1.y);
|
||||||
|
|
||||||
if (dx < 0.5 && dy < 0.5) {
|
if (dx < 0.5 && dy < 0.5) {
|
||||||
console.log('⚠️ [logDeadEndLines] 제로 길이:', { idx, p1: { x: Math.round(line.p1.x), y: Math.round(line.p1.y) } });
|
// console.log('⚠️ [logDeadEndLines] 제로 길이:', { idx, p1: { x: Math.round(line.p1.x), y: Math.round(line.p1.y) } });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1997,7 +2280,7 @@ function logDeadEndLines(skeletonLines, roof) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`🔍 [logDeadEndLines] 전체: ${skeletonLines.length}, 유니크: ${uniqueLines.length}, dead end 꼭짓점: ${deadEndVertices.size}`);
|
// console.log(`🔍 [logDeadEndLines] 전체: ${skeletonLines.length}, 유니크: ${uniqueLines.length}, dead end 꼭짓점: ${deadEndVertices.size}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function findMatchingLine(edgePolygon, roof, roofPoints) {
|
function findMatchingLine(edgePolygon, roof, roofPoints) {
|
||||||
@ -2106,6 +2389,59 @@ function processGableEdge(edgeResult, baseLines, skeletonLines, selectBaseLine,
|
|||||||
* @param {Array<{x,y}>} origPoints - roof.points (원본 폴리곤)
|
* @param {Array<{x,y}>} origPoints - roof.points (원본 폴리곤)
|
||||||
* @returns {Array<{x,y}>} 보정된 스켈레톤 입력 포인트 (실패 시 원본 반환)
|
* @returns {Array<{x,y}>} 보정된 스켈레톤 입력 포인트 (실패 시 원본 반환)
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* calcOverCorrectedPoints 의 안전 래퍼.
|
||||||
|
*
|
||||||
|
* 문제:
|
||||||
|
* 기존 calcOverCorrectedPoints 는 "원본(roof.points) 대비 이동"으로 moved 판정을
|
||||||
|
* 하기 때문에, 누적 이동 시(1차 이동 후 2차 이동) 1차에서 이미 확정된 점까지도
|
||||||
|
* moved=true 로 보고 clampToFixed 대상으로 잡아 원본으로 되돌려 버린다.
|
||||||
|
*
|
||||||
|
* 처리:
|
||||||
|
* 1) lastPoints 가 있으면 "이번 이동에서 실제로 변한 인덱스(changedNow)" 만 추림.
|
||||||
|
* 2) changedNow=false 인 인덱스는 "원본=lastPoints(직전 확정 상태)" 로 간주한 virtualOrig 구성.
|
||||||
|
* → 해당 인덱스는 기존 calcOverCorrectedPoints 내부에서 moved=false 로 판정되어
|
||||||
|
* clampToFixed 대상에서 제외됨 (= 1차 이동 결과 보존).
|
||||||
|
* 3) changedNow=true 인 인덱스만 원본(origPoints) 대비로 오버 보정 그대로 수행.
|
||||||
|
* 4) lastPoints 가 없거나 길이가 안 맞으면 기존 함수를 그대로 호출 (동작 변화 없음).
|
||||||
|
*
|
||||||
|
* 기존 calcOverCorrectedPoints 는 수정하지 않는다.
|
||||||
|
*
|
||||||
|
* @param {Array<{x,y}>} points - changRoofLinePoints (수정 X)
|
||||||
|
* @param {Array<{x,y}>} origPoints - roof.points (원본)
|
||||||
|
* @param {Array<{x,y}>|null} lastPoints - 직전 SK 확정 상태 (canvas.skeleton.lastPoints)
|
||||||
|
* @returns {Array<{x,y}>}
|
||||||
|
*/
|
||||||
|
const calcOverCorrectedPointsSafe = (points, origPoints, lastPoints) => {
|
||||||
|
if (!Array.isArray(lastPoints) || !Array.isArray(origPoints)) {
|
||||||
|
return calcOverCorrectedPoints(points, origPoints)
|
||||||
|
}
|
||||||
|
if (lastPoints.length !== origPoints.length || points.length !== origPoints.length) {
|
||||||
|
return calcOverCorrectedPoints(points, origPoints)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tol = 1
|
||||||
|
const changedNow = points.map((p, i) => {
|
||||||
|
const lp = lastPoints[i]
|
||||||
|
if (!lp) return true
|
||||||
|
return Math.abs(p.x - lp.x) > tol || Math.abs(p.y - lp.y) > tol
|
||||||
|
})
|
||||||
|
|
||||||
|
// 이번에 아무것도 안 바뀌었으면 그대로 반환
|
||||||
|
if (!changedNow.some(Boolean)) return points
|
||||||
|
|
||||||
|
// 이전 이동에서 확정된(이번엔 그대로) 점들은 lastPoints 를 원본으로 간주
|
||||||
|
const virtualOrig = origPoints.map((op, i) =>
|
||||||
|
changedNow[i] ? op : (lastPoints[i] ?? op)
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log('[calcOverCorrectedPointsSafe] changedNow:',
|
||||||
|
changedNow.map((v, i) => v ? i : null).filter(v => v !== null))
|
||||||
|
|
||||||
|
return calcOverCorrectedPoints(points, virtualOrig)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const calcOverCorrectedPoints = (points, origPoints) => {
|
const calcOverCorrectedPoints = (points, origPoints) => {
|
||||||
const n = points.length
|
const n = points.length
|
||||||
const skPoints = points.map(p => ({ x: p.x, y: p.y }))
|
const skPoints = points.map(p => ({ x: p.x, y: p.y }))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user