diff --git a/src/components/floor-plan/modal/auxiliary/AuxiliarySize.jsx b/src/components/floor-plan/modal/auxiliary/AuxiliarySize.jsx index 40fb9f76..6b0abda1 100644 --- a/src/components/floor-plan/modal/auxiliary/AuxiliarySize.jsx +++ b/src/components/floor-plan/modal/auxiliary/AuxiliarySize.jsx @@ -84,12 +84,12 @@ export default function AuxiliarySize(props) { //1지점 선택일때는 2지점의 좌표, 2지점 선택일때는 1지점의 좌표를 조정한다. if (checkedRadio === 1) { - const newX2 = Big(x1).plus(dx.times(scaleFactor)) - const newY2 = Big(y1).plus(dy.times(scaleFactor)) + const newX2 = Big(x1).plus(dx.times(scaleFactor)).toNumber() + const newY2 = Big(y1).plus(dy.times(scaleFactor)).toNumber() currentObject.set({ x2: newX2, y2: newY2 }) } else if (checkedRadio === 2) { - const newX1 = Big(x2).minus(dx.times(scaleFactor)) - const newY1 = Big(y2).minus(dy.times(scaleFactor)) + const newX1 = Big(x2).minus(dx.times(scaleFactor)).toNumber() + const newY1 = Big(y2).minus(dy.times(scaleFactor)).toNumber() currentObject.set({ x1: newX1, y1: newY1 }) } //planeSize와 actualSize를 재계산한다. diff --git a/src/hooks/module/useModule.js b/src/hooks/module/useModule.js index c81c54cb..d4a0e7de 100644 --- a/src/hooks/module/useModule.js +++ b/src/hooks/module/useModule.js @@ -1,4 +1,5 @@ import { BATCH_TYPE, POLYGON_TYPE } from '@/common/common' +import { QPolygon } from '@/components/fabric/QPolygon' import { canvasState } from '@/store/canvasAtom' import { polygonToTurfPolygon } from '@/util/canvas-util' import { useRecoilValue } from 'recoil' @@ -44,6 +45,42 @@ export function useModule() { const { clear: removeTrestleMaterials } = useTrestle() const { checkModuleDisjointSurface } = useTurf() + // clone() 대신 직접 새 QPolygon을 생성하여 Maximum call stack 방지 + const createModuleCopy = (module, newLeft, newTop, overrides = {}) => { + const deltaX = newLeft - module.left + const deltaY = newTop - module.top + const newPoints = module.points.map((p) => ({ + x: p.x + deltaX, + y: p.y + deltaY, + })) + return new QPolygon(newPoints, { + fill: module.fill, + stroke: module.stroke, + strokeWidth: module.strokeWidth, + opacity: module.opacity, + selectable: module.selectable, + lockMovementX: module.lockMovementX, + lockMovementY: module.lockMovementY, + lockRotation: module.lockRotation, + lockScalingX: module.lockScalingX, + lockScalingY: module.lockScalingY, + parentId: module.parentId, + initOptions: module.initOptions, + direction: module.direction, + arrow: module.arrow, + name: module.name, + surfaceId: module.surfaceId, + moduleInfo: module.moduleInfo, + left: newLeft, + top: newTop, + width: module.width, + height: module.height, + toFixed: 2, + sort: false, + ...overrides, + }) + } + const moduleMove = (length, direction) => { const selectedObj = canvas.getActiveObjects() //선택된 객체들을 가져옴 const selectedIds = selectedObj.map((obj) => obj.id) // selectedObj의 ID 추출 @@ -256,27 +293,10 @@ export function useModule() { modules.forEach((module) => { const { top, left } = getPosotion(module, direction, Number(length) + Number(moduleLength) * 10, false) - module.clone((obj) => { - obj.set({ - parentId: module.parentId, - initOptions: module.initOptions, - direction: module.direction, - arrow: module.arrow, - name: module.name, - type: module.type, - length: module.length, - points: module.points, - surfaceId: module.surfaceId, - moduleInfo: module.moduleInfo, - left, - top, - id: uuidv4(), - }) - copyModule = obj - canvas.add(obj) - copyModules.push(obj) - obj.setCoords() - }) + copyModule = createModuleCopy(module, left, top) + canvas.add(copyModule) + copyModules.push(copyModule) + copyModule.setCoords() if (isOverlapObjects(copyModule, objects) || isOutsideSurface(copyModule, surface)) { isWarning = true copyModule.set({ fill: 'red' }) @@ -318,27 +338,10 @@ export function useModule() { canvas.discardActiveObject() //선택해제 modules.forEach((module) => { const { top, left } = getPosotion(module, direction, length, true) - module.clone((obj) => { - obj.set({ - parentId: module.parentId, - initOptions: module.initOptions, - direction: module.direction, - arrow: module.arrow, - name: module.name, - type: module.type, - length: module.length, - points: module.points, - surfaceId: module.surfaceId, - moduleInfo: module.moduleInfo, - left, - top, - id: uuidv4(), - }) - copyModules.push(obj) - copyModule = obj - canvas.add(obj) - canvas.renderAll() - }) + copyModule = createModuleCopy(module, left, top) + copyModules.push(copyModule) + canvas.add(copyModule) + canvas.renderAll() if ( isOverlapOtherModules(copyModule, otherModules) || @@ -412,29 +415,10 @@ export function useModule() { modules.forEach((module) => { const { top, left } = getPosotion(module, direction, Number(length), true) - module.clone((obj) => { - obj.set({ - parentId: module.parentId, - initOptions: module.initOptions, - stroke: 'black', - direction: module.direction, - arrow: module.arrow, - name: module.name, - type: module.type, - length: module.length, - points: module.points, - moduleInfo: module.moduleInfo, - surfaceId: module.surfaceId, - left, - top, - id: uuidv4(), - }) - copyModule = obj - canvas.add(obj) - copyModules.push(obj) - obj.setCoords() - console.log(obj) - }) + copyModule = createModuleCopy(module, left, top, { stroke: 'black' }) + canvas.add(copyModule) + copyModules.push(copyModule) + copyModule.setCoords() if ( isOverlapOtherModules(copyModule, otherModules) || isOverlapObjects(copyModule, objects) || @@ -770,28 +754,10 @@ export function useModule() { otherModules = getOtherModules(columnModules) columnModules.forEach((module) => { const { top, left } = getPosotion(module, type, moduleIntvlHor, true) - let copyModule = null - module.clone((obj) => { - obj.set({ - parentId: module.parentId, - initOptions: module.initOptions, - direction: module.direction, - arrow: module.arrow, - name: module.name, - type: module.type, - length: module.length, - points: module.points, - moduleInfo: module.moduleInfo, - surfaceId: module.surfaceId, - left, - top, - id: uuidv4(), - }) - copyModule = obj - canvas.add(obj) - copyModules.push(obj) - obj.setCoords() - }) + const copyModule = createModuleCopy(module, left, top) + canvas.add(copyModule) + copyModules.push(copyModule) + copyModule.setCoords() canvas.renderAll() if ( @@ -879,29 +845,10 @@ export function useModule() { otherModules = getOtherModules(rowModules) rowModules.forEach((module) => { const { top, left } = getPosotion(module, type, moduleIntvlVer, true) - let copyModule = null - module.clone((obj) => { - obj.set({ - parentId: module.parentId, - initOptions: module.initOptions, - direction: module.direction, - arrow: module.arrow, - name: module.name, - type: module.type, - length: module.length, - points: module.points, - surfaceId: module.surfaceId, - moduleInfo: module.moduleInfo, - fill: module.fill, - left, - top, - id: uuidv4(), - }) - copyModule = obj - canvas.add(obj) - copyModules.push(obj) - obj.setCoords() - }) + const copyModule = createModuleCopy(module, left, top) + canvas.add(copyModule) + copyModules.push(copyModule) + copyModule.setCoords() canvas.renderAll() if ( diff --git a/src/hooks/module/useModuleBasicSetting.js b/src/hooks/module/useModuleBasicSetting.js index 365ed1b0..c48c2d21 100644 --- a/src/hooks/module/useModuleBasicSetting.js +++ b/src/hooks/module/useModuleBasicSetting.js @@ -184,14 +184,14 @@ export function useModuleBasicSetting(tabNum) { // 방향에 따른 좌표 설정 const directionConfig = { - south: { coord1: 'y1', coord2: 'y2', findExtreme: Math.max }, - north: { coord1: 'y1', coord2: 'y2', findExtreme: Math.min }, - east: { coord1: 'x1', coord2: 'x2', findExtreme: Math.max }, - west: { coord1: 'x1', coord2: 'x2', findExtreme: Math.min }, + south: { coord1: 'y1', coord2: 'y2', findExtreme: Math.max, offsetX: 0, offsetY: 1 }, + north: { coord1: 'y1', coord2: 'y2', findExtreme: Math.min, offsetX: 0, offsetY: -1 }, + east: { coord1: 'x1', coord2: 'x2', findExtreme: Math.max, offsetX: 1, offsetY: 0 }, + west: { coord1: 'x1', coord2: 'x2', findExtreme: Math.min, offsetX: -1, offsetY: 0 }, } const config = directionConfig[direction] || directionConfig.south - const { coord1, coord2, findExtreme } = config + const { coord1, coord2, offsetX, offsetY } = config // 2. 직선만 필터링 (대각선 제외) // 남/북: y1 === y2 인 경우 수평 직선 @@ -200,22 +200,29 @@ export function useModuleBasicSetting(tabNum) { if (straightLines.length === 0) return - // 3. 가장 끝에 있는 직선 찾기 - // 남쪽: 가장 하단 (y값이 가장 큰), 북쪽: 가장 상단 (y값이 가장 작은) - // 동쪽: 가장 오른쪽 (x값이 가장 큰), 서쪽: 가장 왼쪽 (x값이 가장 작은) - const extremeValue = findExtreme(...straightLines.map((line) => line[coord1])) - const eavesLines = straightLines.filter((line) => line[coord1] === extremeValue) + // 3. 폴리곤의 turf polygon 생성 (처마 방향 판별용) + const points = polygon.getCurrentPoints() + const turfCoords = points.map((p) => [p.x, p.y]) + turfCoords.push(turfCoords[0]) // 폴리곤 닫기 + const turfPolygon = turf.polygon([turfCoords]) - // 4. 직선에 대해 타입 설정 + // 4. 각 직선에 대해 처마/용마루 판별 + // 라인 중점에서 처마 방향으로 약간 이동한 점이 폴리곤 외부이면 처마(eaves) + // 폴리곤 내부이면 용마루(ridge) straightLines.forEach((line) => { - if (eavesLines.includes(line)) { - // 가장 끝에 있는 직선은 eaves + const midX = (line.x1 + line.x2) / 2 + const midY = (line.y1 + line.y2) / 2 + // 처마 방향으로 약간 이동한 테스트 포인트 + const testPoint = turf.point([midX + offsetX * 0.5, midY + offsetY * 0.5]) + + if (!turf.booleanPointInPolygon(testPoint, turfPolygon)) { + // 테스트 포인트가 폴리곤 외부 → 처마 방향을 향함 line.attributes = { ...line.attributes, type: LINE_TYPE.WALLLINE.EAVES, } } else { - // 나머지 직선은 ridge + // 테스트 포인트가 폴리곤 내부 → 용마루 방향을 향함 line.attributes = { ...line.attributes, type: LINE_TYPE.SUBLINE.RIDGE, diff --git a/src/hooks/surface/useSurfaceShapeBatch.js b/src/hooks/surface/useSurfaceShapeBatch.js index d7df9b6d..27a6f737 100644 --- a/src/hooks/surface/useSurfaceShapeBatch.js +++ b/src/hooks/surface/useSurfaceShapeBatch.js @@ -208,6 +208,13 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) { isXInversion: xInversion, isYInversion: yInversion, }) + // 최초 생성 시 planeSize를 계산된 길이로 저장 (회전 후 좌표 반올림에 의한 오차 방지) + batchSurface.lines.forEach((line) => { + if (!line.attributes.planeSize || line.attributes.planeSize === 0) { + line.attributes.planeSize = line.getLength() + } + }) + canvas.setActiveObject(batchSurface) setSurfaceShapePattern(batchSurface, roofDisplay.column) drawDirectionArrow(batchSurface) @@ -375,9 +382,9 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) { } points = [ - { x: pointer.x, y: pointer.y - parseInt(newLength2) / 2 }, - { x: pointer.x - parseInt(length1) / 2, y: pointer.y + parseInt(newLength2) / 2 }, - { x: pointer.x + parseInt(length1) / 2, y: pointer.y + parseInt(newLength2) / 2 }, + { x: pointer.x, y: pointer.y - newLength2 / 2 }, + { x: pointer.x - length1 / 2, y: pointer.y + newLength2 / 2 }, + { x: pointer.x + length1 / 2, y: pointer.y + newLength2 / 2 }, ] break diff --git a/src/hooks/usePolygon.js b/src/hooks/usePolygon.js index 40a3d85f..3a83c947 100644 --- a/src/hooks/usePolygon.js +++ b/src/hooks/usePolygon.js @@ -1023,6 +1023,9 @@ export const usePolygon = () => { const line = divideLines[i] const { intersections, startPoint, endPoint } = line + // 원본 라인의 기하학적 길이 (비율 계산용) + const originalGeomLength = Math.round(Math.hypot(line.x2 - line.x1, line.y2 - line.y1)) * 10 + if (intersections.length === 1) { const newLinePoint1 = [line.x1, line.y1, intersections[0].x, intersections[0].y] const newLinePoint2 = [intersections[0].x, intersections[0].y, line.x2, line.y2] @@ -1042,25 +1045,43 @@ export const usePolygon = () => { name: 'newLine', }) - // 두 라인 중 큰 길이로 통일 + // 분할된 각 세그먼트의 기하학적 길이 const length1 = Math.round(Math.hypot(newLine1.x1 - newLine1.x2, newLine1.y1 - newLine1.y2)) * 10 const length2 = Math.round(Math.hypot(newLine2.x1 - newLine2.x2, newLine2.y1 - newLine2.y2)) * 10 - const maxLength = Math.max(length1, length2) - const unifiedPlaneSize = line.attributes.planeSize ?? maxLength - const unifiedActualSize = line.attributes.actualSize ?? maxLength + + // 원본에 planeSize/actualSize가 있으면 비율로 분배, 없으면 기하학적 길이 사용 + let planeSize1, planeSize2, actualSize1, actualSize2 + if (line.attributes.planeSize && originalGeomLength > 0) { + const ratio1 = length1 / originalGeomLength + const ratio2 = length2 / originalGeomLength + planeSize1 = Math.round(line.attributes.planeSize * ratio1) + planeSize2 = Math.round(line.attributes.planeSize * ratio2) + } else { + planeSize1 = length1 + planeSize2 = length2 + } + if (line.attributes.actualSize && originalGeomLength > 0) { + const ratio1 = length1 / originalGeomLength + const ratio2 = length2 / originalGeomLength + actualSize1 = Math.round(line.attributes.actualSize * ratio1) + actualSize2 = Math.round(line.attributes.actualSize * ratio2) + } else { + actualSize1 = length1 + actualSize2 = length2 + } newLine1.attributes = { ...line.attributes, - planeSize: unifiedPlaneSize, - actualSize: unifiedActualSize, + planeSize: planeSize1, + actualSize: actualSize1, } - newLine1.length = maxLength + newLine1.length = length1 newLine2.attributes = { ...line.attributes, - planeSize: unifiedPlaneSize, - actualSize: unifiedActualSize, + planeSize: planeSize2, + actualSize: actualSize2, } - newLine2.length = maxLength + newLine2.length = length2 newLines.push(newLine1, newLine2) divideLines.splice(i, 1) // 기존 line 제거 @@ -1080,12 +1101,25 @@ export const usePolygon = () => { }) const calcLength = Math.round(Math.hypot(newLine.x1 - newLine.x2, newLine.y1 - newLine.y2)) * 10 + + let segPlaneSize, segActualSize + if (line.attributes.planeSize && originalGeomLength > 0) { + segPlaneSize = Math.round(line.attributes.planeSize * (calcLength / originalGeomLength)) + } else { + segPlaneSize = calcLength + } + if (line.attributes.actualSize && originalGeomLength > 0) { + segActualSize = Math.round(line.attributes.actualSize * (calcLength / originalGeomLength)) + } else { + segActualSize = calcLength + } + newLine.attributes = { ...line.attributes, - planeSize: line.attributes.planeSize ?? calcLength, - actualSize: line.attributes.actualSize ?? calcLength, + planeSize: segPlaneSize, + actualSize: segActualSize, } - newLine.length = line.attributes.planeSize ?? calcLength + newLine.length = calcLength newLines.push(newLine) currentPoint = minDistancePoint @@ -1100,12 +1134,25 @@ export const usePolygon = () => { name: 'newLine', }) const lastCalcLength = Math.round(Math.hypot(newLine.x1 - newLine.x2, newLine.y1 - newLine.y2)) * 10 + + let lastPlaneSize, lastActualSize + if (line.attributes.planeSize && originalGeomLength > 0) { + lastPlaneSize = Math.round(line.attributes.planeSize * (lastCalcLength / originalGeomLength)) + } else { + lastPlaneSize = lastCalcLength + } + if (line.attributes.actualSize && originalGeomLength > 0) { + lastActualSize = Math.round(line.attributes.actualSize * (lastCalcLength / originalGeomLength)) + } else { + lastActualSize = lastCalcLength + } + newLine.attributes = { ...line.attributes, - planeSize: line.attributes.planeSize ?? lastCalcLength, - actualSize: line.attributes.actualSize ?? lastCalcLength, + planeSize: lastPlaneSize, + actualSize: lastActualSize, } - newLine.length = line.attributes.planeSize ?? lastCalcLength + newLine.length = lastCalcLength newLines.push(newLine) divideLines.splice(i, 1) // 기존 line 제거