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": "모듈 세로 가운데 정렬",