diff --git a/src/components/floor-plan/modal/auxiliary/AuxiliaryEdit.jsx b/src/components/floor-plan/modal/auxiliary/AuxiliaryEdit.jsx
index d7696a84..2f68f3ad 100644
--- a/src/components/floor-plan/modal/auxiliary/AuxiliaryEdit.jsx
+++ b/src/components/floor-plan/modal/auxiliary/AuxiliaryEdit.jsx
@@ -9,6 +9,7 @@ import { useAuxiliaryDrawing } from '@/hooks/roofcover/useAuxiliaryDrawing'
import { useSwal } from '@/hooks/useSwal'
import { normalizeDigits } from '@/util/input-utils'
import { CalculatorInput } from '@/components/common/input/CalcInput'
+import { useUndoRedo } from '@/hooks/useUndoRedo'
export default function AuxiliaryEdit(props) {
const contextPopupPosition = useRecoilValue(contextPopupPositionState)
@@ -22,6 +23,7 @@ export default function AuxiliaryEdit(props) {
const [arrow2, setArrow2] = useState(null)
const currentObject = useRecoilValue(currentObjectState)
const { swalFire } = useSwal()
+ const { saveSnapshot } = useUndoRedo()
const handleSave = () => {
if ((!arrow1 && !arrow2) || (+verticalSize === 0 && +horizonSize === 0)) {
swalFire({ title: getMessage('length.direction.is.required'), type: 'alert' })
@@ -38,6 +40,7 @@ export default function AuxiliaryEdit(props) {
return
}
+ saveSnapshot()
if (type === 'copy') {
if (currentObject) {
copy(
@@ -87,14 +90,12 @@ export default function AuxiliaryEdit(props) {
className={`direction up ${arrow1 === '↑' ? 'act' : ''}`}
onClick={() => {
setArrow1('↑')
- document.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' }))
}}
>
@@ -121,14 +122,12 @@ export default function AuxiliaryEdit(props) {
className={`direction left ${arrow2 === '←' ? 'act' : ''}`}
onClick={() => {
setArrow2('←')
- document.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft' }))
}}
>
diff --git a/src/components/floor-plan/modal/auxiliary/AuxiliarySize.jsx b/src/components/floor-plan/modal/auxiliary/AuxiliarySize.jsx
index 6b0abda1..09091011 100644
--- a/src/components/floor-plan/modal/auxiliary/AuxiliarySize.jsx
+++ b/src/components/floor-plan/modal/auxiliary/AuxiliarySize.jsx
@@ -8,6 +8,7 @@ import { useEffect, useState } from 'react'
import Big from 'big.js'
import { calcLineActualSize, calcLinePlaneSize } from '@/util/qpolygon-utils'
import { normalizeDigits } from '@/util/input-utils'
+import { useUndoRedo } from '@/hooks/useUndoRedo'
import { CalculatorInput } from '@/components/common/input/CalcInput'
export default function AuxiliarySize(props) {
@@ -53,7 +54,9 @@ export default function AuxiliarySize(props) {
}
}
+ const { saveSnapshot } = useUndoRedo()
const handleSave = () => {
+ saveSnapshot()
const { x1, y1, x2, y2 } = currentObject
// 선택한 선분의 planeSize와 actualSize의 사잇각을 구한다.
diff --git a/src/components/floor-plan/modal/module/PanelEdit.jsx b/src/components/floor-plan/modal/module/PanelEdit.jsx
index baf4b101..1c9d0e75 100644
--- a/src/components/floor-plan/modal/module/PanelEdit.jsx
+++ b/src/components/floor-plan/modal/module/PanelEdit.jsx
@@ -8,6 +8,7 @@ import { canvasState } from '@/store/canvasAtom'
import { POLYGON_TYPE } from '@/common/common'
import { useModule } from '@/hooks/module/useModule'
import { useSwal } from '@/hooks/useSwal'
+import { useUndoRedo } from '@/hooks/useUndoRedo'
export const PANEL_EDIT_TYPE = {
MOVE: 'move',
@@ -30,6 +31,7 @@ export default function PanelEdit(props) {
const canvas = useRecoilValue(canvasState)
const { swalFire } = useSwal()
const { moduleMove, moduleCopy, moduleMultiMove, moduleMultiCopy, moduleMoveAll, moduleCopyAll } = useModule()
+ const { saveSnapshot } = useUndoRedo()
useEffect(() => {
if (!canvas) {
const isSetupModules = canvas.getObjects().filter((obj) => obj.name === 'module') // selectedObj에 없는 객체만 필터링
@@ -52,6 +54,7 @@ export default function PanelEdit(props) {
//모듈 이동 적용
const handleApply = () => {
+ saveSnapshot()
if (length <= 0) {
swalFire({
title: getMessage('common.message.please.input.over', [1]),
diff --git a/src/components/floor-plan/modal/module/column/ColumnInsert.jsx b/src/components/floor-plan/modal/module/column/ColumnInsert.jsx
index 0c160656..869e7032 100644
--- a/src/components/floor-plan/modal/module/column/ColumnInsert.jsx
+++ b/src/components/floor-plan/modal/module/column/ColumnInsert.jsx
@@ -1,3 +1,4 @@
+import { useUndoRedo } from '@/hooks/useUndoRedo'
import WithDraggable from '@/components/common/draggable/WithDraggable'
import { useRecoilValue } from 'recoil'
import { contextPopupPositionState } from '@/store/popupAtom'
@@ -14,7 +15,9 @@ export default function ColumnInsert(props) {
const [selectedType, setSelectedType] = useState(MODULE_INSERT_TYPE.LEFT)
const { getMessage } = useMessage()
const { moduleColumnInsert } = useModule()
+ const { saveSnapshot } = useUndoRedo()
const handleApply = () => {
+ saveSnapshot()
moduleColumnInsert(selectedType)
closePopup(id)
}
diff --git a/src/components/floor-plan/modal/module/column/ColumnRemove.jsx b/src/components/floor-plan/modal/module/column/ColumnRemove.jsx
index b9079642..2aafb954 100644
--- a/src/components/floor-plan/modal/module/column/ColumnRemove.jsx
+++ b/src/components/floor-plan/modal/module/column/ColumnRemove.jsx
@@ -1,3 +1,4 @@
+import { useUndoRedo } from '@/hooks/useUndoRedo'
import WithDraggable from '@/components/common/draggable/WithDraggable'
import { useRecoilValue } from 'recoil'
import { contextPopupPositionState } from '@/store/popupAtom'
@@ -14,6 +15,7 @@ export default function ColumnRemove(props) {
const [selectedType, setSelectedType] = useState(MODULE_REMOVE_TYPE.LEFT)
const { getMessage } = useMessage()
const { moduleColumnRemove } = useModule()
+ const { saveSnapshot } = useUndoRedo()
const types = [
{ name: getMessage('modal.panel.column.remove.type.left'), value: MODULE_REMOVE_TYPE.LEFT },
{ name: getMessage('modal.panel.column.remove.type.right'), value: MODULE_REMOVE_TYPE.RIGHT },
@@ -21,6 +23,7 @@ export default function ColumnRemove(props) {
{ name: getMessage('modal.panel.column.remove.type.none'), value: MODULE_REMOVE_TYPE.NONE },
]
const handleApply = () => {
+ saveSnapshot()
moduleColumnRemove(selectedType)
closePopup(id)
}
diff --git a/src/components/floor-plan/modal/module/row/RowInsert.jsx b/src/components/floor-plan/modal/module/row/RowInsert.jsx
index e9fb799c..ff7cb813 100644
--- a/src/components/floor-plan/modal/module/row/RowInsert.jsx
+++ b/src/components/floor-plan/modal/module/row/RowInsert.jsx
@@ -1,3 +1,4 @@
+import { useUndoRedo } from '@/hooks/useUndoRedo'
import WithDraggable from '@/components/common/draggable/WithDraggable'
import Image from 'next/image'
import { useState } from 'react'
@@ -14,7 +15,9 @@ export default function RowInsert(props) {
const [selectedType, setSelectedType] = useState(MODULE_INSERT_TYPE.TOP)
const { getMessage } = useMessage()
const { muduleRowInsert } = useModule()
+ const { saveSnapshot } = useUndoRedo()
const handleApply = () => {
+ saveSnapshot()
muduleRowInsert(selectedType)
closePopup(id)
}
diff --git a/src/components/floor-plan/modal/module/row/RowRemove.jsx b/src/components/floor-plan/modal/module/row/RowRemove.jsx
index 5cf95b3f..cdfeebd5 100644
--- a/src/components/floor-plan/modal/module/row/RowRemove.jsx
+++ b/src/components/floor-plan/modal/module/row/RowRemove.jsx
@@ -1,3 +1,4 @@
+import { useUndoRedo } from '@/hooks/useUndoRedo'
import WithDraggable from '@/components/common/draggable/WithDraggable'
import Image from 'next/image'
import { useState } from 'react'
@@ -14,6 +15,7 @@ export default function RowRemove(props) {
const [selectedType, setSelectedType] = useState(MODULE_REMOVE_TYPE.TOP)
const { getMessage } = useMessage()
const { moduleRowRemove } = useModule()
+ const { saveSnapshot } = useUndoRedo()
const types = [
{ name: getMessage('modal.row.remove.type.up'), value: MODULE_REMOVE_TYPE.TOP },
{ name: getMessage('modal.row.remove.type.down'), value: MODULE_REMOVE_TYPE.BOTTOM },
@@ -21,6 +23,7 @@ export default function RowRemove(props) {
{ name: getMessage('modal.row.remove.type.none'), value: MODULE_REMOVE_TYPE.NONE },
]
const handleApply = () => {
+ saveSnapshot()
// if (apply) apply()
moduleRowRemove(selectedType)
closePopup(id)
diff --git a/src/hooks/common/useMenu.js b/src/hooks/common/useMenu.js
index 40c1b6a0..8cdfeb1b 100644
--- a/src/hooks/common/useMenu.js
+++ b/src/hooks/common/useMenu.js
@@ -21,6 +21,7 @@ import { MENU } from '@/common/common'
import { useTrestle } from '@/hooks/module/useTrestle'
import { useOrientation } from '@/hooks/module/useOrientation'
import { corridorDimensionSelector } from '@/store/settingAtom'
+import { useUndoRedo } from '@/hooks/useUndoRedo'
/**
* 메뉴 처리 훅
@@ -36,6 +37,7 @@ export default function useMenu() {
const { clear: trestleClear, setAllModuleSurfaceIsComplete } = useTrestle()
const { nextStep } = useOrientation()
const [corridorDimension, setCorridorDimension] = useRecoilState(corridorDimensionSelector)
+ const { saveSnapshot } = useUndoRedo()
const handleMenu = (type) => {
closeAll()
if (type === 'outline') {
diff --git a/src/hooks/common/useRoofFn.js b/src/hooks/common/useRoofFn.js
index 4721d581..19b6daa9 100644
--- a/src/hooks/common/useRoofFn.js
+++ b/src/hooks/common/useRoofFn.js
@@ -8,6 +8,7 @@ import { useLine } from '@/hooks/useLine'
import { outerLinePointsState } from '@/store/outerLineAtom'
import { usePolygon } from '@/hooks/usePolygon'
import { useText } from '@/hooks/useText'
+import { useUndoRedo } from '@/hooks/useUndoRedo'
const ROOF_COLOR = {
0: 'rgb(199,240,213)',
@@ -26,6 +27,7 @@ export function useRoofFn() {
const { setPolygonLinesActualSize } = usePolygon()
const { changeCorridorDimensionText } = useText()
const [outerLinePoints, setOuterLinePoints] = useRecoilState(outerLinePointsState)
+ const { saveSnapshot } = useUndoRedo()
//면형상 선택 클릭시 지붕 패턴 입히기
function setSurfaceShapePattern(polygon, mode = 'onlyBorder', trestleMode = false, roofMaterial, isForceChange = false, isDisplay = false) {
@@ -236,6 +238,9 @@ export function useRoofFn() {
})
canvas.on('mouse:down', (event) => {
+ // checkPolygon 을 캔버스에서 제거한 뒤 스냅샷을 찍어야 빨간 사각형이 포함되지 않음
+ canvas.remove(checkPolygon)
+ saveSnapshot()
let mousePos = canvas.getPointer(event.e)
mousePos = { x: Math.round(mousePos.x), y: Math.round(mousePos.y) }
diff --git a/src/hooks/roofcover/useRoofShapePassivitySetting.js b/src/hooks/roofcover/useRoofShapePassivitySetting.js
index b693fe41..bbd9dcaf 100644
--- a/src/hooks/roofcover/useRoofShapePassivitySetting.js
+++ b/src/hooks/roofcover/useRoofShapePassivitySetting.js
@@ -1,10 +1,11 @@
import { ANGLE_TYPE, canvasState, currentAngleTypeSelector, currentObjectState, pitchTextSelector } from '@/store/canvasAtom'
-import { useRecoilValue } from 'recoil'
+import { useRecoilValue, useSetRecoilState } from 'recoil'
+import { currentMenuState } from '@/store/canvasAtom'
import { useEffect, useRef, useState } from 'react'
import { useLine } from '@/hooks/useLine'
import { useMessage } from '@/hooks/useMessage'
import { useEvent } from '@/hooks/useEvent'
-import { LINE_TYPE, POLYGON_TYPE } from '@/common/common'
+import { LINE_TYPE, MENU, POLYGON_TYPE } from '@/common/common'
import { useMode } from '@/hooks/useMode'
import { usePolygon } from '@/hooks/usePolygon'
import { useSwal } from '@/hooks/useSwal'
@@ -20,6 +21,7 @@ export function useRoofShapePassivitySetting(id) {
SHED: 'shed',
}
const canvas = useRecoilValue(canvasState)
+ const setCurrentMenu = useSetRecoilState(currentMenuState)
const currentAngleType = useRecoilValue(currentAngleTypeSelector)
const pitchText = useRecoilValue(pitchTextSelector)
const { getMessage } = useMessage()
@@ -194,10 +196,11 @@ export function useRoofShapePassivitySetting(id) {
}
const handleSave = () => {
- saveSnapshot()
+ saveSnapshot(null, { force: true })
isFix.current = true
handleLineToPolygon()
-
+ // 수동설정 완료 → 지붕면 할당 메뉴로 전환 (undo 가드 해제)
+ setCurrentMenu(MENU.ROOF_COVERING.ROOF_SHAPE_ALLOC)
closePopup(id)
}
diff --git a/src/hooks/roofcover/useWallLineOffsetSetting.js b/src/hooks/roofcover/useWallLineOffsetSetting.js
index 31d8f95f..6b254473 100644
--- a/src/hooks/roofcover/useWallLineOffsetSetting.js
+++ b/src/hooks/roofcover/useWallLineOffsetSetting.js
@@ -7,7 +7,9 @@ import { useLine } from '@/hooks/useLine'
import { useSwal } from '@/hooks/useSwal'
import { usePopup } from '@/hooks/usePopup'
import Big from 'big.js'
+import { calcLinePlaneSize } from '@/util/qpolygon-utils'
import { outerLineFixState } from '@/store/outerLineAtom'
+import { useUndoRedo } from '@/hooks/useUndoRedo'
// 외벽선 편집 및 오프셋
export function useWallLineOffsetSetting(id) {
@@ -17,7 +19,7 @@ export function useWallLineOffsetSetting(id) {
const { closePopup } = usePopup()
const { swalFire } = useSwal()
const { addCanvasMouseEventListener, initEvent } = useEvent()
- // const { addCanvasMouseEventListener, initEvent } = useContext(EventContext)
+ const { saveSnapshot } = useUndoRedo()
const wallLineEditRef = useRef(null)
const length1Ref = useRef(null)
const length2Ref = useRef(null)
@@ -46,7 +48,13 @@ export function useWallLineOffsetSetting(id) {
direction: direction,
})
- line.attributes = { ...currentWallLineRef.current.attributes }
+ // 분할된 라인의 실제 길이로 planeSize/actualSize 재계산
+ const newPlaneSize = calcLinePlaneSize({ x1: point1.x, y1: point1.y, x2: point2.x, y2: point2.y })
+ line.attributes = {
+ ...currentWallLineRef.current.attributes,
+ planeSize: newPlaneSize,
+ actualSize: newPlaneSize,
+ }
}
const TYPES = {
@@ -197,9 +205,9 @@ export function useWallLineOffsetSetting(id) {
const handleSave = () => {
if (currentWallLineRef.current === null) {
- // alert('보조선을 먼저 선택하세요')
return
}
+ saveSnapshot()
switch (type) {
case TYPES.WALL_LINE_EDIT:
handleWallLineEditSave()
diff --git a/src/hooks/surface/usePlacementShapeDrawing.js b/src/hooks/surface/usePlacementShapeDrawing.js
index 892f5f1a..2f08713e 100644
--- a/src/hooks/surface/usePlacementShapeDrawing.js
+++ b/src/hooks/surface/usePlacementShapeDrawing.js
@@ -27,6 +27,7 @@ import { useSurfaceShapeBatch } from './useSurfaceShapeBatch'
import { roofDisplaySelector } from '@/store/settingAtom'
import { useRoofFn } from '@/hooks/common/useRoofFn'
import { useObject } from '@/hooks/useObject'
+import { useUndoRedo } from '@/hooks/useUndoRedo'
// 배치면 그리기
export function usePlacementShapeDrawing(id) {
@@ -48,6 +49,7 @@ export function usePlacementShapeDrawing(id) {
const { setSurfaceShapePattern } = useRoofFn()
const { changeSurfaceLineType } = useSurfaceShapeBatch({})
const { handleSelectableObjects } = useObject()
+ const { saveSnapshot, popLastSnapshot } = useUndoRedo()
const verticalHorizontalMode = useRecoilValue(verticalHorizontalModeState)
const adsorptionRange = useRecoilValue(adsorptionRangeState)
@@ -92,13 +94,19 @@ export function usePlacementShapeDrawing(id) {
}, [verticalHorizontalMode, points, adsorptionRange, adsorptionPointMode])
useEffect(() => {
+ // 배치면 그리기 시작 시 스냅샷 저장 (undo 하면 그리기 전 상태로 복원)
+ saveSnapshot()
setPoints([])
setType(OUTER_LINE_TYPE.OUTER_LINE)
return () => {
const placementShapeDrawingStartPoint = canvas.getObjects().find((obj) => obj.name === 'placementShapeDrawingStartPoint')
-
canvas.remove(placementShapeDrawingStartPoint)
+
+ // 확정하지 않고 닫은 경우 스냅샷 제거
+ if (!isFix.current) {
+ popLastSnapshot()
+ }
}
}, [])
diff --git a/src/hooks/surface/useRoofLinePropertySetting.js b/src/hooks/surface/useRoofLinePropertySetting.js
index 6115f76e..febd84b2 100644
--- a/src/hooks/surface/useRoofLinePropertySetting.js
+++ b/src/hooks/surface/useRoofLinePropertySetting.js
@@ -6,6 +6,7 @@ import { usePopup } from '../usePopup'
import useSWR from 'swr'
import { useSwal } from '../useSwal'
import { useMessage } from '../useMessage'
+import { useUndoRedo } from '../useUndoRedo'
const LINE_COLOR = {
EAVES: '#45CD7D',
@@ -23,6 +24,7 @@ export function useRoofLinePropertySetting(props) {
const { closePopup } = usePopup()
const { swalFire } = useSwal()
const { getMessage } = useMessage()
+ const { saveSnapshot, popLastSnapshot } = useUndoRedo()
useEffect(() => {
if (currentObject && currentObject.name === 'cloneRoofLine') {
@@ -47,10 +49,20 @@ export function useRoofLinePropertySetting(props) {
}
}, [currentObject])
+ const isFixed = useRef(false)
+
useEffect(() => {
+ // 팝업 열릴 때 스냅샷 저장
+ saveSnapshot()
+
return () => {
canvas.remove(...canvas.getObjects().filter((obj) => obj.name === 'cloneRoofLine'))
canvas.renderAll()
+
+ // 설정 완료하지 않고 닫은 경우 스냅샷 제거
+ if (!isFixed.current) {
+ popLastSnapshot()
+ }
}
}, [])
@@ -149,8 +161,6 @@ export function useRoofLinePropertySetting(props) {
}
const handleFix = () => {
- // const roof = canvas.getObjects().find((obj) => currentObject.parentId === obj.id)
- // const notSettingLines = roof.lines.filter(
const notSettingLines = canvas
.getObjects()
.filter((obj) => obj.name === 'cloneRoofLine')
@@ -187,6 +197,7 @@ export function useRoofLinePropertySetting(props) {
// })
// })
+ isFixed.current = true
canvas.renderAll()
closePopup(id)
if (setIsHidden) setIsHidden(false)
diff --git a/src/hooks/surface/useSurfaceShapeBatch.js b/src/hooks/surface/useSurfaceShapeBatch.js
index ac9d5fd1..a5fa5232 100644
--- a/src/hooks/surface/useSurfaceShapeBatch.js
+++ b/src/hooks/surface/useSurfaceShapeBatch.js
@@ -52,6 +52,7 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
const { saveSnapshot } = useUndoRedo()
const applySurfaceShape = (surfaceRefs, selectedType, id) => {
+ saveSnapshot()
let length1, length2, length3, length4, length5
const surfaceId = selectedType?.id
const azimuth = surfaceRefs.azimuth.current
@@ -844,6 +845,7 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
text: getMessage('batch.canvas.delete.all'),
type: 'confirm',
confirmFn: () => {
+ saveSnapshot()
canvas.clear()
delete canvas.outerLineFix
diff --git a/src/hooks/useCanvas.js b/src/hooks/useCanvas.js
index 9a6fb14b..3880174a 100644
--- a/src/hooks/useCanvas.js
+++ b/src/hooks/useCanvas.js
@@ -155,6 +155,7 @@ export function useCanvas(id) {
// 해당 오브젝트 타입의 경우 저장한 값 그대로 불러와야함
OBJECT_PROTOTYPE.forEach((type) => {
+ const originalToObject = type.toObject
type.toObject = function (propertiesToInclude) {
let source = {}
@@ -175,7 +176,9 @@ export function useCanvas(id) {
}
}
- return fabric.util.object.extend(this.callSuper('toObject', propertiesToInclude), source)
+ // 원본 toObject 를 호출하여 Group 의 objects 등 고유 직렬화 로직을 유지
+ const base = originalToObject ? originalToObject.call(this, propertiesToInclude) : this.callSuper('toObject', propertiesToInclude)
+ return fabric.util.object.extend(base, source)
}
})
diff --git a/src/hooks/useCanvasEvent.js b/src/hooks/useCanvasEvent.js
index d15eb05d..351fb8cc 100644
--- a/src/hooks/useCanvasEvent.js
+++ b/src/hooks/useCanvasEvent.js
@@ -21,7 +21,7 @@ export function useCanvasEvent() {
const currentMenu = useRecoilValue(currentMenuState)
const { changeCorridorDimensionText } = useText()
const { setPolygonLinesActualSize } = usePolygon()
- const { undo, redo } = useUndoRedo()
+ const { undo, redo, saveSnapshot } = useUndoRedo()
useEffect(() => {
canvas?.setZoom(canvasZoom / 100)
@@ -291,22 +291,18 @@ export function useCanvasEvent() {
break
case 'Down': // IE/Edge에서 사용되는 값
case 'ArrowDown':
- // "아래 화살표" 키가 눌렸을 때의 동작입니다.
moveDown()
break
case 'Up': // IE/Edge에서 사용되는 값
case 'ArrowUp':
- // "위 화살표" 키가 눌렸을 때의 동작입니다.
moveUp()
break
case 'Left': // IE/Edge에서 사용되는 값
case 'ArrowLeft':
- // "왼쪽 화살표" 키가 눌렸을 때의 동작입니다.
moveLeft()
break
case 'Right': // IE/Edge에서 사용되는 값
case 'ArrowRight':
- // "오른쪽 화살표" 키가 눌렸을 때의 동작입니다.
moveRight()
break
case 'Enter':
diff --git a/src/hooks/useContextMenu.js b/src/hooks/useContextMenu.js
index 25dd2a1a..74fd31ad 100644
--- a/src/hooks/useContextMenu.js
+++ b/src/hooks/useContextMenu.js
@@ -43,6 +43,7 @@ import { useTrestle } from './module/useTrestle'
import { useCircuitTrestle } from './useCirCuitTrestle'
import { usePolygon } from '@/hooks/usePolygon'
import { useText } from '@/hooks/useText'
+import { useUndoRedo } from '@/hooks/useUndoRedo'
export function useContextMenu() {
const canvas = useRecoilValue(canvasState)
@@ -77,6 +78,7 @@ export function useContextMenu() {
const { isExistCircuit } = useCircuitTrestle()
const { changeCorridorDimensionText } = useText()
const { setPolygonLinesActualSize, drawDirectionArrow } = usePolygon()
+ const { saveSnapshot } = useUndoRedo()
const currentMenuSetting = () => {
switch (selectedMenu) {
case 'outline':
@@ -170,7 +172,10 @@ export function useContextMenu() {
{
id: 'roofMaterialRemoveAll',
name: getMessage('contextmenu.roof.material.remove.all'),
- fn: () => removeAllRoofMaterial(),
+ fn: () => {
+ saveSnapshot()
+ removeAllRoofMaterial()
+ },
},
{
id: 'selectMove',
@@ -183,6 +188,7 @@ export function useContextMenu() {
id: 'wallLineRemove',
name: getMessage('contextmenu.wallline.remove'),
fn: (currentMousePos) => {
+ saveSnapshot()
removeOuterLines(currentMousePos)
},
},
@@ -260,7 +266,10 @@ export function useContextMenu() {
{
id: 'roofMaterialRemoveAll',
name: getMessage('contextmenu.roof.material.remove.all'),
- fn: () => removeAllRoofMaterial(),
+ fn: () => {
+ saveSnapshot()
+ removeAllRoofMaterial()
+ },
},
{
id: 'selectMove',
@@ -368,6 +377,7 @@ export function useContextMenu() {
name: `${getMessage('contextmenu.auxiliary.remove')}(D)`,
fn: () => {
if (!currentObject) return
+ saveSnapshot()
const roof = canvas.getObjects().filter((obj) => obj.id === currentObject.attributes.roofId)[0]
if (!roof) {
// 아직 innerLines로 세팅이 안되어있는 line인 경우 제거
@@ -386,6 +396,7 @@ export function useContextMenu() {
name: getMessage('contextmenu.auxiliary.vertical.bisector'),
fn: () => {
if (!currentObject) return
+ saveSnapshot()
const slope = (currentObject.y2 - currentObject.y1) / (currentObject.x2 - currentObject.x1)
const length = currentObject.length
@@ -435,6 +446,7 @@ export function useContextMenu() {
swalFire({ text: getMessage('roof.is.not.selected') })
return
}
+ saveSnapshot()
const innerLines = canvas.getObjects().filter((obj) => obj.id === currentObject.attributes.roofId)[0]?.innerLines
if (innerLines) {
innerLines.forEach((line) => {
@@ -702,7 +714,10 @@ export function useContextMenu() {
{
id: 'remove',
name: getMessage('contextmenu.remove'),
- fn: () => modulesRemove(),
+ fn: () => {
+ saveSnapshot()
+ modulesRemove()
+ },
},
{
id: 'move',
@@ -900,6 +915,7 @@ export function useContextMenu() {
id: 'moduleRemove',
name: getMessage('contextmenu.module.remove'),
fn: () => {
+ saveSnapshot()
moduleRoofRemove(currentObject.arrayData ?? [currentObject])
// const moduleSetupSurface = canvas.getObjects().filter((obj) => canvas.getActiveObjects()[0].id === obj.id)[0]
diff --git a/src/hooks/useUndoRedo.js b/src/hooks/useUndoRedo.js
index 21bfc500..cd1ef27b 100644
--- a/src/hooks/useUndoRedo.js
+++ b/src/hooks/useUndoRedo.js
@@ -16,7 +16,7 @@ import {
import { outerLinePointsState } from '@/store/outerLineAtom'
import { selectedMenuState } from '@/store/menuAtom'
import { pendingPopupState, popupState } from '@/store/popupAtom'
-import { POLYGON_TYPE, SAVE_KEY } from '@/common/common'
+import { MENU, POLYGON_TYPE, SAVE_KEY } from '@/common/common'
const MAX_HISTORY = 3
@@ -182,6 +182,20 @@ export function useUndoRedo() {
surface.modules = surfaceModules
})
+ // fabric.Group 내부의 QPolygon/QLine 자식에 canvas 참조 설정
+ // loadFromJSON 후 Group 자식은 canvas 를 받지 못해 init/addLengthText 등이 실패할 수 있음
+ objects
+ .filter((obj) => obj.type === 'group')
+ .forEach((group) => {
+ if (group._objects) {
+ group._objects.forEach((child) => {
+ if (!child.canvas) {
+ child.canvas = canvas
+ }
+ })
+ }
+ })
+
// 부모가 없는 고아 lengthText, arrow, flowText 제거
const parentIds = new Set(objects.filter((obj) => obj.id).map((obj) => obj.id))
const orphans = objects.filter(
@@ -258,6 +272,26 @@ export function useUndoRedo() {
const cells = roofs.filter((obj) => obj.cells && obj.cells.length > 0)
setDrewRoofCells(cells)
+
+ // loadFromJSON 후 QPolygon/QLine 의 lengthText 가 orphan 제거되었을 수 있으므로 재생성
+ // (initLines 로 새 QLine 이 만들어지면 기존 lengthText 의 parentId 가 안 맞음)
+ const qPolygons = objects.filter((obj) => obj.type === 'QPolygon' && obj.name !== 'arrow' && obj.name !== POLYGON_TYPE.MODULE && obj.name !== POLYGON_TYPE.MODULE_SETUP_SURFACE && obj.name !== POLYGON_TYPE.OBJECT_SURFACE)
+ qPolygons.forEach((polygon) => {
+ try {
+ polygon.initLines()
+ polygon.addLengthText()
+ } catch (e) { /* skip */ }
+ })
+
+ canvas.getObjects()
+ .filter((obj) => obj.type === 'QLine')
+ .forEach((line) => {
+ try {
+ if (typeof line.addLengthText === 'function') {
+ line.addLengthText()
+ }
+ } catch (e) { /* skip */ }
+ })
}, [canvas, rebuildObjectGraph])
const restoreAtomSnapshot = useCallback((atomSnapshot) => {
@@ -277,7 +311,6 @@ export function useUndoRedo() {
if (!canvas) return null
try {
const rawJSON = canvas.toJSON(SAVE_KEY)
- // 순환 참조 제거 후 다시 파싱하여 순수 데이터 객체로 변환
return JSON.parse(safeStringify(rawJSON))
} catch (e) {
console.warn('Canvas snapshot failed:', e)
@@ -285,9 +318,12 @@ export function useUndoRedo() {
}
}, [canvas])
- const saveSnapshot = useCallback((atomOverrides = null) => {
+ const saveSnapshot = useCallback((atomOverrides = null, { force = false } = {}) => {
if (!canvas || _isRestoring) return
+ // 지붕형상 수동설정 메뉴에서는 스냅샷을 쌓지 않는다 (force 시 예외)
+ if (!force && currentMenu === MENU.ROOF_COVERING.ROOF_SHAPE_PASSIVITY_SETTINGS) return
+
// snapshot 전에 mouseLine 제거
const mouseLines = canvas.getObjects().filter((obj) => obj.name === 'mouseLine')
mouseLines.forEach((obj) => canvas.remove(obj))
@@ -315,21 +351,28 @@ export function useUndoRedo() {
return newStack
})
setRedoStack([])
- }, [canvas, getCanvasSnapshot, getAtomSnapshot])
+ }, [canvas, currentMenu, getCanvasSnapshot, getAtomSnapshot])
const closeAllPopups = useCallback(() => {
- setPopup({ config: [], other: [] })
+ setPopup((prev) => ({
+ // 처마 케라바 변경 팝업(EavesGableEdit)은 undo/redo 시 닫지 않는다
+ config: prev.config.filter((p) => p.component?.type?.name === 'EavesGableEdit'),
+ other: prev.other.filter((p) => p.component?.type?.name === 'EavesGableEdit'),
+ }))
}, [setPopup])
const undo = useCallback(() => {
+ // 지붕형상 수동설정 중에는 undo 차단
+ if (currentMenu === MENU.ROOF_COVERING.ROOF_SHAPE_PASSIVITY_SETTINGS) return
+
const currentUndoStack = undoStackRef.current
- console.log('currentUndoStack', currentUndoStack)
if (!canvas || currentUndoStack.length === 0 || _isRestoring) return
_isRestoring = true
const currentCanvasJSON = getCanvasSnapshot()
const currentAtomSnapshot = getAtomSnapshot()
+
const currentSnapshot = {
canvasJSON: currentCanvasJSON,
atomSnapshot: currentAtomSnapshot,
@@ -348,6 +391,9 @@ export function useUndoRedo() {
}, [canvas, currentMenu, getCanvasSnapshot, getAtomSnapshot, restoreAtomSnapshot, rebuildCanvasAtoms, closeAllPopups])
const redo = useCallback(() => {
+ // 지붕형상 수동설정 중에는 redo 차단
+ if (currentMenu === MENU.ROOF_COVERING.ROOF_SHAPE_PASSIVITY_SETTINGS) return
+
const currentRedoStack = redoStackRef.current
if (!canvas || currentRedoStack.length === 0 || _isRestoring) return
@@ -377,11 +423,17 @@ export function useUndoRedo() {
setRedoStack([])
}, [])
+ /** undo 스택에서 마지막 스냅샷 1개만 제거 (취소 시 사용) */
+ const popLastSnapshot = useCallback(() => {
+ setUndoStack((prev) => (prev.length > 0 ? prev.slice(0, -1) : prev))
+ }, [])
+
return {
saveSnapshot,
undo,
redo,
clearStacks,
+ popLastSnapshot,
canUndo: undoStack.length > 0,
canRedo: redoStack.length > 0,
}