From cd5eebd08f4bea43afc075b89dca30cff42dbe53 Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 2 Apr 2026 14:45:02 +0900 Subject: [PATCH] =?UTF-8?q?[1840]=EC=83=9D=EC=84=B1=EB=90=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EC=9D=80=20=ED=8C=94=EC=9E=91=EC=A7=80=EB=B6=95=20?= =?UTF-8?q?=EB=8F=84=EB=A9=B4=20=EC=82=AC=EB=A1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/skeletons/SkeletonBuilder.ts | 13 +++++----- src/util/skeleton-utils.js | 37 +++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/lib/skeletons/SkeletonBuilder.ts b/src/lib/skeletons/SkeletonBuilder.ts index 01ddb95b..6466782d 100644 --- a/src/lib/skeletons/SkeletonBuilder.ts +++ b/src/lib/skeletons/SkeletonBuilder.ts @@ -253,13 +253,12 @@ export default class SkeletonBuilder { } private static CorrectBisectorDirection(bisector: LineParametric2d, beginNextVertex: Vertex, endPreviousVertex: Vertex, beginEdge: Edge, endEdge: Edge) { - const beginEdge2 = beginNextVertex.PreviousEdge; - const endEdge2 = endPreviousVertex.NextEdge; + // Use the vertex's actual edges — chain edges can become stale + // when previous iterations of MultiSplitEvent modify the LAV + const actualBeginEdge = beginNextVertex.PreviousEdge; + const actualEndEdge = endPreviousVertex.NextEdge; - if (beginEdge !== beginEdge2 || endEdge !== endEdge2) - throw new Error(); - - if (beginEdge.Norm.Dot(endEdge.Norm) < -0.97) { + if (actualBeginEdge.Norm.Dot(actualEndEdge.Norm) < -0.97) { const n1 = PrimitiveUtils.FromTo(endPreviousVertex.Point, bisector.A).Normalized(); const n2 = PrimitiveUtils.FromTo(bisector.A, beginNextVertex.Point).Normalized(); const bisectorPrediction = this.CalcVectorBisector(n1, n2); @@ -887,7 +886,7 @@ export default class SkeletonBuilder { private static GetEdgeInLav(lav: CircularList, oppositeEdge: Edge): Vertex { for (const node of lav.Iterate()) if (oppositeEdge === node.PreviousEdge || - oppositeEdge === node.Previous.Next) + oppositeEdge === (node.Previous as Vertex).NextEdge) return node; return null; diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index 17374edd..ae92a224 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -16,6 +16,39 @@ import wallLine from '@/components/floor-plan/modal/wallLineOffset/type/WallLine */ const EPSILON = 0.1 +/** + * 오목(concave) 폴리곤인지 판별합니다. + * cross product의 부호가 혼재하면 오목 폴리곤입니다. + */ +const isConcavePolygon = (points) => { + if (!points || points.length < 4) return false + let positive = 0, negative = 0 + for (let i = 0; i < points.length; i++) { + const prev = points[(i - 1 + points.length) % points.length] + const curr = points[i] + const next = points[(i + 1) % points.length] + const cross = (curr.x - prev.x) * (next.y - curr.y) - (curr.y - prev.y) * (next.x - curr.x) + if (cross > 0) positive++ + else if (cross < 0) negative++ + } + return positive > 0 && negative > 0 +} + +/** + * 오목 폴리곤에만 미세한 perturbation을 적용하여 동시 이벤트(MultiSplitEvent)를 방지합니다. + * 대칭적 변 길이(예: 45.5, 45.5)가 동일 거리의 이벤트를 만들어 LAV 불일치 오류를 유발하므로 + * 각 꼭짓점에 인덱스 기반 미세 오프셋을 적용하면 이벤트가 개별 SplitEvent로 처리됩니다. + * 볼록 폴리곤은 이 문제가 없으므로 원본을 그대로 반환합니다. + */ +const perturbPolygonPoints = (points, eps = 1e-4) => { + if (!points || points.length < 3) return points + if (!isConcavePolygon(points)) return points + return points.map((p, i) => ({ + x: p.x + (i + 1) * eps, + y: p.y + (i + 1) * eps + })) +} + export const drawSkeletonRidgeRoof = (roofId, canvas, textMode) => { // 2. 스켈레톤 생성 및 그리기 @@ -503,11 +536,13 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { // changRoofLinePoints를 roof.skeletonPoints에 저장 (roof.points 원본은 유지) roof.skeletonPoints = changRoofLinePoints.map(p => ({ x: p.x, y: p.y })) - const geoJSONPolygon = toGeoJSON(changRoofLinePoints) + const perturbedPoints = perturbPolygonPoints(changRoofLinePoints) + const geoJSONPolygon = toGeoJSON(perturbedPoints) try { // SkeletonBuilder는 닫히지 않은 폴리곤을 기대하므로 마지막 점 제거 geoJSONPolygon.pop() + console.log('[SkeletonBuilder] geoJSONPolygon:', JSON.stringify(geoJSONPolygon, null, 2)) const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]]) // 스켈레톤 데이터를 기반으로 내부선 생성 -- 2.47.2