From 5102fa2ec08ae9a878340c3b3dcfc571beeb53f0 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Fri, 11 Jul 2025 14:09:50 +0900 Subject: [PATCH] =?UTF-8?q?polygon=20=EC=84=A0=ED=83=9D=EC=9D=B4=20?= =?UTF-8?q?=EC=9E=98=20=EC=95=88=EB=90=98=EB=8A=94=20=ED=98=84=EC=83=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/fabric/QPolygon.js | 153 +++++++++++++++++++++++++----- 1 file changed, 128 insertions(+), 25 deletions(-) diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index 264d62d1..a1a5a169 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -721,8 +721,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { } // Ray casting 알고리즘 - if (((yi > testY) !== (yj > testY)) && - (testX < (xj - xi) * (testY - yi) / (yj - yi) + xi)) { + if (yi > testY !== yj > testY && testX < ((xj - xi) * (testY - yi)) / (yj - yi) + xi) { inside = !inside } } @@ -739,7 +738,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { // 벡터의 외적을 계산하여 점이 선분 위에 있는지 확인 const crossProduct = Math.abs(dxPoint * dySegment - dyPoint * dxSegment) - + if (crossProduct > tolerance) { return false } @@ -747,7 +746,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { // 점이 선분의 범위 내에 있는지 확인 const dotProduct = dxPoint * dxSegment + dyPoint * dySegment const squaredLength = dxSegment * dxSegment + dySegment * dySegment - + return dotProduct >= 0 && dotProduct <= squaredLength }, setCoords: function () { @@ -773,36 +772,140 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { // 먼저 좌표 업데이트 this.setCoords() - // 캔버스 줌과 viewport transform 고려한 좌표 변환 - let localPoint = point - if (this.canvas) { + // 기본 Fabric.js bounding box 체크 먼저 수행 + if (!this.callSuper('containsPoint', point)) { + return false + } + + // 줌 레벨에 관계없이 안정적인 좌표 변환 + const matrix = this.calcTransformMatrix() + const invertedMatrix = fabric.util.invertTransform(matrix) + + // 캔버스 줌 고려 + let testPoint = point + if (this.canvas && this.canvas.viewportTransform) { const vpt = this.canvas.viewportTransform - if (vpt) { - // viewport transform 역변환 - const inverted = fabric.util.invertTransform(vpt) - localPoint = fabric.util.transformPoint(point, inverted) + const invertedVpt = fabric.util.invertTransform(vpt) + testPoint = fabric.util.transformPoint(point, invertedVpt) + } + + // 오브젝트 좌표계로 변환 + const localPoint = fabric.util.transformPoint(testPoint, invertedMatrix) + + // pathOffset 적용 + const pathOffset = this.get('pathOffset') + const finalPoint = { + x: localPoint.x + pathOffset.x, + y: localPoint.y + pathOffset.y, + } + + // 단순하고 안정적인 point-in-polygon 알고리즘 사용 + return this.simplePointInPolygon(finalPoint, this.get('points')) + }, + + simplePointInPolygon: function (point, polygon) { + const x = point.x + const y = point.y + let inside = false + + for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + const xi = polygon[i].x + const yi = polygon[i].y + const xj = polygon[j].x + const yj = polygon[j].y + + if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) { + inside = !inside } } - // 오브젝트의 transform matrix를 고려한 좌표 변환 - const matrix = this.calcTransformMatrix() - const invertedMatrix = fabric.util.invertTransform(matrix) - const transformedPoint = fabric.util.transformPoint(localPoint, invertedMatrix) + return inside + }, - // pathOffset을 고려한 최종 좌표 계산 - const pathOffset = this.get('pathOffset') - const finalPoint = { - x: transformedPoint.x + pathOffset.x, - y: transformedPoint.y + pathOffset.y, + isPointInPolygonRobust: function (point, polygon) { + const x = point.x + const y = point.y + let inside = false + + // 부동소수점 정밀도를 고려한 허용 오차 + const epsilon = 1e-10 + + for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + const xi = polygon[i].x + const yi = polygon[i].y + const xj = polygon[j].x + const yj = polygon[j].y + + // 점이 정점 위에 있는지 확인 (확장된 허용 오차) + if (Math.abs(xi - x) < 0.5 && Math.abs(yi - y) < 0.5) { + return true + } + + // 점이 선분 위에 있는지 확인 + if (this.isPointOnLineSegment(point, { x: xi, y: yi }, { x: xj, y: yj })) { + return true + } + + // Ray casting 알고리즘 - 개선된 버전 + if (Math.abs(yi - yj) > epsilon) { + const minY = Math.min(yi, yj) + const maxY = Math.max(yi, yj) + + if (y > minY && y <= maxY) { + // 교차점 계산 + const intersectionX = xi + ((y - yi) / (yj - yi)) * (xj - xi) + if (intersectionX > x) { + inside = !inside + } + } + } } - if (this.name === POLYGON_TYPE.ROOF && this.isFixed) { - const isInside = this.inPolygonImproved(finalPoint) - this.set('selectable', isInside) - return isInside + return inside + }, + + isPointOnLineSegment: function (point, lineStart, lineEnd) { + const tolerance = 2.0 // 더 큰 허용 오차 + const x = point.x + const y = point.y + const x1 = lineStart.x + const y1 = lineStart.y + const x2 = lineEnd.x + const y2 = lineEnd.y + + // 선분의 길이가 0인 경우 (점) + if (Math.abs(x2 - x1) < 1e-10 && Math.abs(y2 - y1) < 1e-10) { + return Math.abs(x - x1) < tolerance && Math.abs(y - y1) < tolerance + } + + // 점과 선분 사이의 거리 계산 + const A = x - x1 + const B = y - y1 + const C = x2 - x1 + const D = y2 - y1 + + const dot = A * C + B * D + const lenSq = C * C + D * D + const param = dot / lenSq + + let xx, yy + + if (param < 0 || (x1 === x2 && y1 === y2)) { + xx = x1 + yy = y1 + } else if (param > 1) { + xx = x2 + yy = y2 } else { - return this.inPolygonImproved(finalPoint) + xx = x1 + param * C + yy = y1 + param * D } + + const dx = x - xx + const dy = y - yy + const distance = Math.sqrt(dx * dx + dy * dy) + + return distance < tolerance }, inPolygonABType(x, y, polygon) {