From 7973c654b5b45bb645191eb124901ad70da8d787 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Fri, 30 Jan 2026 11:16:18 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A7=80=EB=B6=95=EB=A9=B4=20=EB=B3=B5?= =?UTF-8?q?=EC=82=AC=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?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/common/useCommonUtils.js | 189 +++++++++++++++++++++++++---- 1 file changed, 163 insertions(+), 26 deletions(-) diff --git a/src/hooks/common/useCommonUtils.js b/src/hooks/common/useCommonUtils.js index 9fe5a221..0b028f32 100644 --- a/src/hooks/common/useCommonUtils.js +++ b/src/hooks/common/useCommonUtils.js @@ -13,6 +13,7 @@ import { usePolygon } from '@/hooks/usePolygon' import { useObjectBatch } from '@/hooks/object/useObjectBatch' import { BATCH_TYPE } from '@/common/common' import { useMouse } from '@/hooks/useMouse' +import { QPolygon } from '@/components/fabric/QPolygon' export function useCommonUtils() { const canvas = useRecoilValue(canvasState) @@ -617,6 +618,168 @@ export function useCommonUtils() { const buttonAct = dormerName == BATCH_TYPE.TRIANGLE_DORMER ? 3 : 4 applyDormers(dormerParams, buttonAct) + } else if (obj.name === 'roof' && obj.type === 'QPolygon') { + // roof(QPolygon) 객체는 순환 참조(lines[].parent -> polygon)로 인해 + // fabric.clone() 사용 시 Maximum call stack size exceeded 에러 발생 + // getCurrentPoints()를 사용하여 새 QPolygon을 직접 생성 + + // 원본 객체의 line attributes 복사 (순환 참조 제거) + const lineAttributes = obj.lines.map((line) => ({ + type: line.attributes?.type, + offset: line.attributes?.offset, + actualSize: line.attributes?.actualSize, + planeSize: line.attributes?.planeSize, + })) + + // 원본 roof의 자식 오브젝트들 찾기 (개구, 그림자, 도머 등) + const childObjectTypes = [BATCH_TYPE.OPENING, BATCH_TYPE.SHADOW, BATCH_TYPE.TRIANGLE_DORMER, BATCH_TYPE.PENTAGON_DORMER] + const childObjects = canvas.getObjects().filter( + (o) => o.parentId === obj.id && childObjectTypes.includes(o.name) + ) + + // 원본 roof 중심점 계산 + const originalPoints = obj.getCurrentPoints() + const originalCenterX = originalPoints.reduce((sum, p) => sum + p.x, 0) / originalPoints.length + const originalCenterY = originalPoints.reduce((sum, p) => sum + p.y, 0) / originalPoints.length + + let clonedObj = null + let clonedChildren = [] + + addCanvasMouseEventListener('mouse:move', (e) => { + const pointer = canvas?.getPointer(e.e) + + // 이전 임시 객체들 제거 + canvas + .getObjects() + .filter((o) => o.name === 'clonedObj' || o.name === 'clonedChildTemp') + .forEach((o) => canvas?.remove(o)) + + // 새 QPolygon 생성 (매 move마다 생성하여 위치 업데이트) + const currentPoints = obj.getCurrentPoints() + const centerX = currentPoints.reduce((sum, p) => sum + p.x, 0) / currentPoints.length + const centerY = currentPoints.reduce((sum, p) => sum + p.y, 0) / currentPoints.length + + // 이동 오프셋 계산 + const offsetX = pointer.x - centerX + const offsetY = pointer.y - centerY + + // 포인터 위치로 이동된 새 points 계산 + const newPoints = currentPoints.map((p) => ({ + x: p.x + offsetX, + y: p.y + offsetY, + })) + + clonedObj = new QPolygon(newPoints, { + fill: obj.fill || 'transparent', + stroke: obj.stroke || 'black', + strokeWidth: obj.strokeWidth || 1, + fontSize: 0, // 이동 중에는 lengthText 생성하지 않음 (fontSize=0이면 addLengthText가 스킵됨) + selectable: true, + lockMovementX: true, + lockMovementY: true, + lockRotation: true, + lockScalingX: true, + lockScalingY: true, + name: 'clonedObj', + originX: 'center', + originY: 'center', + pitch: obj.pitch, + surfaceId: obj.surfaceId, + sort: false, + }, canvas) + + canvas.add(clonedObj) + + // 자식 오브젝트들도 이동해서 미리보기 표시 + clonedChildren = [] + childObjects.forEach((child) => { + child.clone((clonedChild) => { + clonedChild.set({ + left: child.left + offsetX, + top: child.top + offsetY, + name: 'clonedChildTemp', + selectable: false, + evented: false, + }) + clonedChildren.push({ original: child, cloned: clonedChild }) + canvas.add(clonedChild) + }) + }) + + canvas.renderAll() + }) + + addCanvasMouseEventListener('mouse:down', (e) => { + if (!clonedObj) return + + const newRoofId = uuidv4() + + clonedObj.set({ + lockMovementX: true, + lockMovementY: true, + name: 'roof', + editable: false, + selectable: true, + id: newRoofId, + direction: obj.direction, + directionText: obj.directionText, + roofMaterial: obj.roofMaterial, + stroke: 'black', + evented: true, + isFixed: false, + fontSize: lengthTextFont.fontSize.value, // 최종 확정 시 fontSize 설정 + }) + + // line attributes 복원 + lineAttributes.forEach((attr, index) => { + if (clonedObj.lines[index]) { + clonedObj.lines[index].set({ attributes: attr }) + } + }) + + // 임시 자식 오브젝트들 제거 + canvas + .getObjects() + .filter((o) => o.name === 'clonedChildTemp') + .forEach((o) => canvas?.remove(o)) + + // 자식 오브젝트들 최종 복사 (새 roof의 id를 parentId로 설정) + const pointer = canvas?.getPointer(e.e) + const currentPoints = obj.getCurrentPoints() + const centerX = currentPoints.reduce((sum, p) => sum + p.x, 0) / currentPoints.length + const centerY = currentPoints.reduce((sum, p) => sum + p.y, 0) / currentPoints.length + const offsetX = pointer.x - centerX + const offsetY = pointer.y - centerY + + childObjects.forEach((child) => { + child.clone((clonedChild) => { + clonedChild.set({ + left: child.left + offsetX, + top: child.top + offsetY, + id: uuidv4(), + parentId: newRoofId, // 새 roof의 id를 부모로 설정 + name: child.name, + selectable: true, + evented: true, + }) + // 그룹 객체인 경우 groupId도 새로 설정 + if (clonedChild.type === 'group') { + clonedChild.set({ groupId: uuidv4() }) + } + canvas.add(clonedChild) + }) + }) + + clonedObj.fire('polygonMoved') + clonedObj.fire('modified') + clonedObj.setCoords() + canvas.setActiveObject(clonedObj) + canvas.renderAll() + addLengthText(clonedObj) // fontSize가 설정된 후 lengthText 추가 + drawDirectionArrow(clonedObj) + + initEvent() + }) } else { let clonedObj = null @@ -655,32 +818,6 @@ export function useCommonUtils() { //객체가 그룹일 경우에는 그룹 아이디를 따로 넣어준다 if (clonedObj.type === 'group') clonedObj.set({ groupId: uuidv4() }) - //배치면일 경우 - if (obj.name === 'roof') { - clonedObj.canvas = canvas // canvas 참조 설정 - clonedObj.set({ - direction: obj.direction, - directionText: obj.directionText, - roofMaterial: obj.roofMaterial, - stroke: 'black', // 복사된 객체는 선택 해제 상태의 색상으로 설정 - selectable: true, // 선택 가능하도록 설정 - evented: true, // 마우스 이벤트를 받을 수 있도록 설정 - isFixed: false, // containsPoint에서 특별 처리 방지 - }) - - obj.lines.forEach((line, index) => { - clonedObj.lines[index].set({ attributes: line.attributes }) - }) - - clonedObj.fire('polygonMoved') // 내부 좌표 재계산 (points, pathOffset) - clonedObj.fire('modified') - clonedObj.setCoords() // 모든 속성 설정 후 좌표 업데이트 - canvas.setActiveObject(clonedObj) - canvas.renderAll() - addLengthText(clonedObj) //수치 추가 - drawDirectionArrow(clonedObj) //방향 화살표 추가 - } - initEvent() }) }