From 7d9b6d5225ed3f297cb5ce5d91ae700c4d278593 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Mon, 15 Sep 2025 14:55:04 +0900 Subject: [PATCH] =?UTF-8?q?object=20=ED=9A=8C=EC=A0=84=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/common.js | 2 + src/components/fabric/QPolygon.js | 6 + .../floor-plan/modal/object/SizeSetting.jsx | 10 +- src/hooks/surface/useSurfaceShapeBatch.js | 236 +++++++++++++----- src/hooks/useContextMenu.js | 17 +- src/locales/ja.json | 1 + src/locales/ko.json | 1 + 7 files changed, 203 insertions(+), 70 deletions(-) diff --git a/src/common/common.js b/src/common/common.js index 9d15e988..757dc0e8 100644 --- a/src/common/common.js +++ b/src/common/common.js @@ -216,6 +216,8 @@ export const SAVE_KEY = [ 'isMultipleOf45', 'from', 'originColor', + 'originWidth', + 'originHeight', ] export const OBJECT_PROTOTYPE = [fabric.Line.prototype, fabric.Polygon.prototype, fabric.Triangle.prototype, fabric.Group.prototype] diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index b50cdaa3..73bd6fd9 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -87,6 +87,10 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { this.initLines() this.init() this.setShape() + const originWidth = this.originWidth ?? this.width + const originHeight = this.originHeight ?? this.height + this.originWidth = this.angle === 90 || this.angle === 270 ? originHeight : originWidth + this.originHeight = this.angle === 90 || this.angle === 270 ? originWidth : originHeight }, setShape() { @@ -126,11 +130,13 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { this.on('moving', () => { this.initLines() this.addLengthText() + this.setCoords() }) this.on('modified', (e) => { this.initLines() this.addLengthText() + this.setCoords() }) this.on('selected', () => { diff --git a/src/components/floor-plan/modal/object/SizeSetting.jsx b/src/components/floor-plan/modal/object/SizeSetting.jsx index c5873006..b4a7d3e7 100644 --- a/src/components/floor-plan/modal/object/SizeSetting.jsx +++ b/src/components/floor-plan/modal/object/SizeSetting.jsx @@ -12,7 +12,7 @@ import { useSurfaceShapeBatch } from '@/hooks/surface/useSurfaceShapeBatch' export default function SizeSetting(props) { const contextPopupPosition = useRecoilValue(contextPopupPositionState) - const [settingTarget, setSettingTarget] = useState(1) + const [settingTarget, setSettingTarget] = useState(props.side || 1) const { id, pos = contextPopupPosition, target } = props const { getMessage } = useMessage() const { closePopup } = usePopup() @@ -47,11 +47,11 @@ export default function SizeSetting(props) {
- + mm
- + mm
@@ -60,11 +60,11 @@ export default function SizeSetting(props) {
- + mm
- + mm
diff --git a/src/hooks/surface/useSurfaceShapeBatch.js b/src/hooks/surface/useSurfaceShapeBatch.js index 874526f8..1d0445d8 100644 --- a/src/hooks/surface/useSurfaceShapeBatch.js +++ b/src/hooks/surface/useSurfaceShapeBatch.js @@ -1,9 +1,9 @@ 'use client' import { useRecoilValue, useResetRecoilState } from 'recoil' -import { canvasSettingState, canvasState, currentCanvasPlanState, globalPitchState } from '@/store/canvasAtom' +import { canvasSettingState, canvasState, currentCanvasPlanState, currentObjectState, globalPitchState } from '@/store/canvasAtom' import { LINE_TYPE, MENU, POLYGON_TYPE } from '@/common/common' -import { getIntersectionPoint, toFixedWithoutRounding } from '@/util/canvas-util' +import { getIntersectionPoint } from '@/util/canvas-util' import { degreesToRadians } from '@turf/turf' import { QPolygon } from '@/components/fabric/QPolygon' import { useSwal } from '@/hooks/useSwal' @@ -21,10 +21,13 @@ import { placementShapeDrawingPointsState } from '@/store/placementShapeDrawingA import { getBackGroundImage } from '@/lib/imageActions' import { useCanvasSetting } from '@/hooks/option/useCanvasSetting' import { useText } from '@/hooks/useText' +import SizeSetting from '@/components/floor-plan/modal/object/SizeSetting' +import { v4 as uuidv4 } from 'uuid' +import { useState } from 'react' export function useSurfaceShapeBatch({ isHidden, setIsHidden }) { const { getMessage } = useMessage() - const { drawDirectionArrow, addPolygon, addLengthText } = usePolygon() + const { drawDirectionArrow, addPolygon, addLengthText, setPolygonLinesActualSize } = usePolygon() const lengthTextFont = useRecoilValue(fontSelector('lengthText')) const resetOuterLinePoints = useResetRecoilState(outerLinePointsState) const resetPlacementShapeDrawingPoints = useResetRecoilState(placementShapeDrawingPointsState) @@ -36,11 +39,13 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) { const { swalFire } = useSwal() const { addCanvasMouseEventListener, initEvent } = useEvent() // const { addCanvasMouseEventListener, initEvent } = useContext(EventContext) - const { addPopup, closePopup } = usePopup() + const { addPopup, closePopup, closeAll } = usePopup() const { setSurfaceShapePattern } = useRoofFn() const { changeCorridorDimensionText } = useText() const currentCanvasPlan = useRecoilValue(currentCanvasPlanState) const { fetchSettings } = useCanvasSetting(false) + const currentObject = useRecoilValue(currentObjectState) + const [popupId, setPopupId] = useState(uuidv4()) const applySurfaceShape = (surfaceRefs, selectedType, id) => { let length1, length2, length3, length4, length5 @@ -879,6 +884,7 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) { drawDirectionArrow(roof) changeCorridorDimensionText() addLengthText(roof) + roof.setCoords() initEvent() canvas.renderAll() }) @@ -916,71 +922,138 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) { } const resizeSurfaceShapeBatch = (side, target, width, height) => { - const originTarget = { ...target } + if (!target || target.type !== 'QPolygon') return - const objectWidth = target.width - const objectHeight = target.height - const changeWidth = width / 10 / objectWidth - const changeHeight = height / 10 / objectHeight - let sideX = 'left' - let sideY = 'top' + width = width / 10 + height = height / 10 - //그룹 중심점 변경 - if (side === 2) { - sideX = 'right' - sideY = 'top' - } else if (side === 3) { - sideX = 'left' - sideY = 'bottom' - } else if (side === 4) { - sideX = 'right' - sideY = 'bottom' + // 현재 QPolygon의 점들 가져오기 (변형 적용된 실제 좌표) + const currentPoints = target.getCurrentPoints() || [] + const angle = target.angle % 360 + if (currentPoints.length === 0) return + + // 현재 바운딩 박스 계산 + let minX = Math.min(...currentPoints.map((p) => p.x)) + let maxX = Math.max(...currentPoints.map((p) => p.x)) + let minY = Math.min(...currentPoints.map((p) => p.y)) + let maxY = Math.max(...currentPoints.map((p) => p.y)) + + let currentWidth = maxX - minX + let currentHeight = maxY - minY + + // 회전에 관계없이 단순한 앵커 포인트 계산 + let anchorX, anchorY + switch (side) { + case 1: // left-top + anchorX = minX + anchorY = minY + break + case 2: // right-top + anchorX = maxX + anchorY = minY + break + case 3: // left-bottom + anchorX = minX + anchorY = maxY + break + case 4: // right-bottom + anchorX = maxX + anchorY = maxY + break + default: + return } - //변경 전 좌표 - const newCoords = target.getPointByOrigin(sideX, sideY) + // 목표 크기 (회전에 관계없이 동일하게 적용) + let targetWidth = width + let targetHeight = height - target.set({ - originX: sideX, - originY: sideY, - left: newCoords.x, - top: newCoords.y, + // 새로운 점들 계산 - 앵커 포인트는 고정, 나머지는 비례적으로 확장 + // 각도와 side에 따라 확장 방향 결정 + const newPoints = currentPoints.map((point) => { + // 앵커 포인트 기준으로 새로운 위치 계산 + // side와 각도에 관계없이 일관된 방식으로 처리 + + // 앵커 포인트에서 각 점까지의 절대 거리 + const deltaX = point.x - anchorX + const deltaY = point.y - anchorY + + // 새로운 크기에 맞춰 비례적으로 확장 + const newDeltaX = (deltaX / currentWidth) * targetWidth + const newDeltaY = (deltaY / currentHeight) * targetHeight + + const newX = anchorX + newDeltaX + const newY = anchorY + newDeltaY + + return { + x: newX, // 소수점 1자리로 반올림 + y: newY, + } }) - target.scaleX = changeWidth - target.scaleY = changeHeight - - const currentPoints = target.getCurrentPoints() - - target.set({ + // 기존 객체의 속성들을 복사 (scale은 1로 고정) + const originalOptions = { + stroke: target.stroke, + strokeWidth: target.strokeWidth, + fill: target.fill, + opacity: target.opacity, + visible: target.visible, + selectable: target.selectable, + evented: target.evented, + hoverCursor: target.hoverCursor, + moveCursor: target.moveCursor, + lockMovementX: target.lockMovementX, + lockMovementY: target.lockMovementY, + lockRotation: target.lockRotation, + lockScalingX: target.lockScalingX, + lockScalingY: target.lockScalingY, + lockUniScaling: target.lockUniScaling, + name: target.name, + uuid: target.uuid, + roofType: target.roofType, + roofMaterial: target.roofMaterial, + azimuth: target.azimuth, + // tilt: target.tilt, + // angle: target.angle, + // scale은 항상 1로 고정 scaleX: 1, scaleY: 1, - width: toFixedWithoutRounding(width / 10, 1), - height: toFixedWithoutRounding(height / 10, 1), - }) - //크기 변경후 좌표를 재 적용 - const changedCoords = target.getPointByOrigin(originTarget.originX, originTarget.originY) - - target.set({ - originX: originTarget.originX, - originY: originTarget.originY, - left: changedCoords.x, - top: changedCoords.y, - }) - canvas.renderAll() - - //면형상 리사이즈시에만 - target.fire('polygonMoved') - target.points = currentPoints - target.fire('modified') - - setSurfaceShapePattern(target, roofDisplay.column, false, target.roofMaterial, true) - - if (target.direction) { - drawDirectionArrow(target) + lines: target.lines, + // 기타 모든 사용자 정의 속성들 + ...Object.fromEntries( + Object.entries(target).filter( + ([key, value]) => + !['type', 'left', 'top', 'width', 'height', 'scaleX', 'scaleY', 'points', 'lines', 'texts', 'canvas', 'angle', 'tilt'].includes(key) && + typeof value !== 'function', + ), + ), } - target.setCoords() - canvas.renderAll() + + // 기존 QPolygon 제거 + canvas.remove(target) + + // 새로운 QPolygon 생성 (scale은 1로 고정됨) + const newPolygon = new QPolygon(newPoints, originalOptions, canvas) + + newPolygon.set({ + originWidth: width, + originHeight: height, + }) + + // 캔버스에 추가 + canvas.add(newPolygon) + + // 선택 상태 유지 + canvas.setActiveObject(newPolygon) + + newPolygon.fire('modified') + setSurfaceShapePattern(newPolygon, null, null, newPolygon.roofMaterial) + drawDirectionArrow(newPolygon) + newPolygon.setCoords() + changeSurfaceLineType(newPolygon) + canvas?.renderAll() + closeAll() + addPopup(popupId, 1, ) } const changeSurfaceLinePropertyEvent = () => { @@ -1364,6 +1437,50 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) { return orderedPoints } + 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() + } + + currentObject.set({ + originWidth: originHeight, + originHeight: originWidth, + }) + + currentObject.setCoords() + currentObject.fire('modified') + + // 화살표와 선 다시 그리기 + drawDirectionArrow(currentObject) + setTimeout(() => { + setPolygonLinesActualSize(currentObject) + changeSurfaceLineType(currentObject) + }, 200) + canvas.renderAll() + } + } + return { applySurfaceShape, deleteAllSurfacesAndObjects, @@ -1373,5 +1490,6 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) { changeSurfaceLineProperty, changeSurfaceLinePropertyReset, changeSurfaceLineType, + rotateSurfaceShapeBatch, } } diff --git a/src/hooks/useContextMenu.js b/src/hooks/useContextMenu.js index fe6d800e..4580f3ee 100644 --- a/src/hooks/useContextMenu.js +++ b/src/hooks/useContextMenu.js @@ -1,7 +1,6 @@ import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil' import { canvasSettingState, canvasState, currentMenuState, currentObjectState } from '@/store/canvasAtom' import { useEffect, useState } from 'react' -import { MENU, POLYGON_TYPE } from '@/common/common' import AuxiliarySize from '@/components/floor-plan/modal/auxiliary/AuxiliarySize' import { usePopup } from '@/hooks/usePopup' import { v4 as uuidv4 } from 'uuid' @@ -12,11 +11,9 @@ import { gridColorState } from '@/store/gridAtom' import { contextPopupPositionState, contextPopupState } from '@/store/popupAtom' import AuxiliaryEdit from '@/components/floor-plan/modal/auxiliary/AuxiliaryEdit' import SizeSetting from '@/components/floor-plan/modal/object/SizeSetting' -import RoofMaterialSetting from '@/components/floor-plan/modal/object/RoofMaterialSetting' import DormerOffset from '@/components/floor-plan/modal/object/DormerOffset' import FontSetting from '@/components/common/font/FontSetting' import RoofAllocationSetting from '@/components/floor-plan/modal/roofAllocation/RoofAllocationSetting' -import LinePropertySetting from '@/components/floor-plan/modal/lineProperty/LinePropertySetting' import FlowDirectionSetting from '@/components/floor-plan/modal/flowDirection/FlowDirectionSetting' import { useCommonUtils } from './common/useCommonUtils' @@ -29,7 +26,6 @@ import ColumnRemove from '@/components/floor-plan/modal/module/column/ColumnRemo import ColumnInsert from '@/components/floor-plan/modal/module/column/ColumnInsert' import RowRemove from '@/components/floor-plan/modal/module/row/RowRemove' import RowInsert from '@/components/floor-plan/modal/module/row/RowInsert' -import CircuitNumberEdit from '@/components/floor-plan/modal/module/CircuitNumberEdit' import { useObjectBatch } from '@/hooks/object/useObjectBatch' import { useSurfaceShapeBatch } from '@/hooks/surface/useSurfaceShapeBatch' import { fontSelector, globalFontAtom } from '@/store/fontAtom' @@ -45,6 +41,8 @@ import PlacementSurfaceLineProperty from '@/components/floor-plan/modal/placemen import { selectedMenuState } from '@/store/menuAtom' import { useTrestle } from './module/useTrestle' import { useCircuitTrestle } from './useCirCuitTrestle' +import { usePolygon } from '@/hooks/usePolygon' +import { useText } from '@/hooks/useText' export function useContextMenu() { const canvas = useRecoilValue(canvasState) @@ -64,7 +62,7 @@ export function useContextMenu() { const [column, setColumn] = useState(null) const { handleZoomClear } = useCanvasEvent() const { moveObjectBatch, copyObjectBatch } = useObjectBatch({}) - const { moveSurfaceShapeBatch } = useSurfaceShapeBatch({}) + const { moveSurfaceShapeBatch, rotateSurfaceShapeBatch } = useSurfaceShapeBatch({}) const [globalFont, setGlobalFont] = useRecoilState(globalFontAtom) const { addLine, removeLine } = useLine() const { removeGrid } = useGrid() @@ -73,10 +71,12 @@ export function useContextMenu() { const { settingsData, setSettingsDataSave } = useCanvasSetting(false) const { swalFire } = useSwal() const { alignModule, modulesRemove, moduleRoofRemove } = useModule() - const { removeRoofMaterial, removeAllRoofMaterial, moveRoofMaterial, removeOuterLines } = useRoofFn() + const { removeRoofMaterial, removeAllRoofMaterial, moveRoofMaterial, removeOuterLines, setSurfaceShapePattern } = useRoofFn() const selectedMenu = useRecoilValue(selectedMenuState) const { isAllComplete, clear: resetModule } = useTrestle() const { isExistCircuit } = useCircuitTrestle() + const { changeCorridorDimensionText } = useText() + const { setPolygonLinesActualSize, drawDirectionArrow } = usePolygon() const currentMenuSetting = () => { switch (selectedMenu) { case 'outline': @@ -170,6 +170,11 @@ export function useContextMenu() { name: getMessage('contextmenu.size.edit'), component: , }, + { + id: 'rotate', + name: `${getMessage('contextmenu.rotate')}`, + fn: () => rotateSurfaceShapeBatch(), + }, { id: 'roofMaterialRemove', shortcut: ['d', 'D'], diff --git a/src/locales/ja.json b/src/locales/ja.json index ac682266..2999728f 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -446,6 +446,7 @@ "contextmenu.remove": "削除", "contextmenu.remove.all": "完全削除", "contextmenu.move": "移動", + "contextmenu.rotate": "回転", "contextmenu.copy": "コピー", "contextmenu.edit": "編集", "contextmenu.module.vertical.align": "モジュールの垂直方向の中央揃え", diff --git a/src/locales/ko.json b/src/locales/ko.json index 0559b28e..4f6601cd 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -446,6 +446,7 @@ "contextmenu.remove": "삭제", "contextmenu.remove.all": "전체 삭제", "contextmenu.move": "이동", + "contextmenu.rotate": "회전", "contextmenu.copy": "복사", "contextmenu.edit": "편집", "contextmenu.module.vertical.align": "모듈 세로 가운데 정렬",