From 0d32f84ad71419d97af4b2d71ca3933d0a37e7a9 Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 2 Jul 2026 09:41:07 +0900 Subject: [PATCH] =?UTF-8?q?[1514=5F2]=20=E3=85=97=ED=98=95=20=EB=B0=95?= =?UTF-8?q?=EA=B3=B5=20=EB=A7=88=EB=A3=A8=20=EB=AF=B8=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=E2=80=94=20overlap-clamp=20=EB=81=8A?= =?UTF-8?q?=EA=B8=B4=20inner=20=EB=81=9D=EC=A0=90=EC=9D=84=20=ED=9E=99=20s?= =?UTF-8?q?eed=20=EA=B5=90=EC=A0=90=EA=B9=8C=EC=A7=80=20=EC=97=B0=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/qpolygon-utils.js | 235 ++++++++----------------------------- 1 file changed, 47 insertions(+), 188 deletions(-) diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index 30c98824..85244457 100644 --- a/src/util/qpolygon-utils.js +++ b/src/util/qpolygon-utils.js @@ -6,7 +6,6 @@ import { QPolygon } from '@/components/fabric/QPolygon' import { LINE_TYPE, POLYGON_TYPE } from '@/common/common' import Big from 'big.js' import * as turf from '@turf/turf' -// import { debugCapture } from '@/util/debugCapture' // [KERAB-GABLE-DIAG 2026-06-29] 박공 마루 미생성 추적용 const TWO_PI = Math.PI * 2 const EPSILON = 1e-10 //좌표계산 시 최소 차이값 @@ -3596,14 +3595,8 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => { const prevIndex = baseLines.findIndex((line) => line === prevLine) const nextIndex = baseLines.findIndex((line) => line === nextLine) - // [KERAB-GABLE-DIAG 2026-06-29] ① 이벤트: 박공 변 처리 진입 - // const __gcoord = `(${Math.round(currentLine.x1)},${Math.round(currentLine.y1)})-(${Math.round(currentLine.x2)},${Math.round(currentLine.y2)})` - // debugCapture.log('KERAB-GABLE-DIAG ①이벤트:gable-enter', { gable: __gcoord, prevType: prevLine?.attributes?.type, nextType: nextLine?.attributes?.type }) - //양옆이 처마가 아닐때 패스 if (prevLine.attributes.type !== LINE_TYPE.WALLLINE.EAVES || nextLine.attributes.type !== LINE_TYPE.WALLLINE.EAVES) { - // [KERAB-GABLE-DIAG 2026-06-29] ②원인→④결과: 양옆이 처마가 아님 → 마루 미생성 - // debugCapture.log('KERAB-GABLE-DIAG ②원인:neighbor-not-eaves → ④결과:no-ridge', { gable: __gcoord }) return } @@ -3615,11 +3608,6 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => { y: (currentLine.y1 + currentLine.y2) / 2 + Math.sign(nextLine.y2 - nextLine.y1), } - // [KERAB-GABLE-DIAG 2026-06-29] ②원인: 본처리 진입 게이트(vector 차이 + inPolygon) 통과 여부 - // const __vecDiff = prevLineVector.x !== nextLineVector.x || prevLineVector.y !== nextLineVector.y - // const __inPoly = checkWallPolygon.inPolygon(inPolygonPoint) - // debugCapture.log('KERAB-GABLE-DIAG ②원인:gate-vec+inPoly', { gable: __gcoord, vecDiff: __vecDiff, inPoly: __inPoly, inPolygonPoint }) - //좌우 라인이 서로 다른방향일때 if ((prevLineVector.x !== nextLineVector.x || prevLineVector.y !== nextLineVector.y) && checkWallPolygon.inPolygon(inPolygonPoint)) { const analyze = analyzeLine(currentLine) @@ -3933,13 +3921,6 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => { } }) - // [KERAB-GABLE-DIAG 2026-06-29] ②원인: 반대편 평행변(oppositeLine) 매칭 — 0이면 마루 미생성 - // debugCapture.log('KERAB-GABLE-DIAG ②원인:oppositeLine-match', { - // gable: __gcoord, - // count: oppositeLine.length, - // lines: oppositeLine.map((o) => `(${Math.round(o.line.x1)},${Math.round(o.line.y1)})-(${Math.round(o.line.x2)},${Math.round(o.line.y2)})`), - // }) - if (oppositeLine.length > 0) { const ridgePoints = [] @@ -4201,14 +4182,6 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => { if (stdAnalyze.isHorizontal) { if (overlapMinX > overlapMaxX) { - // [KERAB-GABLE-DIAG 2026-06-29] ③분기→④결과: 수평 박공 X구간 겹침 없음 → 이 oppositeLine skip(no-ridge) - // debugCapture.log('KERAB-GABLE-DIAG ③분기:horizontal-no-overlap → ④결과:no-ridge', { - // gable: __gcoord, - // overlapMinX, - // overlapMaxX, - // stdRange: [stdMinX, stdMaxX], - // ridgeRange: [rMinX, rMaxX], - // }) return } const dist1 = Math.abs(ridgePoint[0] - overlapMinX) @@ -4226,14 +4199,6 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => { } if (stdAnalyze.isVertical) { if (overlapMinY > overlapMaxY) { - // [KERAB-GABLE-DIAG 2026-06-29] ③분기→④결과: 수직 박공 Y구간 겹침 없음 → 이 oppositeLine skip(no-ridge) - // debugCapture.log('KERAB-GABLE-DIAG ③분기:vertical-no-overlap → ④결과:no-ridge', { - // gable: __gcoord, - // overlapMinY, - // overlapMaxY, - // stdRange: [stdMinY, stdMaxY], - // ridgeRange: [rMinY, rMaxY], - // }) return } const dist1 = Math.abs(ridgePoint[1] - overlapMinY) @@ -4312,6 +4277,53 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => { } } + // [KERAB-GABLE-DIAG-FIX 2026-07-02] 박공 마루 inner 끝점이 roofLine 에도 힙 seed 에도 안 닿고 폴리곤 내부에 떠 있으면 + // (ㅗ/ㄱ 등 비대칭 팔에서 overlap-clamp 가 마루를 짧게 끊어 메인 루프 교점 상대가 없어지는 문제) + // 마루 진행 방향으로 연장해 가장 가까운 힙 seed 교점까지 끝점을 이동시켜, 이후 교점 루프가 마루를 처리하도록 한다. + { + const onRoofLine = (px, py) => roof.lines.find((line) => isPointOnLineNew(line, { x: px, y: py })) !== undefined + const onSeedHip = (px, py) => + linesAnalysis.some( + (l) => l.type === TYPES.HIP && isPointOnLineNew({ x1: l.start.x, y1: l.start.y, x2: l.end.x, y2: l.end.y }, { x: px, y: py }), + ) + const p1Floating = !onRoofLine(point[0], point[1]) && !onSeedHip(point[0], point[1]) + const p2Floating = !onRoofLine(point[2], point[3]) && !onSeedHip(point[2], point[3]) + // 박공 마루는 한쪽 끝이 항상 처마출폭(roofLine) 위에 있으므로 한쪽만 떠 있을 때만 연장한다. + if (p1Floating !== p2Floating) { + const fx = p1Floating ? 0 : 2 + const fy = p1Floating ? 1 : 3 + const bx = p1Floating ? 2 : 0 + const by = p1Floating ? 3 : 1 + const dir = { x: Math.sign(point[fx] - point[bx]), y: Math.sign(point[fy] - point[by]) } + const rayEdge = { + vertex1: { x: point[bx], y: point[by] }, + vertex2: { x: point[fx] + dir.x * 100000, y: point[fy] + dir.y * 100000 }, + } + const floatDist = Math.hypot(point[fx] - point[bx], point[fy] - point[by]) + let best = null + let bestDist = Infinity + linesAnalysis + .filter((l) => l.type === TYPES.HIP) + .forEach((l) => { + const hipEdge = { vertex1: { x: l.start.x, y: l.start.y }, vertex2: { x: l.end.x, y: l.end.y } } + const is = edgesIntersection(rayEdge, hipEdge) + if (!is) return + const isVec = { x: Math.sign(is.x - point[bx]), y: Math.sign(is.y - point[by]) } + if (isVec.x !== dir.x || isVec.y !== dir.y) return + const d = Math.hypot(is.x - point[bx], is.y - point[by]) + if (d <= floatDist) return + if (d < bestDist) { + bestDist = d + best = is + } + }) + if (best) { + point[fx] = best.x + point[fy] = best.y + } + } + } + const ridgeLength = Math.sqrt(Math.pow(point[2] - point[0], 2) + Math.pow(point[3] - point[1], 2)) if (ridgeLength > EPSILON) { @@ -4350,19 +4362,7 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => { type: TYPES.RIDGE, degree: 0, }) - // [KERAB-GABLE-DIAG 2026-06-29] ④결과: 박공 마루 가선분 생성 성공 — left/right 는 이후 교점 매칭의 핵심 단서 - // debugCapture.log('KERAB-GABLE-DIAG ④결과:ridge-pushed', { - // gable: __gcoord, - // ridgeLength, - // left: prevIndex, - // right: nextIndex, - // start: startPoint, - // end: endPoint, - // }) } - } else { - // [KERAB-GABLE-DIAG 2026-06-29] ③분기→④결과: 마루 길이 ~0 → 가선분 미생성 - // debugCapture.log('KERAB-GABLE-DIAG ③분기:ridge-too-short → ④결과:no-ridge', { gable: __gcoord, ridgeLength, point }) } }) } @@ -4370,8 +4370,6 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => { } //반절마루 생성불가이므로 지붕선 분기를 해야하는지 확인 후 처리. if (prevLineVector.x === nextLineVector.x && prevLineVector.y === nextLineVector.y) { - // [KERAB-GABLE-DIAG 2026-06-29] ③분기: 양옆 벡터 동일(U자) → 반절마루 불가, GABLE_LINE 분기로 - // debugCapture.log('KERAB-GABLE-DIAG ③분기:U-shape-gableline', { gable: __gcoord }) const analyze = analyzeLine(currentLine) const roofLine = analyze.roofLine @@ -4434,24 +4432,6 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => { while (linesAnalysis.length > 0 && iterations < MAX_ITERATIONS) { iterations++ - // [KERAB-GABLE-DIAG 2026-06-29] ①이벤트: 교점 루프 iteration 시작 — RIDGE 현재 상태(붕괴 추적) - // { - // const __r = linesAnalysis.find((l) => l.type === TYPES.RIDGE) - // debugCapture.log('KERAB-GABLE-DIAG ①이벤트:loop-iter-start', { - // iter: iterations, - // total: linesAnalysis.length, - // ridge: __r - // ? { - // start: { x: Math.round(__r.start.x * 10) / 10, y: Math.round(__r.start.y * 10) / 10 }, - // end: { x: Math.round(__r.end.x * 10) / 10, y: Math.round(__r.end.y * 10) / 10 }, - // len: Math.round(Math.hypot(__r.end.x - __r.start.x, __r.end.y - __r.start.y) * 10) / 10, - // left: __r.left, - // right: __r.right, - // } - // : null, - // }) - // } - const intersections = [] linesAnalysis.forEach((currLine, i) => { let minDistance = Infinity @@ -4503,24 +4483,6 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => { } }) - // [KERAB-GABLE-DIAG 2026-06-29] ②원인: 이번 iter 에서 각 RIDGE 가 교점쌍(partner)을 찾았는지 — 못 찾으면 루프 미처리되어 잔여로 떨어진다 - // linesAnalysis.forEach((l, li) => { - // if (l.type !== TYPES.RIDGE) return - // const found = intersections.find((it) => it.index === li) - // const partnerLine = found ? linesAnalysis[found.partner] : null - // debugCapture.log('KERAB-GABLE-DIAG ②원인:loop-ridge-intersect-scan', { - // iter: iterations, - // ridge: { - // start: { x: Math.round(l.start.x * 10) / 10, y: Math.round(l.start.y * 10) / 10 }, - // end: { x: Math.round(l.end.x * 10) / 10, y: Math.round(l.end.y * 10) / 10 }, - // share: [l.left, l.right], - // }, - // hasIntersect: !!found, - // partner: partnerLine ? { type: partnerLine.type, share: [partnerLine.left, partnerLine.right] } : null, - // intersect: found ? { x: Math.round(found.intersect.x * 10) / 10, y: Math.round(found.intersect.y * 10) / 10 } : null, - // }) - // }) - const intersectPoints = intersections .map((item) => item.intersect) .filter((point, index, self) => self.findIndex((p) => almostEqual(p.x, point.x) && almostEqual(p.y, point.y)) === index) @@ -4536,78 +4498,31 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => { for (const { index, intersect, linePoint, partner } of intersections) { const cLine = linesAnalysis[index] const pLine = linesAnalysis[partner] - // [KERAB-GABLE-DIAG 2026-06-29] RIDGE 가 낀 교점쌍이 어느 게이트에서 탈락하는지 추적용 - // const __ridgePair = cLine?.type === TYPES.RIDGE || pLine?.type === TYPES.RIDGE - //교점이 없거나, 이미 처리된 선분이면 처리하지 않는다. if (!intersect || processed.has(index)) { - // if (__ridgePair) - // debugCapture.log('KERAB-GABLE-DIAG ③분기:loop-skip → ④결과:skipped', { - // iter: iterations, - // reason: !intersect ? 'no-intersect' : 'already-processed', - // cType: cLine?.type, - // pType: pLine?.type, - // }) continue } //교점이 없거나, 교점 선분이 없으면 처리하지 않는다. const pIntersection = intersections.find((p) => p.index === partner) if (!pIntersection || !pIntersection.intersect) { - // if (__ridgePair) - // debugCapture.log('KERAB-GABLE-DIAG ③분기:loop-skip → ④결과:skipped', { - // iter: iterations, - // reason: 'partner-no-intersect', - // cType: cLine?.type, - // pType: pLine?.type, - // }) continue } //상호 최단 교점이 아닐경우 처리하지 않는다. if (pIntersection.partner !== index) { - // if (__ridgePair) - // debugCapture.log('KERAB-GABLE-DIAG ③분기:loop-skip → ④결과:skipped', { - // iter: iterations, - // reason: 'not-mutual-shortest', - // cType: cLine?.type, - // pType: pLine?.type, - // pPartner: pIntersection.partner, - // index, - // }) continue } //공통선분이 없으면 처리하지 않는다. const isSameLine = cLine.left === pLine.left || cLine.left === pLine.right || cLine.right === pLine.left || cLine.right === pLine.right if (!isSameLine) { - // if (__ridgePair) - // debugCapture.log('KERAB-GABLE-DIAG ③분기:loop-skip → ④결과:skipped', { - // iter: iterations, - // reason: 'no-common-line', - // cType: cLine?.type, - // pType: pLine?.type, - // cShare: [cLine.left, cLine.right], - // pShare: [pLine.left, pLine.right], - // }) continue } //처리 된 라인으로 설정 processed.add(index).add(partner) - // [KERAB-GABLE-DIAG 2026-06-29] ②원인: 이번 교점쌍에 RIDGE 포함 — 어떤 partner 와 어디서 만났나 - if (cLine.type === TYPES.RIDGE || pLine.type === TYPES.RIDGE) { - // debugCapture.log('KERAB-GABLE-DIAG ②원인:loop-pair-has-ridge', { - // iter: iterations, - // cType: cLine.type, - // pType: pLine.type, - // intersect: { x: Math.round(intersect.x * 10) / 10, y: Math.round(intersect.y * 10) / 10 }, - // cShare: [cLine.left, cLine.right], - // pShare: [pLine.left, pLine.right], - // }) - } - let point1 = linePoint let point2 = pIntersection.linePoint let length1 = Math.sqrt(Math.pow(point1[2] - point1[0], 2) + Math.pow(point1[3] - point1[1], 2)) @@ -4623,22 +4538,10 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => { length1 = Math.sqrt((point1[2] - point1[0]) ** 2 + (point1[3] - point1[1]) ** 2) } - // [KERAB-GABLE-DIAG 2026-06-29] ③분기: RIDGE(c) 그리기 게이트 판정(length1>0 && !중복) - if (cLine.type === TYPES.RIDGE) { - // debugCapture.log('KERAB-GABLE-DIAG ③분기:loop-ridge(c)-draw-gate', { - // iter: iterations, - // length1: Math.round(length1 * 10) / 10, - // already: alreadyPoints(innerLines, point1), - // willDraw: length1 > 0 && !alreadyPoints(innerLines, point1), - // point: point1.map((v) => Math.round(v * 10) / 10), - // }) - } if (length1 > 0 && !alreadyPoints(innerLines, point1)) { if (cLine.type === TYPES.HIP) { innerLines.push(drawHipLine(point1, canvas, roof, textMode, cLine.degree, cLine.degree)) } else if (cLine.type === TYPES.RIDGE) { - // [KERAB-GABLE-DIAG 2026-06-29] ④결과: RIDGE(c) 실제로 그려짐 - // debugCapture.log('KERAB-GABLE-DIAG ④결과:loop-ridge(c)-drawn', { iter: iterations, point: point1.map((v) => Math.round(v * 10) / 10) }) innerLines.push(drawRidgeLine(point1, canvas, roof, textMode)) } else if (cLine.type === TYPES.NEW) { const isDiagonal = Math.abs(point1[0] - point1[2]) >= 1 && Math.abs(point1[1] - point1[3]) >= 1 @@ -4656,22 +4559,10 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => { } } - // [KERAB-GABLE-DIAG 2026-06-29] ③분기: RIDGE(p) 그리기 게이트 판정(length2>0 && !중복) - if (pLine.type === TYPES.RIDGE) { - // debugCapture.log('KERAB-GABLE-DIAG ③분기:loop-ridge(p)-draw-gate', { - // iter: iterations, - // length2: Math.round(length2 * 10) / 10, - // already: alreadyPoints(innerLines, point2), - // willDraw: length2 > 0 && !alreadyPoints(innerLines, point2), - // point: point2.map((v) => Math.round(v * 10) / 10), - // }) - } if (length2 > 0 && !alreadyPoints(innerLines, point2)) { if (pLine.type === TYPES.HIP) { innerLines.push(drawHipLine(point2, canvas, roof, textMode, pLine.degree, pLine.degree)) } else if (pLine.type === TYPES.RIDGE) { - // [KERAB-GABLE-DIAG 2026-06-29] ④결과: RIDGE(p) 실제로 그려짐 - // debugCapture.log('KERAB-GABLE-DIAG ④결과:loop-ridge(p)-drawn', { iter: iterations, point: point2.map((v) => Math.round(v * 10) / 10) }) innerLines.push(drawRidgeLine(point2, canvas, roof, textMode)) } else if (pLine.type === TYPES.NEW) { const isDiagonal = Math.abs(point2[0] - point2[2]) >= 1 && Math.abs(point2[1] - point2[3]) >= 1 @@ -4908,23 +4799,6 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => { // 처리된 가선분 제외 linesAnalysis = newAnalysis.concat(linesAnalysis.filter((_, index) => !processed.has(index))) - // [KERAB-GABLE-DIAG 2026-06-29] ④결과: iteration 종료 후 RIDGE 상태(붕괴/소멸 여부) - // { - // const __r = linesAnalysis.find((l) => l.type === TYPES.RIDGE) - // debugCapture.log('KERAB-GABLE-DIAG ④결과:loop-iter-end', { - // iter: iterations, - // newAnalysisCount: newAnalysis.length, - // processedCount: processed.size, - // ridge: __r - // ? { - // start: { x: Math.round(__r.start.x * 10) / 10, y: Math.round(__r.start.y * 10) / 10 }, - // end: { x: Math.round(__r.end.x * 10) / 10, y: Math.round(__r.end.y * 10) / 10 }, - // len: Math.round(Math.hypot(__r.end.x - __r.start.x, __r.end.y - __r.start.y) * 10) / 10, - // } - // : 'GONE', - // }) - // } - canvas .getObjects() .filter((object) => object.name === 'check' || object.name === 'checkAnalysis') @@ -4933,21 +4807,6 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => { if (newAnalysis.length === 0) break } - // [KERAB-GABLE-DIAG 2026-06-29] ①이벤트: 교점 루프 종료 — 잔여 linesAnalysis 덤프(이 시점에 RIDGE 가 남아있는지가 핵심) - // debugCapture.log('KERAB-GABLE-DIAG ①이벤트:after-loop-residual', { - // baseLinesLen: baseLines.length, - // iterations, - // count: linesAnalysis.length, - // lines: linesAnalysis.map((l) => ({ - // type: l.type, - // degree: l.degree, - // left: l.left, - // right: l.right, - // start: { x: Math.round(l.start.x * 10) / 10, y: Math.round(l.start.y * 10) / 10 }, - // end: { x: Math.round(l.end.x * 10) / 10, y: Math.round(l.end.y * 10) / 10 }, - // })), - // }) - // 가선분 중 처리가 안되어있는 붙어있는 라인에 대한 예외처리. const proceedAnalysis = [] linesAnalysis