From 09ad09957b85d7fcfc71fc87e8c955a7fe92714b Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 2 Jul 2026 11:32:09 +0900 Subject: [PATCH] =?UTF-8?q?[1514=5F3]=20=E3=85=97=ED=98=95=20W-6=20?= =?UTF-8?q?=EA=B0=80=EB=A1=9C=EB=B0=95=EA=B3=B5=20=EB=A7=88=EB=A3=A8=20?= =?UTF-8?q?=EB=AF=B8=EC=83=9D=EC=84=B1=20=EC=88=98=EC=A0=95=20=E2=80=94=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EA=B8=B0=EB=B0=98=20SK=20=EB=A7=88?= =?UTF-8?q?=EB=A3=A8=20=EC=A0=9C=EA=B1=B0=20+=20=EB=A3=A8=ED=94=84=20?= =?UTF-8?q?=ED=8F=B4=EB=B0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4 --- src/util/qpolygon-utils.js | 69 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index 85244457..77b84742 100644 --- a/src/util/qpolygon-utils.js +++ b/src/util/qpolygon-utils.js @@ -7,6 +7,7 @@ import { LINE_TYPE, POLYGON_TYPE } from '@/common/common' import Big from 'big.js' import * as turf from '@turf/turf' + const TWO_PI = Math.PI * 2 const EPSILON = 1e-10 //좌표계산 시 최소 차이값 @@ -4361,6 +4362,7 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => { right: nextIndex, type: TYPES.RIDGE, degree: 0, + gableRidge: true, // [KERAB-GABLE-JOG-FIX 2026-07-02] 박공 파이프라인 생성 마루 식별 }) } } @@ -4425,6 +4427,44 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => { canvas.renderAll() }) + // [KERAB-GABLE-JOG-FIX 2026-07-02] 박공 마루(gableRidge)가 생기면 그 팔 안의 중심선 마루 하나만 유효하다. + // 기반 스켈레톤(proceedRidges)이 뱉는, 박공 마루와 평행·index공유·같은 팔에 겹치는 중복 세로/가로 마루는 제거한다. + // (처마일 때는 apex 로 흡수되던 마루가, 박공 마루 추가로 index 를 공유하며 이중마루로 남는 것을 막는다.) + { + const gableRidges = linesAnalysis.filter((l) => l.type === TYPES.RIDGE && l.gableRidge) + if (gableRidges.length > 0) { + const isVert = (l) => Math.abs(l.start.x - l.end.x) < Math.abs(l.start.y - l.end.y) + const removeSet = new Set() + linesAnalysis.forEach((base) => { + if (base.type !== TYPES.RIDGE || base.gableRidge) return + gableRidges.forEach((g) => { + if (isVert(base) !== isVert(g)) return // 평행만 + const shareIdx = base.left === g.left || base.left === g.right || base.right === g.left || base.right === g.right + if (!shareIdx) return + const gLen = Math.hypot(g.end.x - g.start.x, g.end.y - g.start.y) + if (isVert(g)) { + const bMin = Math.min(base.start.y, base.end.y) + const bMax = Math.max(base.start.y, base.end.y) + const gMin = Math.min(g.start.y, g.end.y) + const gMax = Math.max(g.start.y, g.end.y) + const overlap = Math.min(bMax, gMax) - Math.max(bMin, gMin) > EPSILON + if (overlap && Math.abs(base.start.x - g.start.x) < gLen) removeSet.add(base) + } else { + const bMin = Math.min(base.start.x, base.end.x) + const bMax = Math.max(base.start.x, base.end.x) + const gMin = Math.min(g.start.x, g.end.x) + const gMax = Math.max(g.start.x, g.end.x) + const overlap = Math.min(bMax, gMax) - Math.max(bMin, gMin) > EPSILON + if (overlap && Math.abs(base.start.y - g.start.y) < gLen) removeSet.add(base) + } + }) + }) + if (removeSet.size > 0) { + linesAnalysis = linesAnalysis.filter((l) => !removeSet.has(l)) + } + } + } + //각 선분의 교차점을 찾아 선을 그린다. const MAX_ITERATIONS = 1000 //무한루프 방지 let iterations = 0 @@ -4439,6 +4479,14 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => { let linePoint = null let partner = null + // [KERAB-GABLE-JOG-FIX 2026-07-02] 인덱스를 공유하는(=정당한 접합 가능) 최근접 파트너를 별도 추적. + // ㅗ 노치의 계단형 마루(offset 평행)처럼 인덱스 미공유 교점이 최근접으로 끼어들어 상호최단을 가로채면, + // 마루↔힙의 정당한 접합이 막혀 마루가 소멸한다. 전역 최근접이 인덱스 미공유일 때만 이 후보로 폴백한다. + let sameMinDistance = Infinity + let sameIntersectPoint = null + let sameLinePoint = null + let samePartner = null + const cLength = Math.sqrt(Math.pow(currLine.end.x - currLine.start.x, 2) + Math.pow(currLine.end.y - currLine.start.y, 2)) //남은 길이가 0이면 무시 if (cLength < EPSILON) return @@ -4476,8 +4524,29 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => { partner = j } } + // [KERAB-GABLE-JOG-FIX 2026-07-02] 인덱스 공유(정당한 접합) 최근접 후보를 별도로 추적 + const shareIdx = + currLine.left === nextLine.left || currLine.left === nextLine.right || currLine.right === nextLine.left || currLine.right === nextLine.right + if (shareIdx && distance1 < sameMinDistance && !almostEqual(distance1, sameMinDistance) && !(distance1 < EPSILON && distance2 < EPSILON)) { + sameMinDistance = distance1 + sameIntersectPoint = intersect + sameLinePoint = point + samePartner = j + } } }) + // [KERAB-GABLE-JOG-FIX 2026-07-02] 전역 최근접 파트너가 인덱스 미공유(접합 불가)이면, 인덱스 공유 최근접으로 폴백 + if (partner !== null) { + const pLine = linesAnalysis[partner] + const chosenSame = + currLine.left === pLine.left || currLine.left === pLine.right || currLine.right === pLine.left || currLine.right === pLine.right + if (!chosenSame && samePartner !== null) { + minDistance = sameMinDistance + intersectPoint = sameIntersectPoint + linePoint = sameLinePoint + partner = samePartner + } + } if (intersectPoint) { intersections.push({ index: i, intersect: intersectPoint, linePoint, partner }) }