diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index 7d6b1b23..6a3c567e 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -394,79 +394,81 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { const isClosedPolygon = (points) => points.length > 1 && isSamePoint(points[0], points[points.length - 1]) - const roofLinePoints = isClosedPolygon(roof.points) ? roof.points.slice(0, -1) : roof.points const orderedBaseLinePoints = isClosedPolygon(baseLinePoints) ? baseLinePoints.slice(0, -1) : baseLinePoints - console.log('roofLinePoints:', roofLinePoints) - console.log('baseLinePoints:', orderedBaseLinePoints) - - // baseLinePoint에서 roofLinePoint 방향으로 45도 대각선 확장 후 가장 가까운 접점 계산 - // 각 포인트의 dx, dy, sign 정보 수집 - const contactData = orderedBaseLinePoints.map((point, index) => { - const roofPoint = roofLinePoints[index] - if (!roofPoint) return { dx: 0, dy: 0, signDx: 0, signDy: 0 } - - const dx = roofPoint.x - point.x - const dy = roofPoint.y - point.y - return { dx, dy, signDx: Math.sign(dx), signDy: Math.sign(dy) } - }) - - // maxStep: 모든 포인트 중 가장 큰 45도 step - const maxStep = contactData.reduce((max, data) => { - return Math.max(max, Math.min(Math.abs(data.dx), Math.abs(data.dy))) - }, 0) - - // baseLine 폴리곤의 중심 계산 (확장 방향 결정용) + // baseLine 폴리곤의 중심 계산 (offset 방향 결정용) const centroid = orderedBaseLinePoints.reduce((acc, p) => { acc.x += p.x / orderedBaseLinePoints.length acc.y += p.y / orderedBaseLinePoints.length return acc }, { x: 0, y: 0 }) - // 모든 포인트를 동일한 maxStep으로 45도 확장 - let roofLineContactPoints = orderedBaseLinePoints.map((point, index) => { - const data = contactData[index] - const { dx, dy } = data - - // dx 또는 dy가 0인 경우 중심에서 바깥쪽 방향으로 확장 - let signDx = data.signDx - let signDy = data.signDy - if (signDx === 0) { - signDx = point.x >= centroid.x ? 1 : -1 - } - if (signDy === 0) { - signDy = point.y >= centroid.y ? 1 : -1 - } - + // baseLine을 offset만큼 바깥으로 평행이동 + const offsetLine = (line) => { + const sp = line.startPoint, ep = line.endPoint + const offset = line.attributes?.offset ?? 0 + const edx = ep.x - sp.x, edy = ep.y - sp.y + const len = Math.hypot(edx, edy) + if (len === 0) return { sp: { ...sp }, ep: { ...ep } } + let nx = -edy / len, ny = edx / len + const mid = { x: (sp.x + ep.x) / 2, y: (sp.y + ep.y) / 2 } + if (nx * (centroid.x - mid.x) + ny * (centroid.y - mid.y) > 0) { nx = -nx; ny = -ny } return { - x: point.x + signDx * maxStep, - y: point.y + signDy * maxStep + sp: { x: sp.x + nx * offset, y: sp.y + ny * offset }, + ep: { x: ep.x + nx * offset, y: ep.y + ny * offset } } + } + + // 두 직선의 교차점 + const lineIntersect = (a1, a2, b1, b2) => { + const denom = (a1.x - a2.x) * (b1.y - b2.y) - (a1.y - a2.y) * (b1.x - b2.x) + if (Math.abs(denom) < 0.001) return null + const t = ((a1.x - b1.x) * (b1.y - b2.y) - (a1.y - b1.y) * (b1.x - b2.x)) / denom + return { x: a1.x + t * (a2.x - a1.x), y: a1.y + t * (a2.y - a1.y) } + } + + // 각 꼭짓점에서 인접 두 baseLine의 offset 교차점으로 확장 좌표 계산 + let changRoofLinePoints = orderedBaseLinePoints.map((point) => { + const adjLines = baseLines.filter(line => + isSamePoint(point, line.startPoint) || isSamePoint(point, line.endPoint) + ) + if (adjLines.length < 2) return { ...point } + const oL1 = offsetLine(adjLines[0]), oL2 = offsetLine(adjLines[1]) + return lineIntersect(oL1.sp, oL1.ep, oL2.sp, oL2.ep) || { ...point } }) - const maxContactDistance = Math.hypot(maxStep, maxStep) - - let changRoofLinePoints = orderedBaseLinePoints.map((point, index) => { - const contactPoint = roofLineContactPoints[index] - if (!contactPoint) return point - - const dx = contactPoint.x - point.x - const dy = contactPoint.y - point.y - const len = Math.hypot(dx, dy) - - // 거리가 0이면 이미 같은 위치 - if (len === 0) return point - - const step = maxContactDistance - if (step === 0) return point - - const nextX = point.x + (dx / len) * step - const nextY = point.y + (dy / len) * step - return { - x: nextX, - y: nextY + // 중복 좌표 및 일직선 위의 불필요한 점 제거 (L자 확장 시 사각형으로 단순화) + const simplifyPolygon = (pts) => { + let result = [...pts] + let changed = true + while (changed) { + changed = false + for (let i = result.length - 1; i >= 0; i--) { + const next = result[(i + 1) % result.length] + if (Math.abs(result[i].x - next.x) < 0.5 && Math.abs(result[i].y - next.y) < 0.5) { + result.splice(i, 1) + changed = true + } + } + for (let i = result.length - 1; i >= 0 && result.length > 3; i--) { + const prev = result[(i - 1 + result.length) % result.length] + const curr = result[i] + const next = result[(i + 1) % result.length] + const cross = (curr.x - prev.x) * (next.y - prev.y) - (curr.y - prev.y) * (next.x - prev.x) + if (Math.abs(cross) < 1) { + result.splice(i, 1) + changed = true + } + } } - }) + return result + } + changRoofLinePoints = simplifyPolygon(changRoofLinePoints) + + let roofLineContactPoints = [...changRoofLinePoints] + + console.log('baseLinePoints:', orderedBaseLinePoints) + console.log('changRoofLinePoints:', changRoofLinePoints) //마루이동 if (moveFlowLine !== 0 || moveUpDown !== 0) { @@ -489,7 +491,7 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { // 스켈레톤 데이터를 기반으로 내부선 생성 roof.innerLines = roof.innerLines || [] roof.innerLines = createInnerLinesFromSkeleton(roofId, canvas, skeleton, textMode) - //console.log("roofInnerLines:::", roof.innerLines); + console.log("roofInnerLines:::", roof.innerLines); // 캔버스에 스켈레톤 상태 저장 if (!canvas.skeletonStates) { canvas.skeletonStates = {}