diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index 73bd6fd9..bfc283c7 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -714,7 +714,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { }, inPolygonImproved(point) { - const vertices = this.points + const vertices = this.getCurrentPoints() let inside = false const testX = Number(point.x.toFixed(this.toFixed)) const testY = Number(point.y.toFixed(this.toFixed)) @@ -726,7 +726,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { const yj = Number(vertices[j].y.toFixed(this.toFixed)) // 점이 정점 위에 있는지 확인 - if (Math.abs(xi - testX) < 0.01 && Math.abs(yi - testY) < 0.01) { + if (Math.abs(xi - testX) <= 0.01 && Math.abs(yi - testY) <= 0.01) { return true } diff --git a/src/hooks/module/useModuleBasicSetting.js b/src/hooks/module/useModuleBasicSetting.js index 933a423e..a0392407 100644 --- a/src/hooks/module/useModuleBasicSetting.js +++ b/src/hooks/module/useModuleBasicSetting.js @@ -15,6 +15,7 @@ import { } from '@/store/canvasAtom' import { calculateVisibleModuleHeight, getDegreeByChon, polygonToTurfPolygon, rectToPolygon, toFixedWithoutRounding } from '@/util/canvas-util' +import '@/util/fabric-extensions' // fabric 객체들에 getCurrentPoints 메서드 추가 import { basicSettingState, roofDisplaySelector } from '@/store/settingAtom' import offsetPolygon, { calculateAngle, createLinesFromPolygon } from '@/util/qpolygon-utils' import { QPolygon } from '@/components/fabric/QPolygon' @@ -265,14 +266,14 @@ export function useModuleBasicSetting(tabNum) { batchObjects.forEach((obj) => { //도머일때 if (obj.name === BATCH_TYPE.TRIANGLE_DORMER || obj.name === BATCH_TYPE.PENTAGON_DORMER) { - const groupPoints = obj.groupPoints + const groupPoints = obj.getCurrentPoints() const offsetObjects = offsetPolygon(groupPoints, 10) const dormerOffset = new QPolygon(offsetObjects, batchObjectOptions) dormerOffset.setViewLengthText(false) canvas.add(dormerOffset) //모듈설치면 만들기 } else { //개구, 그림자일때 - const points = obj.points + const points = obj.getCurrentPoints() const offsetObjects = offsetPolygon(points, 10) const offset = new QPolygon(offsetObjects, batchObjectOptions) offset.setViewLengthText(false) @@ -319,7 +320,7 @@ export function useModuleBasicSetting(tabNum) { const margin = moduleSelectionData.common.margin ? moduleSelectionData.common.margin : 200 //육지붕일때는 그냥 하드코딩 - offsetPoints = offsetPolygon(roof.points, -Number(margin) / 10) //육지붕일때 + offsetPoints = offsetPolygon(roof.getCurrentPoints(), -Number(margin) / 10) //육지붕일때 } else { //육지붕이 아닐때 if (allPointsOutside) { diff --git a/src/hooks/object/useObjectBatch.js b/src/hooks/object/useObjectBatch.js index 959a0798..ef2cf587 100644 --- a/src/hooks/object/useObjectBatch.js +++ b/src/hooks/object/useObjectBatch.js @@ -675,6 +675,8 @@ export function useObjectBatch({ isHidden, setIsHidden }) { } }) + objectGroup.recalculateGroupPoints() + isDown = false initEvent() // dbClickEvent() @@ -1426,7 +1428,7 @@ export function useObjectBatch({ isHidden, setIsHidden }) { //그림자는 아무데나 설치 할 수 있게 해달라고 함 if (obj.name === BATCH_TYPE.OPENING) { - const turfObject = pointsToTurfPolygon(obj.points) + const turfObject = pointsToTurfPolygon(obj.getCurrentPoints()) if (turf.booleanWithin(turfObject, turfSurface)) { obj.set({ @@ -1459,7 +1461,7 @@ export function useObjectBatch({ isHidden, setIsHidden }) { const calcLeft = obj.left - originLeft const calcTop = obj.top - originTop - const currentDormerPoints = obj.groupPoints.map((item) => { + const currentDormerPoints = obj.getCurrentPoints().map((item) => { return { x: item.x + calcLeft, y: item.y + calcTop, diff --git a/src/hooks/surface/useSurfaceShapeBatch.js b/src/hooks/surface/useSurfaceShapeBatch.js index 1d0445d8..e42a7025 100644 --- a/src/hooks/surface/useSurfaceShapeBatch.js +++ b/src/hooks/surface/useSurfaceShapeBatch.js @@ -1439,44 +1439,89 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) { const rotateSurfaceShapeBatch = () => { if (currentObject) { - // 기존 관련 객체들 제거 - const relatedObjects = canvas - .getObjects() - .filter( - (obj) => - obj.parentId === currentObject.id || - (obj.name === 'lengthText' && obj.parentId === currentObject.id) || - (obj.name === 'arrow' && obj.parentId === currentObject.id), - ) - relatedObjects.forEach((obj) => canvas.remove(obj)) - - // 현재 회전값에 90도 추가 - const currentAngle = currentObject.angle || 0 - const newAngle = (currentAngle + 90) % 360 - const originWidth = currentObject.originWidth - const originHeight = currentObject.originHeight - // 회전 적용 (width/height 교체 제거로 도형 깨짐 방지) - currentObject.rotate(newAngle) - - // QPolygon 내부 구조 재구성 (선이 깨지는 문제 해결) - if (currentObject.type === 'QPolygon' && currentObject.lines) { - currentObject.initLines() + // 관련 객체들 찾기 + // arrow는 제거 + const arrow = canvas.getObjects().find((obj) => obj.parentId === currentObject.id && obj.name === 'arrow') + if (arrow) { + canvas.remove(arrow) } - currentObject.set({ - originWidth: originHeight, - originHeight: originWidth, + const relatedObjects = canvas.getObjects().filter((obj) => obj.parentId === currentObject.id) + + // 그룹화할 객체들 배열 (currentObject + relatedObjects) + const objectsToGroup = [currentObject, ...relatedObjects] + + // 기존 객체들을 캔버스에서 제거 + objectsToGroup.forEach((obj) => canvas.remove(obj)) + + // fabric.Group 생성 + const group = new fabric.Group(objectsToGroup, { + originX: 'center', + originY: 'center', }) - currentObject.setCoords() - currentObject.fire('modified') + // 그룹을 캔버스에 추가 + canvas.add(group) + // 현재 회전값에 90도 추가 + const currentAngle = group.angle || 0 + const newAngle = (currentAngle + 90) % 360 + + // 그룹 전체를 회전 + group.rotate(newAngle) + group.setCoords() + + // 그룹을 해제하고 개별 객체로 복원 + group._restoreObjectsState() + canvas.remove(group) + + // 개별 객체들을 다시 캔버스에 추가하고 처리 + group.getObjects().forEach((obj) => { + canvas.add(obj) + obj.setCoords() + + // currentObject인 경우 추가 처리 + if (obj.id === currentObject.id) { + const originWidth = obj.originWidth + const originHeight = obj.originHeight + + // QPolygon 내부 구조 재구성 (선이 깨지는 문제 해결) + if (obj.type === 'QPolygon' && obj.lines) { + obj.initLines() + } + + obj.set({ + originWidth: originHeight, + originHeight: originWidth, + }) + } else { + // relatedObject인 경우에도 필요한 처리 + if (obj.type === 'QPolygon' && obj.lines) { + obj.initLines() + } + if (obj.type === 'group') { + // 회전 후의 points를 groupPoints로 업데이트 + // getCurrentPoints를 직접 호출하지 말고 recalculateGroupPoints만 실행 + + obj.recalculateGroupPoints() + + obj._objects?.forEach((obj) => { + obj.initLines() + obj.fire('modified') + }) + } + } + }) + currentObject.fire('modified') // 화살표와 선 다시 그리기 drawDirectionArrow(currentObject) setTimeout(() => { setPolygonLinesActualSize(currentObject) changeSurfaceLineType(currentObject) - }, 200) + }, 500) + + // currentObject를 다시 선택 상태로 설정 + canvas.setActiveObject(currentObject) canvas.renderAll() } } diff --git a/src/util/fabric-extensions.js b/src/util/fabric-extensions.js new file mode 100644 index 00000000..9410f764 --- /dev/null +++ b/src/util/fabric-extensions.js @@ -0,0 +1,232 @@ +import { fabric } from 'fabric' + +/** + * fabric.Rect에 getCurrentPoints 메서드를 추가 + * QPolygon의 getCurrentPoints와 동일한 방식으로 변형된 현재 점들을 반환 + */ +fabric.Rect.prototype.getCurrentPoints = function () { + // 사각형의 네 모서리 점들을 계산 + const width = this.width + const height = this.height + + // 사각형의 로컬 좌표계에서의 네 모서리 점 + const points = [ + { x: -width / 2, y: -height / 2 }, // 좌상단 + { x: width / 2, y: -height / 2 }, // 우상단 + { x: width / 2, y: height / 2 }, // 우하단 + { x: -width / 2, y: height / 2 }, // 좌하단 + ] + + // 변형 매트릭스 계산 + const matrix = this.calcTransformMatrix() + + // 각 점을 변형 매트릭스로 변환 + return points.map(function (p) { + const point = new fabric.Point(p.x, p.y) + return fabric.util.transformPoint(point, matrix) + }) +} + +/** + * fabric.Group에 getCurrentPoints 메서드를 추가 (도머 그룹용) + * 그룹의 groupPoints를 다시 계산하여 반환 + */ +fabric.Group.prototype.getCurrentPoints = function () { + // groupPoints를 다시 계산 + + // 그룹에 groupPoints가 있으면 해당 점들을 사용 (도머의 경우) + if (this.groupPoints && Array.isArray(this.groupPoints)) { + const matrix = this.calcTransformMatrix() + console.log('this.groupPoints', this.groupPoints) + return this.groupPoints.map(function (p) { + const point = new fabric.Point(p.x, p.y) + return fabric.util.transformPoint(point, matrix) + }) + } + + // groupPoints가 없으면 바운딩 박스를 사용 + const bounds = this.getBoundingRect() + const points = [ + { x: bounds.left, y: bounds.top }, + { x: bounds.left + bounds.width, y: bounds.top }, + { x: bounds.left + bounds.width, y: bounds.top + bounds.height }, + { x: bounds.left, y: bounds.top + bounds.height }, + ] + + return points.map(function (p) { + return new fabric.Point(p.x, p.y) + }) +} + +/** + * fabric.Group에 groupPoints 재계산 메서드 추가 + * 그룹 내 모든 객체의 점들을 기반으로 groupPoints를 새로 계산 + * Convex Hull 알고리즘을 사용하여 가장 외곽의 점들만 반환 + */ +fabric.Group.prototype.recalculateGroupPoints = function () { + if (!this._objects || this._objects.length === 0) { + return + } + + let allPoints = [] + + // 그룹 내 모든 객체의 점들을 수집 + this._objects.forEach(function (obj) { + if (obj.getCurrentPoints && typeof obj.getCurrentPoints === 'function') { + // getCurrentPoints가 있는 객체는 해당 메서드 사용 + const objPoints = obj.getCurrentPoints() + allPoints = allPoints.concat(objPoints) + } else if (obj.points && Array.isArray(obj.points)) { + // QPolygon과 같이 points 배열이 있는 경우 + const pathOffset = obj.pathOffset || { x: 0, y: 0 } + const matrix = obj.calcTransformMatrix() + const transformedPoints = obj.points + .map(function (p) { + return new fabric.Point(p.x - pathOffset.x, p.y - pathOffset.y) + }) + .map(function (p) { + return fabric.util.transformPoint(p, matrix) + }) + allPoints = allPoints.concat(transformedPoints) + } else { + // 일반 객체는 바운딩 박스의 네 모서리 점 사용 + const bounds = obj.getBoundingRect() + const cornerPoints = [ + { x: bounds.left, y: bounds.top }, + { x: bounds.left + bounds.width, y: bounds.top }, + { x: bounds.left + bounds.width, y: bounds.top + bounds.height }, + { x: bounds.left, y: bounds.top + bounds.height }, + ] + allPoints = allPoints.concat( + cornerPoints.map(function (p) { + return new fabric.Point(p.x, p.y) + }), + ) + } + }) + + if (allPoints.length > 0) { + // Convex Hull 알고리즘을 사용하여 외곽 점들만 추출 + const convexHullPoints = this.getConvexHull(allPoints) + + // 그룹의 로컬 좌표계로 변환하기 위해 그룹의 역변환 적용 + const groupMatrix = this.calcTransformMatrix() + const invertedMatrix = fabric.util.invertTransform(groupMatrix) + + this.groupPoints = convexHullPoints.map(function (p) { + const localPoint = fabric.util.transformPoint(p, invertedMatrix) + return { x: localPoint.x, y: localPoint.y } + }) + } +} + +/** + * Graham Scan 알고리즘을 사용한 Convex Hull 계산 + * 점들의 집합에서 가장 외곽의 점들만 반환 + */ +fabric.Group.prototype.getConvexHull = function (points) { + if (points.length < 3) return points + + // 중복 점 제거 + const uniquePoints = [] + const seen = new Set() + + points.forEach(function (p) { + const key = `${Math.round(p.x * 10) / 10},${Math.round(p.y * 10) / 10}` + if (!seen.has(key)) { + seen.add(key) + uniquePoints.push({ x: p.x, y: p.y }) + } + }) + + if (uniquePoints.length < 3) return uniquePoints + + // 가장 아래쪽 점을 찾기 (y가 가장 작고, 같으면 x가 가장 작은 점) + let pivot = uniquePoints[0] + for (let i = 1; i < uniquePoints.length; i++) { + if (uniquePoints[i].y < pivot.y || (uniquePoints[i].y === pivot.y && uniquePoints[i].x < pivot.x)) { + pivot = uniquePoints[i] + } + } + + // 극각에 따라 정렬 + const sortedPoints = uniquePoints + .filter(function (p) { return p !== pivot }) + .sort(function (a, b) { + const angleA = Math.atan2(a.y - pivot.y, a.x - pivot.x) + const angleB = Math.atan2(b.y - pivot.y, b.x - pivot.x) + if (angleA !== angleB) return angleA - angleB + + // 각도가 같으면 거리로 정렬 + const distA = Math.pow(a.x - pivot.x, 2) + Math.pow(a.y - pivot.y, 2) + const distB = Math.pow(b.x - pivot.x, 2) + Math.pow(b.y - pivot.y, 2) + return distA - distB + }) + + // Graham Scan 실행 + const hull = [pivot] + + for (let i = 0; i < sortedPoints.length; i++) { + const current = sortedPoints[i] + + // 반시계방향이 아닌 점들 제거 + while (hull.length > 1) { + const p1 = hull[hull.length - 2] + const p2 = hull[hull.length - 1] + const cross = (p2.x - p1.x) * (current.y - p1.y) - (p2.y - p1.y) * (current.x - p1.x) + + if (cross > 0) break // 반시계방향이면 유지 + hull.pop() // 시계방향이면 제거 + } + + hull.push(current) + } + + return hull +} + +/** + * fabric.Triangle에 getCurrentPoints 메서드를 추가 + * 삼각형의 세 꼭짓점을 반환 + */ +fabric.Triangle.prototype.getCurrentPoints = function () { + const width = this.width + const height = this.height + + // 삼각형의 로컬 좌표계에서의 세 꼭짓점 + const points = [ + { x: 0, y: -height / 2 }, // 상단 중앙 + { x: -width / 2, y: height / 2 }, // 좌하단 + { x: width / 2, y: height / 2 }, // 우하단 + ] + + // 변형 매트릭스 계산 + const matrix = this.calcTransformMatrix() + + // 각 점을 변형 매트릭스로 변환 + return points.map(function (p) { + const point = new fabric.Point(p.x, p.y) + return fabric.util.transformPoint(point, matrix) + }) +} + +/** + * fabric.Polygon에 getCurrentPoints 메서드를 추가 (QPolygon이 아닌 일반 Polygon용) + * QPolygon과 동일한 방식으로 구현 + */ +if (!fabric.Polygon.prototype.getCurrentPoints) { + fabric.Polygon.prototype.getCurrentPoints = function () { + const pathOffset = this.get('pathOffset') || { x: 0, y: 0 } + const matrix = this.calcTransformMatrix() + + return this.get('points') + .map(function (p) { + return new fabric.Point(p.x - pathOffset.x, p.y - pathOffset.y) + }) + .map(function (p) { + return fabric.util.transformPoint(p, matrix) + }) + } +} + +export default {}