외벽선, 배치면 Redo undo 수정

This commit is contained in:
hyojun.choi 2026-05-07 10:53:51 +09:00
parent 2074c4aa93
commit 80720fef2a
5 changed files with 80 additions and 10 deletions

View File

@ -9,6 +9,7 @@ import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil'
import { subMenusState } from '@/store/menuAtom' import { subMenusState } from '@/store/menuAtom'
import { useCanvasMenu } from '@/hooks/common/useCanvasMenu' import { useCanvasMenu } from '@/hooks/common/useCanvasMenu'
import { commonUtilsState } from '@/store/commonUtilsAtom' import { commonUtilsState } from '@/store/commonUtilsAtom'
import { isUndoRedoRestoring } from '@/hooks/useUndoRedo'
export default function MenuDepth01() { export default function MenuDepth01() {
const canvas = useRecoilValue(canvasState) const canvas = useRecoilValue(canvasState)
@ -29,6 +30,10 @@ export default function MenuDepth01() {
} }
useEffect(() => { useEffect(() => {
// undo/redo .
// setDrawing(true)
// redo .
if (isUndoRedoRestoring()) return
handleMenu(selectedMenu) handleMenu(selectedMenu)
canvas?.discardActiveObject() canvas?.discardActiveObject()
}, [currentMenu]) }, [currentMenu])

View File

@ -63,7 +63,7 @@ export function useOuterLineWall(id, propertiesId) {
const { tempGridMode } = useTempGrid() const { tempGridMode } = useTempGrid()
const { addPolygonByLines } = usePolygon() const { addPolygonByLines } = usePolygon()
const { handleSelectableObjects } = useObject() const { handleSelectableObjects } = useObject()
const { saveSnapshot } = useUndoRedo() const { saveSnapshot, setDrawing } = useUndoRedo()
const verticalHorizontalMode = useRecoilValue(verticalHorizontalModeState) const verticalHorizontalMode = useRecoilValue(verticalHorizontalModeState)
const adsorptionPointAddMode = useRecoilValue(adsorptionPointAddModeState) const adsorptionPointAddMode = useRecoilValue(adsorptionPointAddModeState)
@ -121,6 +121,15 @@ export function useOuterLineWall(id, propertiesId) {
} }
}, [verticalHorizontalMode, points, adsorptionPointAddMode, adsorptionPointMode, adsorptionRange, interval, tempGridMode]) }, [verticalHorizontalMode, points, adsorptionPointAddMode, adsorptionPointMode, adsorptionRange, interval, tempGridMode])
// 외벽선 그리기 진행 중에는 undo/redo/스냅샷을 차단한다.
// 그리기 전 스냅샷은 첫 클릭(mouseDown) 에서 force 로 저장한다 (기존 동작 유지).
useEffect(() => {
setDrawing(true)
return () => {
setDrawing(false)
}
}, [])
useEffect(() => { useEffect(() => {
arrow1Ref.current = arrow1 arrow1Ref.current = arrow1
}, [arrow1]) }, [arrow1])
@ -158,7 +167,9 @@ export function useOuterLineWall(id, propertiesId) {
pointer = { x: Big(pointer.x).toNumber(), y: Big(pointer.y).toNumber() } pointer = { x: Big(pointer.x).toNumber(), y: Big(pointer.y).toNumber() }
if (points.length === 0) { if (points.length === 0) {
saveSnapshot() // 그리기 진행 중에는 스냅샷이 차단되므로 force 로 저장한다.
// (처음 한 번만 저장되고 이후 추가 클릭은 points.length > 0 분기로 빠지므로 중복 저장 안 됨)
saveSnapshot(null, { force: true })
setPoints((prev) => [...prev, pointer]) setPoints((prev) => [...prev, pointer])
} else { } else {
const lastPoint = points[points.length - 1] const lastPoint = points[points.length - 1]

View File

@ -51,7 +51,7 @@ export function usePlacementShapeDrawing(id) {
const { setSurfaceShapePattern } = useRoofFn() const { setSurfaceShapePattern } = useRoofFn()
const { changeSurfaceLineType } = useSurfaceShapeBatch({}) const { changeSurfaceLineType } = useSurfaceShapeBatch({})
const { handleSelectableObjects } = useObject() const { handleSelectableObjects } = useObject()
const { saveSnapshot, popLastSnapshot } = useUndoRedo() const { saveSnapshot, popLastSnapshot, setDrawing } = useUndoRedo()
const { swalFire } = useSwal() const { swalFire } = useSwal()
const { getMessage } = useMessage() const { getMessage } = useMessage()
@ -98,12 +98,15 @@ export function usePlacementShapeDrawing(id) {
}, [verticalHorizontalMode, points, adsorptionRange, adsorptionPointMode]) }, [verticalHorizontalMode, points, adsorptionRange, adsorptionPointMode])
useEffect(() => { useEffect(() => {
// 배치면 그리기 시작 시 스냅샷 저장 (undo 하면 그리기 전 상태로 복원) // 배치면 그리기 시작 시 스냅샷 저장 (undo 하면 그리기 전 상태로 복원).
saveSnapshot() // 그리기 진행 중에는 undo/redo/snapshot 을 차단하므로 force 로 저장한다.
setDrawing(true)
saveSnapshot(null, { force: true })
setPoints([]) setPoints([])
setType(OUTER_LINE_TYPE.OUTER_LINE) setType(OUTER_LINE_TYPE.OUTER_LINE)
return () => { return () => {
setDrawing(false)
const placementShapeDrawingStartPoint = canvas.getObjects().find((obj) => obj.name === 'placementShapeDrawingStartPoint') const placementShapeDrawingStartPoint = canvas.getObjects().find((obj) => obj.name === 'placementShapeDrawingStartPoint')
canvas.remove(placementShapeDrawingStartPoint) canvas.remove(placementShapeDrawingStartPoint)

View File

@ -11,6 +11,7 @@ import {
moduleSetupSurfaceState, moduleSetupSurfaceState,
redoStackState, redoStackState,
roofPolygonArrayState, roofPolygonArrayState,
undoRedoDrawingState,
undoStackState, undoStackState,
} from '@/store/canvasAtom' } from '@/store/canvasAtom'
import { outerLinePointsState } from '@/store/outerLineAtom' import { outerLinePointsState } from '@/store/outerLineAtom'
@ -25,6 +26,10 @@ const MAX_HISTORY = 3
let _isRestoring = false let _isRestoring = false
let _restoreTimer = null let _restoreTimer = null
// 외벽선/배치면 등 그리기 진행 중에는 undo/redo/snapshot 을 차단한다.
// (확정 시점부터 history 가 동작해야 하므로)
let _isDrawing = false
/** /**
* 외부 /모듈에서 현재 undo/redo 복원 중인지 확인할 사용 * 외부 /모듈에서 현재 undo/redo 복원 중인지 확인할 사용
*/ */
@ -32,6 +37,21 @@ export function isUndoRedoRestoring() {
return _isRestoring return _isRestoring
} }
/**
* 외벽선/배치면 그리기 진행 상태를 외부에서 설정한다.
* 그리기가 시작될 true, 확정 또는 취소 false 설정한다.
*/
export function setUndoRedoDrawing(value) {
_isDrawing = !!value
}
/**
* 외부 /모듈에서 현재 그리기 진행 중인지 확인할 사용
*/
export function isUndoRedoDrawing() {
return _isDrawing
}
// loadFromJSON 공통 처리 - 이벤트 일시 중단 후 복원 // loadFromJSON 공통 처리 - 이벤트 일시 중단 후 복원
function _restoreFromJSON(canvas, snapshot, restoreAtomSnapshot, rebuildCanvasAtoms, setCanvasZoom) { function _restoreFromJSON(canvas, snapshot, restoreAtomSnapshot, rebuildCanvasAtoms, setCanvasZoom) {
// 진행 중인 복원 타이머가 있으면 취소 // 진행 중인 복원 타이머가 있으면 취소
@ -98,6 +118,7 @@ export function useUndoRedo() {
const canvas = useRecoilValue(canvasState) const canvas = useRecoilValue(canvasState)
const [undoStack, setUndoStack] = useRecoilState(undoStackState) const [undoStack, setUndoStack] = useRecoilState(undoStackState)
const [redoStack, setRedoStack] = useRecoilState(redoStackState) const [redoStack, setRedoStack] = useRecoilState(redoStackState)
const [isDrawing, setIsDrawing] = useRecoilState(undoRedoDrawingState)
const [, setModuleSetupSurface] = useRecoilState(moduleSetupSurfaceState) const [, setModuleSetupSurface] = useRecoilState(moduleSetupSurfaceState)
const [moduleIsSetup, setModuleIsSetup] = useRecoilState(moduleIsSetupState) const [moduleIsSetup, setModuleIsSetup] = useRecoilState(moduleIsSetupState)
@ -375,6 +396,9 @@ export function useUndoRedo() {
// 지붕형상 수동설정 메뉴에서는 스냅샷을 쌓지 않는다 (force 시 예외) // 지붕형상 수동설정 메뉴에서는 스냅샷을 쌓지 않는다 (force 시 예외)
if (!force && currentMenu === MENU.ROOF_COVERING.ROOF_SHAPE_PASSIVITY_SETTINGS) return if (!force && currentMenu === MENU.ROOF_COVERING.ROOF_SHAPE_PASSIVITY_SETTINGS) return
// 외벽선/배치면 등 그리기 진행 중에는 스냅샷을 쌓지 않는다 (force 시 예외)
if (!force && (_isDrawing || isDrawing)) return
// snapshot 전에 mouseLine 제거 // snapshot 전에 mouseLine 제거
const mouseLines = canvas.getObjects().filter((obj) => obj.name === 'mouseLine') const mouseLines = canvas.getObjects().filter((obj) => obj.name === 'mouseLine')
mouseLines.forEach((obj) => canvas.remove(obj)) mouseLines.forEach((obj) => canvas.remove(obj))
@ -402,7 +426,7 @@ export function useUndoRedo() {
return newStack return newStack
}) })
setRedoStack([]) setRedoStack([])
}, [canvas, currentMenu, getCanvasSnapshot, getAtomSnapshot]) }, [canvas, currentMenu, isDrawing, getCanvasSnapshot, getAtomSnapshot])
const closeAllPopups = useCallback(() => { const closeAllPopups = useCallback(() => {
setPopup((prev) => ({ setPopup((prev) => ({
@ -416,6 +440,9 @@ export function useUndoRedo() {
// 지붕형상 수동설정 중에는 undo 차단 // 지붕형상 수동설정 중에는 undo 차단
if (currentMenu === MENU.ROOF_COVERING.ROOF_SHAPE_PASSIVITY_SETTINGS) return if (currentMenu === MENU.ROOF_COVERING.ROOF_SHAPE_PASSIVITY_SETTINGS) return
// 외벽선/배치면 등 그리기 진행 중에는 undo 차단
if (_isDrawing || isDrawing) return
const currentUndoStack = undoStackRef.current const currentUndoStack = undoStackRef.current
if (!canvas || currentUndoStack.length === 0 || _isRestoring) return if (!canvas || currentUndoStack.length === 0 || _isRestoring) return
@ -439,12 +466,15 @@ export function useUndoRedo() {
setRedoStack((prev) => [...prev, currentSnapshot]) setRedoStack((prev) => [...prev, currentSnapshot])
_restoreFromJSON(canvas, snapshot, restoreAtomSnapshot, rebuildCanvasAtoms, setCanvasZoom) _restoreFromJSON(canvas, snapshot, restoreAtomSnapshot, rebuildCanvasAtoms, setCanvasZoom)
}, [canvas, currentMenu, getCanvasSnapshot, getAtomSnapshot, restoreAtomSnapshot, rebuildCanvasAtoms, closeAllPopups]) }, [canvas, currentMenu, isDrawing, getCanvasSnapshot, getAtomSnapshot, restoreAtomSnapshot, rebuildCanvasAtoms, closeAllPopups])
const redo = useCallback(() => { const redo = useCallback(() => {
// 지붕형상 수동설정 중에는 redo 차단 // 지붕형상 수동설정 중에는 redo 차단
if (currentMenu === MENU.ROOF_COVERING.ROOF_SHAPE_PASSIVITY_SETTINGS) return if (currentMenu === MENU.ROOF_COVERING.ROOF_SHAPE_PASSIVITY_SETTINGS) return
// 외벽선/배치면 등 그리기 진행 중에는 redo 차단
if (_isDrawing || isDrawing) return
const currentRedoStack = redoStackRef.current const currentRedoStack = redoStackRef.current
if (!canvas || currentRedoStack.length === 0 || _isRestoring) return if (!canvas || currentRedoStack.length === 0 || _isRestoring) return
@ -467,7 +497,7 @@ export function useUndoRedo() {
setUndoStack((prev) => [...prev, currentSnapshot]) setUndoStack((prev) => [...prev, currentSnapshot])
_restoreFromJSON(canvas, snapshot, restoreAtomSnapshot, rebuildCanvasAtoms, setCanvasZoom) _restoreFromJSON(canvas, snapshot, restoreAtomSnapshot, rebuildCanvasAtoms, setCanvasZoom)
}, [canvas, currentMenu, getCanvasSnapshot, getAtomSnapshot, restoreAtomSnapshot, rebuildCanvasAtoms, closeAllPopups]) }, [canvas, currentMenu, isDrawing, getCanvasSnapshot, getAtomSnapshot, restoreAtomSnapshot, rebuildCanvasAtoms, closeAllPopups])
const clearStacks = useCallback(() => { const clearStacks = useCallback(() => {
setUndoStack([]) setUndoStack([])
@ -479,13 +509,27 @@ export function useUndoRedo() {
setUndoStack((prev) => (prev.length > 0 ? prev.slice(0, -1) : prev)) setUndoStack((prev) => (prev.length > 0 ? prev.slice(0, -1) : prev))
}, []) }, [])
/**
* 외벽선/배치면 그리기 진행 중에는 undo/redo 차단한다.
* 마운트 true, 언마운트 false 설정한다.
*/
const setDrawing = useCallback(
(value) => {
const v = !!value
setIsDrawing(v)
setUndoRedoDrawing(v)
},
[setIsDrawing],
)
return { return {
saveSnapshot, saveSnapshot,
undo, undo,
redo, redo,
clearStacks, clearStacks,
popLastSnapshot, popLastSnapshot,
canUndo: undoStack.length > 0, setDrawing,
canRedo: redoStack.length > 0, canUndo: undoStack.length > 0 && !isDrawing,
canRedo: redoStack.length > 0 && !isDrawing,
} }
} }

View File

@ -421,3 +421,10 @@ export const redoStackState = atom({
default: [], default: [],
dangerouslyAllowMutability: true, dangerouslyAllowMutability: true,
}) })
// 외벽선/배치면 등 그리기 진행 중 여부.
// true 동안에는 undo/redo 가 비활성화되고 새 스냅샷이 쌓이지 않는다.
export const undoRedoDrawingState = atom({
key: 'undoRedoDrawingState',
default: false,
})