undo redo 작업중
This commit is contained in:
parent
5f4673dd3f
commit
7e08184227
@ -52,6 +52,7 @@ import { QcastContext } from '@/app/QcastProvider'
|
|||||||
import { useRoofFn } from '@/hooks/common/useRoofFn'
|
import { useRoofFn } from '@/hooks/common/useRoofFn'
|
||||||
import { usePolygon } from '@/hooks/usePolygon'
|
import { usePolygon } from '@/hooks/usePolygon'
|
||||||
import { useTrestle } from '@/hooks/module/useTrestle'
|
import { useTrestle } from '@/hooks/module/useTrestle'
|
||||||
|
import { useUndoRedo } from '@/hooks/useUndoRedo'
|
||||||
|
|
||||||
export default function CanvasMenu(props) {
|
export default function CanvasMenu(props) {
|
||||||
const [currentCanvasPlan, setCurrentCanvasPlan] = useRecoilState(currentCanvasPlanState)
|
const [currentCanvasPlan, setCurrentCanvasPlan] = useRecoilState(currentCanvasPlanState)
|
||||||
@ -73,6 +74,7 @@ export default function CanvasMenu(props) {
|
|||||||
const { handleZoomClear, handleZoom } = useCanvasEvent()
|
const { handleZoomClear, handleZoom } = useCanvasEvent()
|
||||||
const { setAllModuleSurfaceIsComplete, isAllComplete } = useTrestle()
|
const { setAllModuleSurfaceIsComplete, isAllComplete } = useTrestle()
|
||||||
const { handleMenu } = useMenu()
|
const { handleMenu } = useMenu()
|
||||||
|
const { undo, redo, canUndo, canRedo } = useUndoRedo()
|
||||||
// const urlParams = useSearchParams()
|
// const urlParams = useSearchParams()
|
||||||
const { handleEstimateSubmit, fetchSetting, estimateContextState, setEstimateContextState } = useEstimateController()
|
const { handleEstimateSubmit, fetchSetting, estimateContextState, setEstimateContextState } = useEstimateController()
|
||||||
const estimateRecoilState = useRecoilValue(estimateState)
|
const estimateRecoilState = useRecoilValue(estimateState)
|
||||||
@ -569,6 +571,30 @@ export default function CanvasMenu(props) {
|
|||||||
<div className="canvas-side-btn-wrap">
|
<div className="canvas-side-btn-wrap">
|
||||||
{!['estimate', 'simulation'].some((num) => num === selectedMenu) && (
|
{!['estimate', 'simulation'].some((num) => num === selectedMenu) && (
|
||||||
<>
|
<>
|
||||||
|
<div className="undo-redo-wrap">
|
||||||
|
<button
|
||||||
|
className={`undo-btn ${!canUndo ? 'disabled' : ''}`}
|
||||||
|
onClick={undo}
|
||||||
|
disabled={!canUndo}
|
||||||
|
title="Undo (Ctrl+Z)"
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<path d="M3 10h10c4.42 0 8 3.58 8 8v1" />
|
||||||
|
<polyline points="7 14 3 10 7 6" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`redo-btn ${!canRedo ? 'disabled' : ''}`}
|
||||||
|
onClick={redo}
|
||||||
|
disabled={!canRedo}
|
||||||
|
title="Redo (Ctrl+Shift+Z)"
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<path d="M21 10H11C6.58 10 3 13.58 3 18v1" />
|
||||||
|
<polyline points="17 14 21 10 17 6" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
{
|
{
|
||||||
<div className={`vertical-horizontal ${verticalHorizontalMode ? 'on' : ''}`}>
|
<div className={`vertical-horizontal ${verticalHorizontalMode ? 'on' : ''}`}>
|
||||||
<span>{getMessage('plan.mode.vertical.horizontal')}</span>
|
<span>{getMessage('plan.mode.vertical.horizontal')}</span>
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { canvasState } from '@/store/canvasAtom'
|
|||||||
import { usePolygon } from '@/hooks/usePolygon'
|
import { usePolygon } from '@/hooks/usePolygon'
|
||||||
import { useSurfaceShapeBatch } from '@/hooks/surface/useSurfaceShapeBatch'
|
import { useSurfaceShapeBatch } from '@/hooks/surface/useSurfaceShapeBatch'
|
||||||
import { useRoofFn } from '@/hooks/common/useRoofFn'
|
import { useRoofFn } from '@/hooks/common/useRoofFn'
|
||||||
|
import { useUndoRedo } from '@/hooks/useUndoRedo'
|
||||||
|
|
||||||
const FLOW_DIRECTION_TYPE = {
|
const FLOW_DIRECTION_TYPE = {
|
||||||
EIGHT_AZIMUTH: 'eightAzimuth',
|
EIGHT_AZIMUTH: 'eightAzimuth',
|
||||||
@ -24,6 +25,7 @@ export default function FlowDirectionSetting(props) {
|
|||||||
const { setPolygonLinesActualSize } = usePolygon()
|
const { setPolygonLinesActualSize } = usePolygon()
|
||||||
|
|
||||||
const { changeSurfaceLineType } = useSurfaceShapeBatch({})
|
const { changeSurfaceLineType } = useSurfaceShapeBatch({})
|
||||||
|
const { saveSnapshot } = useUndoRedo()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@ -77,6 +79,7 @@ export default function FlowDirectionSetting(props) {
|
|||||||
const [type, setType] = useState(target.surfaceCompassType ?? FLOW_DIRECTION_TYPE.EIGHT_AZIMUTH)
|
const [type, setType] = useState(target.surfaceCompassType ?? FLOW_DIRECTION_TYPE.EIGHT_AZIMUTH)
|
||||||
|
|
||||||
const changeSurfaceFlowDirection = (roof, direction, orientation) => {
|
const changeSurfaceFlowDirection = (roof, direction, orientation) => {
|
||||||
|
saveSnapshot()
|
||||||
roof.set({
|
roof.set({
|
||||||
direction: direction,
|
direction: direction,
|
||||||
surfaceCompass: orientation,
|
surfaceCompass: orientation,
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { useObjectBatch } from '@/hooks/object/useObjectBatch'
|
|||||||
import { BATCH_TYPE } from '@/common/common'
|
import { BATCH_TYPE } from '@/common/common'
|
||||||
import { useMouse } from '@/hooks/useMouse'
|
import { useMouse } from '@/hooks/useMouse'
|
||||||
import { QPolygon } from '@/components/fabric/QPolygon'
|
import { QPolygon } from '@/components/fabric/QPolygon'
|
||||||
|
import { useUndoRedo } from '@/hooks/useUndoRedo'
|
||||||
|
|
||||||
export function useCommonUtils() {
|
export function useCommonUtils() {
|
||||||
const canvas = useRecoilValue(canvasState)
|
const canvas = useRecoilValue(canvasState)
|
||||||
@ -28,6 +29,7 @@ export function useCommonUtils() {
|
|||||||
const { drawDirectionArrow, addLengthText } = usePolygon()
|
const { drawDirectionArrow, addLengthText } = usePolygon()
|
||||||
const { applyDormers } = useObjectBatch({})
|
const { applyDormers } = useObjectBatch({})
|
||||||
const { getIntersectMousePoint } = useMouse()
|
const { getIntersectMousePoint } = useMouse()
|
||||||
|
const { saveSnapshot } = useUndoRedo()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
commonTextMode()
|
commonTextMode()
|
||||||
@ -834,6 +836,7 @@ export function useCommonUtils() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deleteObject = () => {
|
const deleteObject = () => {
|
||||||
|
saveSnapshot()
|
||||||
const selectedObj = canvas?.getActiveObjects()
|
const selectedObj = canvas?.getActiveObjects()
|
||||||
if (selectedObj) {
|
if (selectedObj) {
|
||||||
selectedObj.forEach((obj) => {
|
selectedObj.forEach((obj) => {
|
||||||
@ -863,6 +866,7 @@ export function useCommonUtils() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const copyObject = () => {
|
const copyObject = () => {
|
||||||
|
saveSnapshot()
|
||||||
const obj = canvas?.getActiveObject()
|
const obj = canvas?.getActiveObject()
|
||||||
commonCopyObject(obj)
|
commonCopyObject(obj)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { selectedModuleState } from '@/store/selectedModuleOptions'
|
|||||||
import { useCircuitTrestle } from '../useCirCuitTrestle'
|
import { useCircuitTrestle } from '../useCirCuitTrestle'
|
||||||
import { useTrestle } from './useTrestle'
|
import { useTrestle } from './useTrestle'
|
||||||
import { useTurf } from '@/hooks/common/useTurf'
|
import { useTurf } from '@/hooks/common/useTurf'
|
||||||
|
import { useUndoRedo } from '@/hooks/useUndoRedo'
|
||||||
|
|
||||||
export const MODULE_REMOVE_TYPE = {
|
export const MODULE_REMOVE_TYPE = {
|
||||||
LEFT: 'left',
|
LEFT: 'left',
|
||||||
@ -44,6 +45,7 @@ export function useModule() {
|
|||||||
const { setModuleStatisticsData, resetCircuits } = useCircuitTrestle()
|
const { setModuleStatisticsData, resetCircuits } = useCircuitTrestle()
|
||||||
const { clear: removeTrestleMaterials } = useTrestle()
|
const { clear: removeTrestleMaterials } = useTrestle()
|
||||||
const { checkModuleDisjointSurface } = useTurf()
|
const { checkModuleDisjointSurface } = useTurf()
|
||||||
|
const { saveSnapshot } = useUndoRedo()
|
||||||
|
|
||||||
// clone() 대신 직접 새 QPolygon을 생성하여 Maximum call stack 방지
|
// clone() 대신 직접 새 QPolygon을 생성하여 Maximum call stack 방지
|
||||||
const createModuleCopy = (module, newLeft, newTop, overrides = {}) => {
|
const createModuleCopy = (module, newLeft, newTop, overrides = {}) => {
|
||||||
@ -92,6 +94,7 @@ export function useModule() {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
saveSnapshot()
|
||||||
// canvas.discardActiveObject() //선택해제
|
// canvas.discardActiveObject() //선택해제
|
||||||
|
|
||||||
const isSetupModules = getOtherModules(selectedObj)
|
const isSetupModules = getOtherModules(selectedObj)
|
||||||
@ -171,6 +174,7 @@ export function useModule() {
|
|||||||
|
|
||||||
removeTrestleMaterials()
|
removeTrestleMaterials()
|
||||||
}
|
}
|
||||||
|
saveSnapshot()
|
||||||
const modules = type === 'row' ? getRowModules(activeModule) : getColumnModules(activeModule)
|
const modules = type === 'row' ? getRowModules(activeModule) : getColumnModules(activeModule)
|
||||||
const otherModules = getOtherModules(modules)
|
const otherModules = getOtherModules(modules)
|
||||||
const objects = getObjects()
|
const objects = getObjects()
|
||||||
@ -221,6 +225,7 @@ export function useModule() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const moduleMoveAll = (length, direction, surfaceArray) => {
|
const moduleMoveAll = (length, direction, surfaceArray) => {
|
||||||
|
saveSnapshot()
|
||||||
surfaceArray.forEach((surface) => {
|
surfaceArray.forEach((surface) => {
|
||||||
const modules = canvas
|
const modules = canvas
|
||||||
.getObjects()
|
.getObjects()
|
||||||
@ -471,6 +476,7 @@ export function useModule() {
|
|||||||
if (surfaces.some((surface) => surface.isComplete)) {
|
if (surfaces.some((surface) => surface.isComplete)) {
|
||||||
removeTrestleMaterials()
|
removeTrestleMaterials()
|
||||||
}
|
}
|
||||||
|
saveSnapshot()
|
||||||
canvas.discardActiveObject()
|
canvas.discardActiveObject()
|
||||||
moduleSetupSurface.set({ modules: otherModules })
|
moduleSetupSurface.set({ modules: otherModules })
|
||||||
canvas.remove(...columnModules)
|
canvas.remove(...columnModules)
|
||||||
@ -605,6 +611,7 @@ export function useModule() {
|
|||||||
if (surfaces.some((surface) => surface.isComplete)) {
|
if (surfaces.some((surface) => surface.isComplete)) {
|
||||||
removeTrestleMaterials()
|
removeTrestleMaterials()
|
||||||
}
|
}
|
||||||
|
saveSnapshot()
|
||||||
canvas.discardActiveObject()
|
canvas.discardActiveObject()
|
||||||
moduleSetupSurface.set({ modules: otherModules })
|
moduleSetupSurface.set({ modules: otherModules })
|
||||||
canvas.remove(...rowModules)
|
canvas.remove(...rowModules)
|
||||||
@ -969,6 +976,7 @@ export function useModule() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveSnapshot()
|
||||||
surface.set({ modules: modules })
|
surface.set({ modules: modules })
|
||||||
canvas.remove(activeModule)
|
canvas.remove(activeModule)
|
||||||
canvas.renderAll()
|
canvas.renderAll()
|
||||||
|
|||||||
@ -33,6 +33,7 @@ import { isObjectNotEmpty } from '@/util/common-utils'
|
|||||||
import { useCircuitTrestle } from '@/hooks/useCirCuitTrestle'
|
import { useCircuitTrestle } from '@/hooks/useCirCuitTrestle'
|
||||||
import { useMode } from '@/hooks/useMode'
|
import { useMode } from '@/hooks/useMode'
|
||||||
import { useTurf } from '@/hooks/common/useTurf'
|
import { useTurf } from '@/hooks/common/useTurf'
|
||||||
|
import { useUndoRedo } from '@/hooks/useUndoRedo'
|
||||||
|
|
||||||
export function useModuleBasicSetting(tabNum) {
|
export function useModuleBasicSetting(tabNum) {
|
||||||
const canvas = useRecoilValue(canvasState)
|
const canvas = useRecoilValue(canvasState)
|
||||||
@ -62,6 +63,7 @@ export function useModuleBasicSetting(tabNum) {
|
|||||||
const setModuleRowColArray = useSetRecoilState(moduleRowColArrayState)
|
const setModuleRowColArray = useSetRecoilState(moduleRowColArrayState)
|
||||||
|
|
||||||
const { checkModuleDisjointSurface } = useTurf()
|
const { checkModuleDisjointSurface } = useTurf()
|
||||||
|
const { saveSnapshot } = useUndoRedo()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initEvent()
|
initEvent()
|
||||||
@ -104,6 +106,7 @@ export function useModuleBasicSetting(tabNum) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
//육지붕 일경우에는 바로 배치면 설치LL
|
//육지붕 일경우에는 바로 배치면 설치LL
|
||||||
|
saveSnapshot()
|
||||||
canvas
|
canvas
|
||||||
.getObjects()
|
.getObjects()
|
||||||
.filter((roof) => roof.name === POLYGON_TYPE.ROOF)
|
.filter((roof) => roof.name === POLYGON_TYPE.ROOF)
|
||||||
@ -117,6 +120,7 @@ export function useModuleBasicSetting(tabNum) {
|
|||||||
//가대 상세 데이터 들어오면 실행
|
//가대 상세 데이터 들어오면 실행
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (trestleDetailList.length > 0) {
|
if (trestleDetailList.length > 0) {
|
||||||
|
saveSnapshot()
|
||||||
let rowColArray = []
|
let rowColArray = []
|
||||||
//지붕을 가져옴
|
//지붕을 가져옴
|
||||||
canvas
|
canvas
|
||||||
@ -570,7 +574,7 @@ export function useModuleBasicSetting(tabNum) {
|
|||||||
*/
|
*/
|
||||||
const manualModuleSetup = () => {
|
const manualModuleSetup = () => {
|
||||||
//레이아웃 수동설치 토글
|
//레이아웃 수동설치 토글
|
||||||
const moduleSetupSurfaces = canvas?.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE) //모듈설치면를 가져옴
|
let moduleSetupSurfaces = canvas?.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE) //모듈설치면를 가져옴
|
||||||
const isChidori = moduleSetupOption.isChidori
|
const isChidori = moduleSetupOption.isChidori
|
||||||
|
|
||||||
if (isManualModuleSetup) {
|
if (isManualModuleSetup) {
|
||||||
@ -626,6 +630,9 @@ export function useModuleBasicSetting(tabNum) {
|
|||||||
//마우스 이벤트 삭제 후 재추가
|
//마우스 이벤트 삭제 후 재추가
|
||||||
const mousePoint = canvas.getPointer(e.e)
|
const mousePoint = canvas.getPointer(e.e)
|
||||||
|
|
||||||
|
// undo 후 캔버스 객체가 교체될 수 있으므로 매번 다시 가져옴
|
||||||
|
moduleSetupSurfaces = canvas?.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE) || []
|
||||||
|
|
||||||
for (let i = 0; i < moduleSetupSurfaces.length; i++) {
|
for (let i = 0; i < moduleSetupSurfaces.length; i++) {
|
||||||
//배치면이 여러개 일때 옮겨가면서 동작해야함
|
//배치면이 여러개 일때 옮겨가면서 동작해야함
|
||||||
const moduleSetupSurface = moduleSetupSurfaces[i]
|
const moduleSetupSurface = moduleSetupSurfaces[i]
|
||||||
@ -916,8 +923,10 @@ export function useModuleBasicSetting(tabNum) {
|
|||||||
addCanvasMouseEventListener('mouse:up', (e) => {
|
addCanvasMouseEventListener('mouse:up', (e) => {
|
||||||
let isIntersection = true
|
let isIntersection = true
|
||||||
|
|
||||||
|
if (!trestlePolygon) return
|
||||||
|
|
||||||
// 혼합 설치 불가능한 모듈인지 확인 Y는 Y끼리 N은 N끼리만 설치 가능
|
// 혼합 설치 불가능한 모듈인지 확인 Y는 Y끼리 N은 N끼리만 설치 가능
|
||||||
if (trestlePolygon.modules.length > 0) {
|
if (trestlePolygon.modules && trestlePolygon.modules.length > 0) {
|
||||||
//이미 설치된 모듈중에 한개만 가져옴
|
//이미 설치된 모듈중에 한개만 가져옴
|
||||||
const mixAsgYn = trestlePolygon.modules[0].moduleInfo.mixAsgYn
|
const mixAsgYn = trestlePolygon.modules[0].moduleInfo.mixAsgYn
|
||||||
//현재 체크된 모듈기준으로 혼합가능인지 확인 Y === Y, N === N 일때만 설치 가능
|
//현재 체크된 모듈기준으로 혼합가능인지 확인 Y === Y, N === N 일때만 설치 가능
|
||||||
@ -951,9 +960,10 @@ export function useModuleBasicSetting(tabNum) {
|
|||||||
tempModule.set({ points: rectPoints })
|
tempModule.set({ points: rectPoints })
|
||||||
const tempTurfModule = polygonToTurfPolygon(tempModule)
|
const tempTurfModule = polygonToTurfPolygon(tempModule)
|
||||||
|
|
||||||
//도머 객체를 가져옴
|
//도머 객체를 가져옴 (undo 후 캔버스 객체가 교체될 수 있으므로 다시 가져옴)
|
||||||
if (batchObjects) {
|
const currentBatchObjects = canvas?.getObjects().filter((obj) => obj.name === POLYGON_TYPE.OBJECT_SURFACE)
|
||||||
batchObjects.forEach((object) => {
|
if (currentBatchObjects) {
|
||||||
|
currentBatchObjects.forEach((object) => {
|
||||||
let dormerTurfPolygon = polygonToTurfPolygon(object, true)
|
let dormerTurfPolygon = polygonToTurfPolygon(object, true)
|
||||||
const intersection = turf.intersect(turf.featureCollection([dormerTurfPolygon, tempTurfModule])) //겹치는지 확인
|
const intersection = turf.intersect(turf.featureCollection([dormerTurfPolygon, tempTurfModule])) //겹치는지 확인
|
||||||
//겹치면 안됨
|
//겹치면 안됨
|
||||||
@ -989,6 +999,7 @@ export function useModuleBasicSetting(tabNum) {
|
|||||||
toFixed: 2,
|
toFixed: 2,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
saveSnapshot()
|
||||||
canvas?.add(manualModule)
|
canvas?.add(manualModule)
|
||||||
canvas.bringToFront(manualModule)
|
canvas.bringToFront(manualModule)
|
||||||
manualDrawModules.push(manualModule)
|
manualDrawModules.push(manualModule)
|
||||||
@ -1026,7 +1037,7 @@ export function useModuleBasicSetting(tabNum) {
|
|||||||
|
|
||||||
//단 열수 지정배치
|
//단 열수 지정배치
|
||||||
const manualModuleLayoutSetup = (layoutSetupRef) => {
|
const manualModuleLayoutSetup = (layoutSetupRef) => {
|
||||||
const moduleSetupSurfaces = canvas?.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE) //모듈설치면를 가져옴
|
let moduleSetupSurfaces = canvas?.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE) //모듈설치면를 가져옴
|
||||||
|
|
||||||
const isChidori = moduleSetupOption.isChidori //치도리 여부
|
const isChidori = moduleSetupOption.isChidori //치도리 여부
|
||||||
const setupLocation = moduleSetupOption.setupLocation //모듈 설치 위치 처마, 용마루
|
const setupLocation = moduleSetupOption.setupLocation //모듈 설치 위치 처마, 용마루
|
||||||
@ -1124,6 +1135,9 @@ export function useModuleBasicSetting(tabNum) {
|
|||||||
//마우스 이벤트 삭제 후 재추가
|
//마우스 이벤트 삭제 후 재추가
|
||||||
const mousePoint = canvas.getPointer(e.e)
|
const mousePoint = canvas.getPointer(e.e)
|
||||||
|
|
||||||
|
// undo 후 캔버스 객체가 교체될 수 있으므로 매번 다시 가져옴
|
||||||
|
moduleSetupSurfaces = canvas?.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE) || []
|
||||||
|
|
||||||
for (let i = 0; i < moduleSetupSurfaces.length; i++) {
|
for (let i = 0; i < moduleSetupSurfaces.length; i++) {
|
||||||
//배치면이 여러개 일때 옮겨가면서 동작해야함
|
//배치면이 여러개 일때 옮겨가면서 동작해야함
|
||||||
const moduleSetupSurface = moduleSetupSurfaces[i]
|
const moduleSetupSurface = moduleSetupSurfaces[i]
|
||||||
@ -1401,7 +1415,9 @@ export function useModuleBasicSetting(tabNum) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
addCanvasMouseEventListener('mouse:up', (e) => {
|
addCanvasMouseEventListener('mouse:up', (e) => {
|
||||||
if (trestlePolygon.modules.length > 0) {
|
if (!trestlePolygon) return
|
||||||
|
|
||||||
|
if (trestlePolygon.modules && trestlePolygon.modules.length > 0) {
|
||||||
//이미 설치된 모듈중에 한개만 가져옴
|
//이미 설치된 모듈중에 한개만 가져옴
|
||||||
const mixAsgYn = trestlePolygon.modules[0].moduleInfo.mixAsgYn
|
const mixAsgYn = trestlePolygon.modules[0].moduleInfo.mixAsgYn
|
||||||
//현재 체크된 모듈기준으로 혼합가능인지 확인 Y === Y, N === N 일때만 설치 가능
|
//현재 체크된 모듈기준으로 혼합가능인지 확인 Y === Y, N === N 일때만 설치 가능
|
||||||
@ -1455,6 +1471,7 @@ export function useModuleBasicSetting(tabNum) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tempModule) {
|
if (tempModule) {
|
||||||
|
saveSnapshot()
|
||||||
let startX, startY
|
let startX, startY
|
||||||
let installedLastHeightCoord = 0 //마지막으로 설치된 모듈의 좌표
|
let installedLastHeightCoord = 0 //마지막으로 설치된 모듈의 좌표
|
||||||
let installedHeightModuleCount = 0
|
let installedHeightModuleCount = 0
|
||||||
@ -2913,6 +2930,7 @@ export function useModuleBasicSetting(tabNum) {
|
|||||||
return orderA - orderB
|
return orderA - orderB
|
||||||
})
|
})
|
||||||
|
|
||||||
|
saveSnapshot()
|
||||||
sortedModuleSetupSurfaces.forEach((moduleSetupSurface, index) => {
|
sortedModuleSetupSurfaces.forEach((moduleSetupSurface, index) => {
|
||||||
moduleSetupSurface.fire('mousedown')
|
moduleSetupSurface.fire('mousedown')
|
||||||
const moduleSetupArray = []
|
const moduleSetupArray = []
|
||||||
@ -3740,6 +3758,7 @@ export function useModuleBasicSetting(tabNum) {
|
|||||||
canvas?.add(manualModule)
|
canvas?.add(manualModule)
|
||||||
manualDrawModules.push(manualModule)
|
manualDrawModules.push(manualModule)
|
||||||
setModuleStatisticsData()
|
setModuleStatisticsData()
|
||||||
|
saveSnapshot()
|
||||||
} else {
|
} else {
|
||||||
swalFire({ text: getMessage('module.place.overlab'), icon: 'warning' })
|
swalFire({ text: getMessage('module.place.overlab'), icon: 'warning' })
|
||||||
}
|
}
|
||||||
@ -3751,6 +3770,7 @@ export function useModuleBasicSetting(tabNum) {
|
|||||||
}
|
}
|
||||||
// getModuleStatistics()
|
// getModuleStatistics()
|
||||||
setModuleStatisticsData()
|
setModuleStatisticsData()
|
||||||
|
saveSnapshot()
|
||||||
} else {
|
} else {
|
||||||
if (moduleSetupSurfaces) {
|
if (moduleSetupSurfaces) {
|
||||||
//수동모드 해제시 모듈 설치면 선택 잠금
|
//수동모드 해제시 모듈 설치면 선택 잠금
|
||||||
@ -4059,6 +4079,7 @@ export function useModuleBasicSetting(tabNum) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveSnapshot()
|
||||||
moduleSetupSurfaces.forEach((moduleSetupSurface, index) => {
|
moduleSetupSurfaces.forEach((moduleSetupSurface, index) => {
|
||||||
moduleSetupSurface.fire('mousedown')
|
moduleSetupSurface.fire('mousedown')
|
||||||
const moduleSetupArray = []
|
const moduleSetupArray = []
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import { usePopup } from '@/hooks/usePopup'
|
|||||||
import { useMouse } from '@/hooks/useMouse'
|
import { useMouse } from '@/hooks/useMouse'
|
||||||
import { useTurf } from '@/hooks/common/useTurf'
|
import { useTurf } from '@/hooks/common/useTurf'
|
||||||
import SizeSetting from '@/components/floor-plan/modal/object/SizeSetting'
|
import SizeSetting from '@/components/floor-plan/modal/object/SizeSetting'
|
||||||
|
import { useUndoRedo } from '@/hooks/useUndoRedo'
|
||||||
|
|
||||||
export function useObjectBatch({ isHidden, setIsHidden }) {
|
export function useObjectBatch({ isHidden, setIsHidden }) {
|
||||||
const { getMessage } = useMessage()
|
const { getMessage } = useMessage()
|
||||||
@ -41,6 +42,7 @@ export function useObjectBatch({ isHidden, setIsHidden }) {
|
|||||||
const { getIntersectMousePoint } = useMouse()
|
const { getIntersectMousePoint } = useMouse()
|
||||||
const { checkModuleDisjointSurface } = useTurf()
|
const { checkModuleDisjointSurface } = useTurf()
|
||||||
const currentObject = useRecoilValue(currentObjectState)
|
const currentObject = useRecoilValue(currentObjectState)
|
||||||
|
const { saveSnapshot } = useUndoRedo()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (canvas) {
|
if (canvas) {
|
||||||
@ -99,6 +101,8 @@ export function useObjectBatch({ isHidden, setIsHidden }) {
|
|||||||
let rect, isDown, origX, origY
|
let rect, isDown, origX, origY
|
||||||
|
|
||||||
let selectedSurface
|
let selectedSurface
|
||||||
|
|
||||||
|
saveSnapshot()
|
||||||
//프리입력
|
//프리입력
|
||||||
|
|
||||||
if (selectedType === INPUT_TYPE.FREE) {
|
if (selectedType === INPUT_TYPE.FREE) {
|
||||||
@ -321,6 +325,8 @@ export function useObjectBatch({ isHidden, setIsHidden }) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveSnapshot()
|
||||||
|
|
||||||
let theta = 0
|
let theta = 0
|
||||||
|
|
||||||
let bottomLength = 0,
|
let bottomLength = 0,
|
||||||
@ -1296,6 +1302,7 @@ export function useObjectBatch({ isHidden, setIsHidden }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resizeObjectBatch = (side, target, width, height, popupId) => {
|
const resizeObjectBatch = (side, target, width, height, popupId) => {
|
||||||
|
saveSnapshot()
|
||||||
const objectWidth = target.width
|
const objectWidth = target.width
|
||||||
const objectHeight = target.height
|
const objectHeight = target.height
|
||||||
|
|
||||||
@ -1417,6 +1424,7 @@ export function useObjectBatch({ isHidden, setIsHidden }) {
|
|||||||
const parentSurface = canvas?.getObjects().filter((item) => item.name === POLYGON_TYPE.ROOF && item.id === obj.parentId)[0]
|
const parentSurface = canvas?.getObjects().filter((item) => item.name === POLYGON_TYPE.ROOF && item.id === obj.parentId)[0]
|
||||||
|
|
||||||
if (obj) {
|
if (obj) {
|
||||||
|
saveSnapshot()
|
||||||
obj.set({
|
obj.set({
|
||||||
lockMovementX: false,
|
lockMovementX: false,
|
||||||
lockMovementY: false,
|
lockMovementY: false,
|
||||||
@ -1505,6 +1513,7 @@ export function useObjectBatch({ isHidden, setIsHidden }) {
|
|||||||
const copyObjectBatch = () => {
|
const copyObjectBatch = () => {
|
||||||
const obj = canvas.getActiveObject()
|
const obj = canvas.getActiveObject()
|
||||||
if (obj) {
|
if (obj) {
|
||||||
|
saveSnapshot()
|
||||||
let clonedObj = null
|
let clonedObj = null
|
||||||
const parentSurface = canvas?.getObjects().filter((item) => item.name === POLYGON_TYPE.ROOF && item.id === obj.parentId)[0]
|
const parentSurface = canvas?.getObjects().filter((item) => item.name === POLYGON_TYPE.ROOF && item.id === obj.parentId)[0]
|
||||||
|
|
||||||
@ -1583,6 +1592,7 @@ export function useObjectBatch({ isHidden, setIsHidden }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dormerOffset = (arrow1, arrow2, length1, length2) => {
|
const dormerOffset = (arrow1, arrow2, length1, length2) => {
|
||||||
|
saveSnapshot()
|
||||||
length1 = parseInt(length1) / 10
|
length1 = parseInt(length1) / 10
|
||||||
length2 = parseInt(length2) / 10
|
length2 = parseInt(length2) / 10
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { usePopup } from '@/hooks/usePopup'
|
|||||||
import { calculateAngle, isSamePoint } from '@/util/qpolygon-utils'
|
import { calculateAngle, isSamePoint } from '@/util/qpolygon-utils'
|
||||||
import { POLYGON_TYPE } from '@/common/common'
|
import { POLYGON_TYPE } from '@/common/common'
|
||||||
import { useMessage } from '../useMessage'
|
import { useMessage } from '../useMessage'
|
||||||
|
import { useUndoRedo } from '@/hooks/useUndoRedo'
|
||||||
import {
|
import {
|
||||||
auxiliaryLineAngle1State,
|
auxiliaryLineAngle1State,
|
||||||
auxiliaryLineAngle2State,
|
auxiliaryLineAngle2State,
|
||||||
@ -36,6 +37,7 @@ export function useAuxiliaryDrawing(id, isUseEffect = true) {
|
|||||||
const { getAdsorptionPoints } = useAdsorptionPoint()
|
const { getAdsorptionPoints } = useAdsorptionPoint()
|
||||||
const { closePopup } = usePopup()
|
const { closePopup } = usePopup()
|
||||||
const { getMessage } = useMessage()
|
const { getMessage } = useMessage()
|
||||||
|
const { saveSnapshot } = useUndoRedo()
|
||||||
|
|
||||||
const adsorptionRange = useRecoilValue(adsorptionRangeState)
|
const adsorptionRange = useRecoilValue(adsorptionRangeState)
|
||||||
|
|
||||||
@ -652,6 +654,8 @@ export function useAuxiliaryDrawing(id, isUseEffect = true) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveSnapshot()
|
||||||
|
|
||||||
const roofBases = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF)
|
const roofBases = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF)
|
||||||
/*const allLines = [...auxiliaryLines]
|
/*const allLines = [...auxiliaryLines]
|
||||||
|
|
||||||
@ -883,6 +887,18 @@ export function useAuxiliaryDrawing(id, isUseEffect = true) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lineHistory가 비어있으면 캔버스에서 직접 미확정 보조선을 찾아 제거
|
||||||
|
if (lineHistory.current.length === 0) {
|
||||||
|
const unfixedLines = canvas.getObjects().filter((obj) => obj.name === 'auxiliaryLine' && !obj.isAuxiliaryFixed)
|
||||||
|
if (unfixedLines.length > 0) {
|
||||||
|
const lastLine = unfixedLines[unfixedLines.length - 1]
|
||||||
|
canvas.remove(lastLine)
|
||||||
|
canvas.renderAll()
|
||||||
|
}
|
||||||
|
addCanvasMouseEventListener('mouse:move', mouseMove)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const lastLine = lineHistory.current.pop()
|
const lastLine = lineHistory.current.pop()
|
||||||
if (lastLine) {
|
if (lastLine) {
|
||||||
roofAdsorptionPoints.current = roofAdsorptionPoints.current.filter(
|
roofAdsorptionPoints.current = roofAdsorptionPoints.current.filter(
|
||||||
@ -901,6 +917,7 @@ export function useAuxiliaryDrawing(id, isUseEffect = true) {
|
|||||||
if (!confirm(getMessage('want.to.complete.auxiliary.creation'))) {
|
if (!confirm(getMessage('want.to.complete.auxiliary.creation'))) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
saveSnapshot()
|
||||||
|
|
||||||
// cutAuxiliary()
|
// cutAuxiliary()
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { useSwal } from '@/hooks/useSwal'
|
|||||||
import { usePopup } from '@/hooks/usePopup'
|
import { usePopup } from '@/hooks/usePopup'
|
||||||
import { getChonByDegree } from '@/util/canvas-util'
|
import { getChonByDegree } from '@/util/canvas-util'
|
||||||
import { settingModalFirstOptionsState } from '@/store/settingAtom'
|
import { settingModalFirstOptionsState } from '@/store/settingAtom'
|
||||||
|
import { useUndoRedo } from '@/hooks/useUndoRedo'
|
||||||
|
|
||||||
// 처마.케라바 변경
|
// 처마.케라바 변경
|
||||||
export function useEavesGableEdit(id) {
|
export function useEavesGableEdit(id) {
|
||||||
@ -29,6 +30,7 @@ export function useEavesGableEdit(id) {
|
|||||||
const typeRef = useRef(TYPES.EAVES)
|
const typeRef = useRef(TYPES.EAVES)
|
||||||
const { removeLine, addPitchTextsByOuterLines } = useLine()
|
const { removeLine, addPitchTextsByOuterLines } = useLine()
|
||||||
const { swalFire } = useSwal()
|
const { swalFire } = useSwal()
|
||||||
|
const { saveSnapshot } = useUndoRedo()
|
||||||
|
|
||||||
const { drawRoofPolygon } = useMode()
|
const { drawRoofPolygon } = useMode()
|
||||||
const currentAngleType = useRecoilValue(currentAngleTypeSelector)
|
const currentAngleType = useRecoilValue(currentAngleTypeSelector)
|
||||||
@ -210,6 +212,7 @@ export function useEavesGableEdit(id) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveSnapshot()
|
||||||
target.set({
|
target.set({
|
||||||
attributes,
|
attributes,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import Big from 'big.js'
|
|||||||
import { calcLinePlaneSize } from '@/util/qpolygon-utils'
|
import { calcLinePlaneSize } from '@/util/qpolygon-utils'
|
||||||
import { getSelectLinePosition } from '@/util/skeleton-utils'
|
import { getSelectLinePosition } from '@/util/skeleton-utils'
|
||||||
import { useMouse } from '@/hooks/useMouse'
|
import { useMouse } from '@/hooks/useMouse'
|
||||||
|
import { useUndoRedo } from '@/hooks/useUndoRedo'
|
||||||
|
|
||||||
//동선이동 형 올림 내림
|
//동선이동 형 올림 내림
|
||||||
export function useMovementSetting(id) {
|
export function useMovementSetting(id) {
|
||||||
@ -31,6 +32,7 @@ export function useMovementSetting(id) {
|
|||||||
const [type, setType] = useState(TYPE.FLOW_LINE)
|
const [type, setType] = useState(TYPE.FLOW_LINE)
|
||||||
const typeRef = useRef(type)
|
const typeRef = useRef(type)
|
||||||
const { swalFire } = useSwal()
|
const { swalFire } = useSwal()
|
||||||
|
const { saveSnapshot } = useUndoRedo()
|
||||||
|
|
||||||
const FLOW_LINE_REF = {
|
const FLOW_LINE_REF = {
|
||||||
POINTER_INPUT_REF: useRef(null),
|
POINTER_INPUT_REF: useRef(null),
|
||||||
@ -464,6 +466,7 @@ export function useMovementSetting(id) {
|
|||||||
canvas.renderAll()
|
canvas.renderAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveSnapshot()
|
||||||
const target = selectedObject.current !== null ? selectedObject.current : CONFIRM_LINE_REF.current?.target
|
const target = selectedObject.current !== null ? selectedObject.current : CONFIRM_LINE_REF.current?.target
|
||||||
if (!target) return
|
if (!target) return
|
||||||
|
|
||||||
|
|||||||
@ -33,6 +33,7 @@ import { usePopup } from '@/hooks/usePopup'
|
|||||||
import Big from 'big.js'
|
import Big from 'big.js'
|
||||||
import RoofShapeSetting from '@/components/floor-plan/modal/roofShape/RoofShapeSetting'
|
import RoofShapeSetting from '@/components/floor-plan/modal/roofShape/RoofShapeSetting'
|
||||||
import { useObject } from '@/hooks/useObject'
|
import { useObject } from '@/hooks/useObject'
|
||||||
|
import { useUndoRedo } from '@/hooks/useUndoRedo'
|
||||||
|
|
||||||
//외벽선 그리기
|
//외벽선 그리기
|
||||||
export function useOuterLineWall(id, propertiesId) {
|
export function useOuterLineWall(id, propertiesId) {
|
||||||
@ -58,6 +59,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 verticalHorizontalMode = useRecoilValue(verticalHorizontalModeState)
|
const verticalHorizontalMode = useRecoilValue(verticalHorizontalModeState)
|
||||||
const adsorptionPointAddMode = useRecoilValue(adsorptionPointAddModeState)
|
const adsorptionPointAddMode = useRecoilValue(adsorptionPointAddModeState)
|
||||||
@ -149,6 +151,7 @@ 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()
|
||||||
setPoints((prev) => [...prev, pointer])
|
setPoints((prev) => [...prev, pointer])
|
||||||
} else {
|
} else {
|
||||||
const lastPoint = points[points.length - 1]
|
const lastPoint = points[points.length - 1]
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import RoofAllocationSetting from '@/components/floor-plan/modal/roofAllocation/
|
|||||||
import { settingModalFirstOptionsState } from '@/store/settingAtom'
|
import { settingModalFirstOptionsState } from '@/store/settingAtom'
|
||||||
import { useSwal } from '@/hooks/useSwal'
|
import { useSwal } from '@/hooks/useSwal'
|
||||||
import { useMessage } from '@/hooks/useMessage'
|
import { useMessage } from '@/hooks/useMessage'
|
||||||
|
import { useUndoRedo } from '@/hooks/useUndoRedo'
|
||||||
|
|
||||||
// 외벽선 속성 설정
|
// 외벽선 속성 설정
|
||||||
export function usePropertiesSetting(id) {
|
export function usePropertiesSetting(id) {
|
||||||
@ -29,6 +30,7 @@ export function usePropertiesSetting(id) {
|
|||||||
const { addPopup, closePopup } = usePopup()
|
const { addPopup, closePopup } = usePopup()
|
||||||
const { swalFire } = useSwal()
|
const { swalFire } = useSwal()
|
||||||
const { getMessage } = useMessage()
|
const { getMessage } = useMessage()
|
||||||
|
const { saveSnapshot } = useUndoRedo()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const lines = canvas.getObjects().filter((obj) => obj.name === 'outerLine')
|
const lines = canvas.getObjects().filter((obj) => obj.name === 'outerLine')
|
||||||
@ -67,6 +69,7 @@ export function usePropertiesSetting(id) {
|
|||||||
if (!selectedLine) {
|
if (!selectedLine) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
saveSnapshot()
|
||||||
selectedLine.set({
|
selectedLine.set({
|
||||||
stroke: '#45CD7D',
|
stroke: '#45CD7D',
|
||||||
strokeWidth: 4,
|
strokeWidth: 4,
|
||||||
@ -86,6 +89,7 @@ export function usePropertiesSetting(id) {
|
|||||||
if (!selectedLine) {
|
if (!selectedLine) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
saveSnapshot()
|
||||||
selectedLine.set({
|
selectedLine.set({
|
||||||
stroke: '#3FBAE6',
|
stroke: '#3FBAE6',
|
||||||
strokeWidth: 4,
|
strokeWidth: 4,
|
||||||
@ -150,6 +154,7 @@ export function usePropertiesSetting(id) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveSnapshot()
|
||||||
lines.forEach((line) => {
|
lines.forEach((line) => {
|
||||||
line.set({
|
line.set({
|
||||||
attributes: line.attributes ? line.attributes : { offset: 0, type: LINE_TYPE.WALLLINE.WALL },
|
attributes: line.attributes ? line.attributes : { offset: 0, type: LINE_TYPE.WALLLINE.WALL },
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import { usePlan } from '@/hooks/usePlan'
|
|||||||
import { roofsState } from '@/store/roofAtom'
|
import { roofsState } from '@/store/roofAtom'
|
||||||
import { useText } from '@/hooks/useText'
|
import { useText } from '@/hooks/useText'
|
||||||
import { QLine } from '@/components/fabric/QLine'
|
import { QLine } from '@/components/fabric/QLine'
|
||||||
|
import { useUndoRedo } from '@/hooks/useUndoRedo'
|
||||||
|
|
||||||
export function useRoofAllocationSetting(id) {
|
export function useRoofAllocationSetting(id) {
|
||||||
const canvas = useRecoilValue(canvasState)
|
const canvas = useRecoilValue(canvasState)
|
||||||
@ -65,6 +66,7 @@ export function useRoofAllocationSetting(id) {
|
|||||||
const resetPoints = useResetRecoilState(outerLinePointsState)
|
const resetPoints = useResetRecoilState(outerLinePointsState)
|
||||||
const [corridorDimension, setCorridorDimension] = useRecoilState(corridorDimensionSelector)
|
const [corridorDimension, setCorridorDimension] = useRecoilState(corridorDimensionSelector)
|
||||||
const { changeCorridorDimensionText } = useText()
|
const { changeCorridorDimensionText } = useText()
|
||||||
|
const { saveSnapshot } = useUndoRedo()
|
||||||
const outlineDisplay = useRecoilValue(outlineDisplaySelector)
|
const outlineDisplay = useRecoilValue(outlineDisplaySelector)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -440,6 +442,7 @@ export function useRoofAllocationSetting(id) {
|
|||||||
* 지붕면 할당
|
* 지붕면 할당
|
||||||
*/
|
*/
|
||||||
const apply = () => {
|
const apply = () => {
|
||||||
|
saveSnapshot()
|
||||||
const roofBases = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF && !obj.roofMaterial)
|
const roofBases = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF && !obj.roofMaterial)
|
||||||
const wallLines = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.WALL)
|
const wallLines = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.WALL)
|
||||||
roofBases.forEach((roofBase) => {
|
roofBases.forEach((roofBase) => {
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { usePolygon } from '@/hooks/usePolygon'
|
|||||||
import { useSwal } from '@/hooks/useSwal'
|
import { useSwal } from '@/hooks/useSwal'
|
||||||
import { usePopup } from '@/hooks/usePopup'
|
import { usePopup } from '@/hooks/usePopup'
|
||||||
import { getChonByDegree } from '@/util/canvas-util'
|
import { getChonByDegree } from '@/util/canvas-util'
|
||||||
|
import { useUndoRedo } from '@/hooks/useUndoRedo'
|
||||||
|
|
||||||
//지붕형상 수동 설정
|
//지붕형상 수동 설정
|
||||||
export function useRoofShapePassivitySetting(id) {
|
export function useRoofShapePassivitySetting(id) {
|
||||||
@ -39,6 +40,7 @@ export function useRoofShapePassivitySetting(id) {
|
|||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
const { closePopup } = usePopup()
|
const { closePopup } = usePopup()
|
||||||
|
const { saveSnapshot } = useUndoRedo()
|
||||||
const buttons = [
|
const buttons = [
|
||||||
{ id: 1, name: getMessage('eaves'), type: TYPES.EAVES },
|
{ id: 1, name: getMessage('eaves'), type: TYPES.EAVES },
|
||||||
{ id: 2, name: getMessage('gable'), type: TYPES.GABLE },
|
{ id: 2, name: getMessage('gable'), type: TYPES.GABLE },
|
||||||
@ -166,6 +168,7 @@ export function useRoofShapePassivitySetting(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveSnapshot()
|
||||||
currentLineRef.current.set({
|
currentLineRef.current.set({
|
||||||
attributes: { ...attributes, isFixed: true },
|
attributes: { ...attributes, isFixed: true },
|
||||||
})
|
})
|
||||||
@ -191,6 +194,7 @@ export function useRoofShapePassivitySetting(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
|
saveSnapshot()
|
||||||
isFix.current = true
|
isFix.current = true
|
||||||
handleLineToPolygon()
|
handleLineToPolygon()
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import {
|
|||||||
} from '@/store/settingAtom'
|
} from '@/store/settingAtom'
|
||||||
import { useAxios } from '@/hooks/useAxios'
|
import { useAxios } from '@/hooks/useAxios'
|
||||||
import { globalLocaleStore } from '@/store/localeAtom'
|
import { globalLocaleStore } from '@/store/localeAtom'
|
||||||
|
import { useUndoRedo } from '@/hooks/useUndoRedo'
|
||||||
|
|
||||||
// 지붕형상 설정
|
// 지붕형상 설정
|
||||||
export function useRoofShapeSetting(id) {
|
export function useRoofShapeSetting(id) {
|
||||||
@ -69,6 +70,7 @@ export function useRoofShapeSetting(id) {
|
|||||||
const { post } = useAxios(globalLocaleState)
|
const { post } = useAxios(globalLocaleState)
|
||||||
|
|
||||||
const { addLine, removeLine } = useLine()
|
const { addLine, removeLine } = useLine()
|
||||||
|
const { saveSnapshot } = useUndoRedo()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
pitchRef.current = currentAngleType === ANGLE_TYPE.SLOPE ? pitch : getChonByDegree(pitch)
|
pitchRef.current = currentAngleType === ANGLE_TYPE.SLOPE ? pitch : getChonByDegree(pitch)
|
||||||
@ -182,6 +184,7 @@ export function useRoofShapeSetting(id) {
|
|||||||
]
|
]
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
|
saveSnapshot()
|
||||||
let outerLines = canvas.getObjects().filter((obj) => obj.name === 'outerLine')
|
let outerLines = canvas.getObjects().filter((obj) => obj.name === 'outerLine')
|
||||||
let direction
|
let direction
|
||||||
|
|
||||||
@ -661,6 +664,7 @@ export function useRoofShapeSetting(id) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveSnapshot()
|
||||||
let attributes
|
let attributes
|
||||||
switch (buttonAct) {
|
switch (buttonAct) {
|
||||||
case 1: {
|
case 1: {
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import { useSurfaceShapeBatch } from './useSurfaceShapeBatch'
|
|||||||
import { roofDisplaySelector } from '@/store/settingAtom'
|
import { roofDisplaySelector } from '@/store/settingAtom'
|
||||||
import { useRoofFn } from '@/hooks/common/useRoofFn'
|
import { useRoofFn } from '@/hooks/common/useRoofFn'
|
||||||
import { useObject } from '@/hooks/useObject'
|
import { useObject } from '@/hooks/useObject'
|
||||||
|
import { useUndoRedo } from '@/hooks/useUndoRedo'
|
||||||
|
|
||||||
// 배치면 그리기
|
// 배치면 그리기
|
||||||
export function usePlacementShapeDrawing(id) {
|
export function usePlacementShapeDrawing(id) {
|
||||||
@ -74,6 +75,7 @@ export function usePlacementShapeDrawing(id) {
|
|||||||
const isFix = useRef(false)
|
const isFix = useRef(false)
|
||||||
|
|
||||||
const { closePopup, addPopup } = usePopup()
|
const { closePopup, addPopup } = usePopup()
|
||||||
|
const { saveSnapshot } = useUndoRedo()
|
||||||
|
|
||||||
const globalPitch = useRecoilValue(globalPitchState)
|
const globalPitch = useRecoilValue(globalPitchState)
|
||||||
|
|
||||||
@ -130,6 +132,7 @@ export function usePlacementShapeDrawing(id) {
|
|||||||
let pointer = getIntersectMousePoint(e)
|
let pointer = getIntersectMousePoint(e)
|
||||||
|
|
||||||
if (points.length === 0) {
|
if (points.length === 0) {
|
||||||
|
saveSnapshot()
|
||||||
setPoints((prev) => [...prev, pointer])
|
setPoints((prev) => [...prev, pointer])
|
||||||
} else {
|
} else {
|
||||||
const lastPoint = points[points.length - 1]
|
const lastPoint = points[points.length - 1]
|
||||||
@ -142,9 +145,17 @@ export function usePlacementShapeDrawing(id) {
|
|||||||
const slope = Math.abs(vector.y / vector.x)
|
const slope = Math.abs(vector.y / vector.x)
|
||||||
|
|
||||||
if (slope >= 1) {
|
if (slope >= 1) {
|
||||||
newPoint = { x: lastPoint.x, y: pointer.y }
|
// 기울기가 1 이상이면 수직으로 제한
|
||||||
|
newPoint = {
|
||||||
|
x: lastPoint.x,
|
||||||
|
y: pointer.y,
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
newPoint = { x: pointer.x, y: lastPoint.y }
|
// 기울기가 1 미만이면 수평으로 제한
|
||||||
|
newPoint = {
|
||||||
|
x: pointer.x,
|
||||||
|
y: lastPoint.y,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setPoints((prev) => [...prev, newPoint])
|
setPoints((prev) => [...prev, newPoint])
|
||||||
@ -302,9 +313,9 @@ export function usePlacementShapeDrawing(id) {
|
|||||||
// 기존 도형의 변과 일치하는 경우 planeSize를 상속하는 함수
|
// 기존 도형의 변과 일치하는 경우 planeSize를 상속하는 함수
|
||||||
const inheritPlaneSizeFromExistingShapes = (newPolygon) => {
|
const inheritPlaneSizeFromExistingShapes = (newPolygon) => {
|
||||||
const tolerance = 2
|
const tolerance = 2
|
||||||
const existingPolygons = canvas.getObjects().filter(
|
const existingPolygons = canvas
|
||||||
(obj) => (obj.name === POLYGON_TYPE.ROOF || obj.name === POLYGON_TYPE.WALL) && obj.id !== newPolygon.id && obj.lines,
|
.getObjects()
|
||||||
)
|
.filter((obj) => (obj.name === POLYGON_TYPE.ROOF || obj.name === POLYGON_TYPE.WALL) && obj.id !== newPolygon.id && obj.lines)
|
||||||
|
|
||||||
if (existingPolygons.length === 0) return
|
if (existingPolygons.length === 0) return
|
||||||
|
|
||||||
@ -330,15 +341,9 @@ export function usePlacementShapeDrawing(id) {
|
|||||||
|
|
||||||
// 1순위: 양 끝점이 정확히 일치
|
// 1순위: 양 끝점이 정확히 일치
|
||||||
const forwardMatch =
|
const forwardMatch =
|
||||||
Math.abs(x1 - ex1) < tolerance &&
|
Math.abs(x1 - ex1) < tolerance && Math.abs(y1 - ey1) < tolerance && Math.abs(x2 - ex2) < tolerance && Math.abs(y2 - ey2) < tolerance
|
||||||
Math.abs(y1 - ey1) < tolerance &&
|
|
||||||
Math.abs(x2 - ex2) < tolerance &&
|
|
||||||
Math.abs(y2 - ey2) < tolerance
|
|
||||||
const reverseMatch =
|
const reverseMatch =
|
||||||
Math.abs(x1 - ex2) < tolerance &&
|
Math.abs(x1 - ex2) < tolerance && Math.abs(y1 - ey2) < tolerance && Math.abs(x2 - ex1) < tolerance && Math.abs(y2 - ey1) < tolerance
|
||||||
Math.abs(y1 - ey2) < tolerance &&
|
|
||||||
Math.abs(x2 - ex1) < tolerance &&
|
|
||||||
Math.abs(y2 - ey1) < tolerance
|
|
||||||
|
|
||||||
if (forwardMatch || reverseMatch) {
|
if (forwardMatch || reverseMatch) {
|
||||||
line.attributes = { ...line.attributes, planeSize: edge.attributes.planeSize }
|
line.attributes = { ...line.attributes, planeSize: edge.attributes.planeSize }
|
||||||
@ -423,9 +428,7 @@ export function usePlacementShapeDrawing(id) {
|
|||||||
// 기존 도형에서 매칭되는 변의 planeSize를 찾는 헬퍼 함수
|
// 기존 도형에서 매칭되는 변의 planeSize를 찾는 헬퍼 함수
|
||||||
const findMatchingEdgePlaneSize = (x1, y1, x2, y2) => {
|
const findMatchingEdgePlaneSize = (x1, y1, x2, y2) => {
|
||||||
const tolerance = 2
|
const tolerance = 2
|
||||||
const existingPolygons = canvas.getObjects().filter(
|
const existingPolygons = canvas.getObjects().filter((obj) => (obj.name === POLYGON_TYPE.ROOF || obj.name === POLYGON_TYPE.WALL) && obj.lines)
|
||||||
(obj) => (obj.name === POLYGON_TYPE.ROOF || obj.name === POLYGON_TYPE.WALL) && obj.lines,
|
|
||||||
)
|
|
||||||
|
|
||||||
const isHorizontal = Math.abs(y1 - y2) < tolerance
|
const isHorizontal = Math.abs(y1 - y2) < tolerance
|
||||||
const isVertical = Math.abs(x1 - x2) < tolerance
|
const isVertical = Math.abs(x1 - x2) < tolerance
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import { useText } from '@/hooks/useText'
|
|||||||
import SizeSetting from '@/components/floor-plan/modal/object/SizeSetting'
|
import SizeSetting from '@/components/floor-plan/modal/object/SizeSetting'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { useUndoRedo } from '@/hooks/useUndoRedo'
|
||||||
|
|
||||||
export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
||||||
const { getMessage } = useMessage()
|
const { getMessage } = useMessage()
|
||||||
@ -52,6 +53,7 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
|||||||
const { fetchSettings } = useCanvasSetting(false)
|
const { fetchSettings } = useCanvasSetting(false)
|
||||||
const [currentObject, setCurrentObject] = useRecoilState(currentObjectState)
|
const [currentObject, setCurrentObject] = useRecoilState(currentObjectState)
|
||||||
const [popupId, setPopupId] = useState(uuidv4())
|
const [popupId, setPopupId] = useState(uuidv4())
|
||||||
|
const { saveSnapshot } = useUndoRedo()
|
||||||
|
|
||||||
const applySurfaceShape = (surfaceRefs, selectedType, id) => {
|
const applySurfaceShape = (surfaceRefs, selectedType, id) => {
|
||||||
let length1, length2, length3, length4, length5
|
let length1, length2, length3, length4, length5
|
||||||
@ -857,6 +859,7 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const moveSurfaceShapeBatch = () => {
|
const moveSurfaceShapeBatch = () => {
|
||||||
|
saveSnapshot()
|
||||||
const roof = canvas.getActiveObject()
|
const roof = canvas.getActiveObject()
|
||||||
|
|
||||||
if (roof) {
|
if (roof) {
|
||||||
@ -937,6 +940,7 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
|||||||
|
|
||||||
const resizeSurfaceShapeBatch = (side, target, width, height) => {
|
const resizeSurfaceShapeBatch = (side, target, width, height) => {
|
||||||
if (!target || target.type !== 'QPolygon') return
|
if (!target || target.type !== 'QPolygon') return
|
||||||
|
saveSnapshot()
|
||||||
|
|
||||||
width = width / 10
|
width = width / 10
|
||||||
height = height / 10
|
height = height / 10
|
||||||
@ -1454,6 +1458,7 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
|||||||
|
|
||||||
const rotateSurfaceShapeBatch = () => {
|
const rotateSurfaceShapeBatch = () => {
|
||||||
if (currentObject) {
|
if (currentObject) {
|
||||||
|
saveSnapshot()
|
||||||
// 관련 객체들 찾기
|
// 관련 객체들 찾기
|
||||||
// arrow는 제거
|
// arrow는 제거
|
||||||
const arrow = canvas.getObjects().find((obj) => obj.parentId === currentObject.id && obj.name === 'arrow')
|
const arrow = canvas.getObjects().find((obj) => obj.parentId === currentObject.id && obj.name === 'arrow')
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { fontSelector } from '@/store/fontAtom'
|
|||||||
import { MENU, POLYGON_TYPE } from '@/common/common'
|
import { MENU, POLYGON_TYPE } from '@/common/common'
|
||||||
import { useText } from '@/hooks/useText'
|
import { useText } from '@/hooks/useText'
|
||||||
import { usePolygon } from '@/hooks/usePolygon'
|
import { usePolygon } from '@/hooks/usePolygon'
|
||||||
|
import { useUndoRedo } from '@/hooks/useUndoRedo'
|
||||||
|
|
||||||
// 캔버스에 필요한 이벤트
|
// 캔버스에 필요한 이벤트
|
||||||
export function useCanvasEvent() {
|
export function useCanvasEvent() {
|
||||||
@ -19,6 +20,7 @@ export function useCanvasEvent() {
|
|||||||
const currentMenu = useRecoilValue(currentMenuState)
|
const currentMenu = useRecoilValue(currentMenuState)
|
||||||
const { changeCorridorDimensionText } = useText()
|
const { changeCorridorDimensionText } = useText()
|
||||||
const { setPolygonLinesActualSize } = usePolygon()
|
const { setPolygonLinesActualSize } = usePolygon()
|
||||||
|
const { undo, redo } = useUndoRedo()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
canvas?.setZoom(canvasZoom / 100)
|
canvas?.setZoom(canvasZoom / 100)
|
||||||
@ -28,6 +30,13 @@ export function useCanvasEvent() {
|
|||||||
attachDefaultEventOnCanvas()
|
attachDefaultEventOnCanvas()
|
||||||
}, [currentMenu])
|
}, [currentMenu])
|
||||||
|
|
||||||
|
// 키보드 이벤트 등록 (Cmd+Z / Ctrl+Z: undo, Shift+Cmd+Z / Shift+Ctrl+Z: redo)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!canvas) return
|
||||||
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
|
return () => document.removeEventListener('keydown', handleKeyDown)
|
||||||
|
}, [canvas, undo, redo])
|
||||||
|
|
||||||
// 기본적인 이벤트 필요시 추가
|
// 기본적인 이벤트 필요시 추가
|
||||||
const attachDefaultEventOnCanvas = () => {
|
const attachDefaultEventOnCanvas = () => {
|
||||||
removeEventOnCanvas()
|
removeEventOnCanvas()
|
||||||
@ -296,12 +305,14 @@ export function useCanvasEvent() {
|
|||||||
break
|
break
|
||||||
|
|
||||||
case 'z':
|
case 'z':
|
||||||
if (!e.ctrlKey) {
|
if (!e.ctrlKey && !e.metaKey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (e.shiftKey) {
|
||||||
console.log('뒤로가기')
|
redo()
|
||||||
|
} else {
|
||||||
|
undo()
|
||||||
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -381,20 +392,26 @@ export function useCanvasEvent() {
|
|||||||
/**
|
/**
|
||||||
* 선택한 도형을 삭제한다.
|
* 선택한 도형을 삭제한다.
|
||||||
*/
|
*/
|
||||||
|
let _isDeleting = false
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
|
if (_isDeleting) return
|
||||||
const targets = canvas?.getActiveObjects()
|
const targets = canvas?.getActiveObjects()
|
||||||
if (targets?.length === 0) {
|
if (!targets || targets.length === 0) {
|
||||||
alert('삭제할 대상을 선택해주세요.')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_isDeleting = true
|
||||||
if (!confirm('정말로 삭제하시겠습니까?')) {
|
if (!confirm('정말로 삭제하시겠습니까?')) {
|
||||||
|
_isDeleting = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
targets?.forEach((target) => {
|
targets.forEach((target) => {
|
||||||
canvas?.remove(target)
|
canvas?.remove(target)
|
||||||
})
|
})
|
||||||
|
canvas?.discardActiveObject()
|
||||||
|
canvas?.renderAll()
|
||||||
|
_isDeleting = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleZoom = (isZoom) => {
|
const handleZoom = (isZoom) => {
|
||||||
|
|||||||
369
src/hooks/useUndoRedo.js
Normal file
369
src/hooks/useUndoRedo.js
Normal file
@ -0,0 +1,369 @@
|
|||||||
|
import { useCallback, useRef } from 'react'
|
||||||
|
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'
|
||||||
|
import {
|
||||||
|
canvasState,
|
||||||
|
canvasZoomState,
|
||||||
|
checkedModuleState,
|
||||||
|
currentMenuState,
|
||||||
|
drewRoofCellsState,
|
||||||
|
moduleIsSetupState,
|
||||||
|
moduleRowColArrayState,
|
||||||
|
moduleSetupSurfaceState,
|
||||||
|
redoStackState,
|
||||||
|
roofPolygonArrayState,
|
||||||
|
undoStackState,
|
||||||
|
} from '@/store/canvasAtom'
|
||||||
|
import { outerLinePointsState } from '@/store/outerLineAtom'
|
||||||
|
import { selectedMenuState } from '@/store/menuAtom'
|
||||||
|
import { popupState } from '@/store/popupAtom'
|
||||||
|
import { POLYGON_TYPE, SAVE_KEY } from '@/common/common'
|
||||||
|
|
||||||
|
const MAX_HISTORY = 20
|
||||||
|
|
||||||
|
// 모든 useUndoRedo 인스턴스가 공유하는 복원 플래그
|
||||||
|
// useRef는 인스턴스마다 독립적이므로, 모듈 레벨 변수로 전환
|
||||||
|
let _isRestoring = false
|
||||||
|
let _restoreTimer = null
|
||||||
|
|
||||||
|
// loadFromJSON 공통 처리 - 이벤트 일시 중단 후 복원
|
||||||
|
function _restoreFromJSON(canvas, snapshot, restoreAtomSnapshot, rebuildCanvasAtoms, setCanvasZoom) {
|
||||||
|
// 진행 중인 복원 타이머가 있으면 취소
|
||||||
|
if (_restoreTimer) {
|
||||||
|
clearTimeout(_restoreTimer)
|
||||||
|
_restoreTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadFromJSON 중 object:added 등 이벤트로 인한 부작용 방지
|
||||||
|
// 리스너를 저장/복원하지 않고 단순히 off만 한다
|
||||||
|
// restoreAtomSnapshot에서 currentMenu 변경 → useCanvasEvent가 attachDefaultEventOnCanvas로 재등록
|
||||||
|
;['object:added', 'object:modified', 'object:removed'].forEach((eventName) => {
|
||||||
|
canvas.off(eventName)
|
||||||
|
})
|
||||||
|
|
||||||
|
canvas.loadFromJSON(snapshot.canvasJSON, () => {
|
||||||
|
rebuildCanvasAtoms()
|
||||||
|
canvas.renderAll()
|
||||||
|
restoreAtomSnapshot(snapshot.atomSnapshot)
|
||||||
|
|
||||||
|
if (canvas.viewportTransform) {
|
||||||
|
setCanvasZoom(Number((canvas.viewportTransform[0] * 100).toFixed(0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 다음 undo/redo 허용까지 짧은 딜레이 (연속 클릭 디바운스)
|
||||||
|
_restoreTimer = setTimeout(() => {
|
||||||
|
_isRestoring = false
|
||||||
|
_restoreTimer = null
|
||||||
|
}, 50)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 순환 참조만 안전하게 건너뛰는 JSON 직렬화
|
||||||
|
// WeakSet 방식은 공유 참조(같은 배열이 여러 곳에서 참조)까지 제거하므로 사용 불가
|
||||||
|
// 대신 조상 경로(ancestors) 스택으로 진짜 순환만 감지
|
||||||
|
function safeStringify(obj) {
|
||||||
|
const ancestors = []
|
||||||
|
return JSON.stringify(obj, function (key, value) {
|
||||||
|
if (typeof value !== 'object' || value === null) return value
|
||||||
|
|
||||||
|
// `this`는 현재 key를 담고 있는 부모 객체
|
||||||
|
// ancestors 스택에서 현재 부모 이후의 항목(형제 경로)을 정리
|
||||||
|
while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) {
|
||||||
|
ancestors.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 현재 value가 조상 경로에 이미 있으면 순환 참조
|
||||||
|
if (ancestors.includes(value)) return undefined
|
||||||
|
|
||||||
|
ancestors.push(value)
|
||||||
|
return value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeClone(value) {
|
||||||
|
if (value === null || value === undefined) return value
|
||||||
|
try {
|
||||||
|
return JSON.parse(safeStringify(value))
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUndoRedo() {
|
||||||
|
const canvas = useRecoilValue(canvasState)
|
||||||
|
const [undoStack, setUndoStack] = useRecoilState(undoStackState)
|
||||||
|
const [redoStack, setRedoStack] = useRecoilState(redoStackState)
|
||||||
|
|
||||||
|
const [, setModuleSetupSurface] = useRecoilState(moduleSetupSurfaceState)
|
||||||
|
const [moduleIsSetup, setModuleIsSetup] = useRecoilState(moduleIsSetupState)
|
||||||
|
const [checkedModule, setCheckedModule] = useRecoilState(checkedModuleState)
|
||||||
|
const [moduleRowColArray, setModuleRowColArray] = useRecoilState(moduleRowColArrayState)
|
||||||
|
const [, setDrewRoofCells] = useRecoilState(drewRoofCellsState)
|
||||||
|
const [, setRoofPolygonArray] = useRecoilState(roofPolygonArrayState)
|
||||||
|
const [outerLinePoints, setOuterLinePoints] = useRecoilState(outerLinePointsState)
|
||||||
|
const [selectedMenu, setSelectedMenu] = useRecoilState(selectedMenuState)
|
||||||
|
const [currentMenu, setCurrentMenu] = useRecoilState(currentMenuState)
|
||||||
|
const [, setCanvasZoom] = useRecoilState(canvasZoomState)
|
||||||
|
const setPopup = useSetRecoilState(popupState)
|
||||||
|
|
||||||
|
// 최신 스택 값을 ref로 유지하여 클로저 stale 문제 방지
|
||||||
|
const undoStackRef = useRef(undoStack)
|
||||||
|
undoStackRef.current = undoStack
|
||||||
|
const redoStackRef = useRef(redoStack)
|
||||||
|
redoStackRef.current = redoStack
|
||||||
|
|
||||||
|
const getAtomSnapshot = useCallback(() => {
|
||||||
|
return {
|
||||||
|
moduleIsSetup: safeClone(moduleIsSetup || []),
|
||||||
|
checkedModule: safeClone(checkedModule || []),
|
||||||
|
moduleRowColArray: safeClone(moduleRowColArray || []),
|
||||||
|
outerLinePoints: safeClone(outerLinePoints || []),
|
||||||
|
selectedMenu,
|
||||||
|
currentMenu,
|
||||||
|
}
|
||||||
|
}, [moduleIsSetup, checkedModule, moduleRowColArray, outerLinePoints, selectedMenu, currentMenu])
|
||||||
|
|
||||||
|
// loadFromJSON 후 오브젝트 간 라이브 참조 관계를 재구축
|
||||||
|
const rebuildObjectGraph = useCallback(() => {
|
||||||
|
if (!canvas) return
|
||||||
|
const objects = canvas.getObjects()
|
||||||
|
|
||||||
|
// wall polygon의 lines를 캔버스의 outerLine 오브젝트로 재연결
|
||||||
|
const walls = objects.filter((obj) => obj.name === POLYGON_TYPE.WALL)
|
||||||
|
const outerLines = objects.filter((obj) => obj.name === 'outerLine')
|
||||||
|
|
||||||
|
walls.forEach((wall) => {
|
||||||
|
if (outerLines.length > 0) {
|
||||||
|
// outerLine에 idx가 있으면 순서대로, 없으면 parentId로 매칭
|
||||||
|
const wallOuterLines = outerLines.filter((line) => !line.parentId || line.parentId === wall.id).sort((a, b) => (a.idx ?? 0) - (b.idx ?? 0))
|
||||||
|
if (wallOuterLines.length > 0) {
|
||||||
|
wall.lines = wallOuterLines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// roof polygon의 innerLines, hips, ridges, lines를 캔버스 오브젝트로 재연결
|
||||||
|
const roofs = objects.filter((obj) => obj.name === POLYGON_TYPE.ROOF)
|
||||||
|
roofs.forEach((roof) => {
|
||||||
|
// innerLines: hip과 ridge 라인들 (roof.innerLines.push(hip), roof.innerLines.push(ridge) 패턴)
|
||||||
|
const hipLines = objects.filter((obj) => obj.parentId === roof.id && obj.name === 'hip')
|
||||||
|
const ridgeLines = objects.filter((obj) => obj.parentId === roof.id && obj.name === 'ridge')
|
||||||
|
|
||||||
|
if (hipLines.length > 0 || ridgeLines.length > 0) {
|
||||||
|
roof.innerLines = [...hipLines, ...ridgeLines]
|
||||||
|
}
|
||||||
|
if (hipLines.length > 0) {
|
||||||
|
roof.hips = hipLines
|
||||||
|
}
|
||||||
|
if (ridgeLines.length > 0) {
|
||||||
|
roof.ridges = ridgeLines
|
||||||
|
}
|
||||||
|
|
||||||
|
// roof.lines: eaveHelpLine 또는 helpLine (지붕면 할당에서 사용)
|
||||||
|
const eaveHelpLines = objects.filter(
|
||||||
|
(obj) => (obj.lineName === 'eaveHelpLine' || obj.lineName === 'helpLine' || obj.name === 'helpLine') && obj.roofId === roof.id,
|
||||||
|
)
|
||||||
|
if (eaveHelpLines.length > 0) {
|
||||||
|
roof.lines = eaveHelpLines
|
||||||
|
}
|
||||||
|
|
||||||
|
// skeletonLines 재연결
|
||||||
|
const skeletonLines = objects.filter((obj) => (obj.lineName === 'roofLine' || obj.lineName === 'skeletonLine') && obj.parentId === roof.id)
|
||||||
|
if (skeletonLines.length > 0) {
|
||||||
|
roof.skeletonLines = skeletonLines
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// moduleSetupSurface의 modules 배열을 캔버스의 실제 QPolygon 모듈 객체로 재연결
|
||||||
|
const surfaces = objects.filter((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE)
|
||||||
|
const moduleObjects = objects.filter((obj) => obj.name === POLYGON_TYPE.MODULE)
|
||||||
|
surfaces.forEach((surface) => {
|
||||||
|
const surfaceModules = moduleObjects.filter((mod) => mod.surfaceId === surface.id)
|
||||||
|
surface.modules = surfaceModules
|
||||||
|
})
|
||||||
|
|
||||||
|
// 부모가 없는 고아 lengthText, arrow, flowText 제거
|
||||||
|
const parentIds = new Set(objects.filter((obj) => obj.id).map((obj) => obj.id))
|
||||||
|
const orphans = objects.filter(
|
||||||
|
(obj) => (obj.name === 'lengthText' || obj.name === 'arrow' || obj.name === 'flowText') && obj.parentId && !parentIds.has(obj.parentId),
|
||||||
|
)
|
||||||
|
orphans.forEach((obj) => canvas.remove(obj))
|
||||||
|
|
||||||
|
// lengthText 이동 관련 속성 및 이벤트 재설정
|
||||||
|
// loadFromJSON 중 object:added 이벤트가 꺼져있어 addEvent가 실행되지 않으므로 수동 설정
|
||||||
|
const lengthTexts = canvas.getObjects().filter((obj) => obj.name === 'lengthText' && obj.type?.toLowerCase().includes('text'))
|
||||||
|
lengthTexts.forEach((target) => {
|
||||||
|
target.bringToFront()
|
||||||
|
target.selectable = true
|
||||||
|
target.evented = true
|
||||||
|
target.lockMovementX = false
|
||||||
|
target.lockMovementY = false
|
||||||
|
target.lockRotation = true
|
||||||
|
target.lockScalingX = true
|
||||||
|
target.lockScalingY = true
|
||||||
|
|
||||||
|
// 기존 per-object 이벤트 제거 후 재등록
|
||||||
|
target.off('selected')
|
||||||
|
target.off('moving')
|
||||||
|
|
||||||
|
const x = target.left
|
||||||
|
const y = target.top
|
||||||
|
|
||||||
|
target.on('selected', () => {
|
||||||
|
Object.keys(target.controls).forEach((controlKey) => {
|
||||||
|
target.setControlVisible(controlKey, false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
target.on('moving', () => {
|
||||||
|
if (target.parentDirection === 'left' || target.parentDirection === 'right') {
|
||||||
|
const minX = target.minX
|
||||||
|
const maxX = target.maxX
|
||||||
|
if (target.left <= minX) {
|
||||||
|
target.set({ left: minX, top: y })
|
||||||
|
} else if (target.left >= maxX) {
|
||||||
|
target.set({ left: maxX, top: y })
|
||||||
|
} else {
|
||||||
|
target.set({ top: y })
|
||||||
|
}
|
||||||
|
} else if (target.parentDirection === 'top' || target.parentDirection === 'bottom') {
|
||||||
|
const minY = target.minY
|
||||||
|
const maxY = target.maxY
|
||||||
|
if (target.top <= minY) {
|
||||||
|
target.set({ left: x, top: minY })
|
||||||
|
} else if (target.top >= maxY) {
|
||||||
|
target.set({ left: x, top: maxY })
|
||||||
|
} else {
|
||||||
|
target.set({ left: x })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
canvas?.renderAll()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, [canvas])
|
||||||
|
|
||||||
|
const rebuildCanvasAtoms = useCallback(() => {
|
||||||
|
if (!canvas) return
|
||||||
|
|
||||||
|
// 먼저 오브젝트 간 참조 관계 재구축
|
||||||
|
rebuildObjectGraph()
|
||||||
|
|
||||||
|
const objects = canvas.getObjects()
|
||||||
|
|
||||||
|
const surfaces = objects.filter((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE)
|
||||||
|
setModuleSetupSurface(surfaces)
|
||||||
|
|
||||||
|
const roofs = objects.filter((obj) => obj.name === POLYGON_TYPE.ROOF)
|
||||||
|
setRoofPolygonArray(roofs)
|
||||||
|
|
||||||
|
const cells = roofs.filter((obj) => obj.cells && obj.cells.length > 0)
|
||||||
|
setDrewRoofCells(cells)
|
||||||
|
}, [canvas, rebuildObjectGraph])
|
||||||
|
|
||||||
|
const restoreAtomSnapshot = useCallback((atomSnapshot) => {
|
||||||
|
if (!atomSnapshot) return
|
||||||
|
setModuleIsSetup(atomSnapshot.moduleIsSetup || [])
|
||||||
|
setCheckedModule(atomSnapshot.checkedModule || [])
|
||||||
|
setModuleRowColArray(atomSnapshot.moduleRowColArray || [])
|
||||||
|
setOuterLinePoints(atomSnapshot.outerLinePoints || [])
|
||||||
|
if (atomSnapshot.selectedMenu !== undefined) setSelectedMenu(atomSnapshot.selectedMenu)
|
||||||
|
if (atomSnapshot.currentMenu !== undefined) setCurrentMenu(atomSnapshot.currentMenu)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// canvas.toJSON(SAVE_KEY)의 결과를 순환 참조 없이 안전하게 직렬화
|
||||||
|
const getCanvasSnapshot = useCallback(() => {
|
||||||
|
if (!canvas) return null
|
||||||
|
try {
|
||||||
|
const rawJSON = canvas.toJSON(SAVE_KEY)
|
||||||
|
// 순환 참조 제거 후 다시 파싱하여 순수 데이터 객체로 변환
|
||||||
|
return JSON.parse(safeStringify(rawJSON))
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Canvas snapshot failed:', e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}, [canvas])
|
||||||
|
|
||||||
|
const saveSnapshot = useCallback(() => {
|
||||||
|
if (!canvas || _isRestoring) return
|
||||||
|
|
||||||
|
// snapshot 전에 mouseLine 제거
|
||||||
|
const mouseLines = canvas.getObjects().filter((obj) => obj.name === 'mouseLine')
|
||||||
|
mouseLines.forEach((obj) => canvas.remove(obj))
|
||||||
|
|
||||||
|
const canvasJSON = getCanvasSnapshot()
|
||||||
|
if (!canvasJSON) return
|
||||||
|
|
||||||
|
const atomSnapshot = getAtomSnapshot()
|
||||||
|
|
||||||
|
const snapshot = {
|
||||||
|
canvasJSON,
|
||||||
|
atomSnapshot,
|
||||||
|
}
|
||||||
|
|
||||||
|
setUndoStack((prev) => {
|
||||||
|
const newStack = [...prev, snapshot]
|
||||||
|
if (newStack.length > MAX_HISTORY) {
|
||||||
|
return newStack.slice(newStack.length - MAX_HISTORY)
|
||||||
|
}
|
||||||
|
return newStack
|
||||||
|
})
|
||||||
|
setRedoStack([])
|
||||||
|
}, [canvas, getCanvasSnapshot, getAtomSnapshot])
|
||||||
|
|
||||||
|
const closeAllPopups = useCallback(() => {
|
||||||
|
setPopup({ config: [], other: [] })
|
||||||
|
}, [setPopup])
|
||||||
|
|
||||||
|
const undo = useCallback(() => {
|
||||||
|
const currentUndoStack = undoStackRef.current
|
||||||
|
console.log('currentUndoStack', currentUndoStack)
|
||||||
|
if (!canvas || currentUndoStack.length === 0 || _isRestoring) return
|
||||||
|
|
||||||
|
_isRestoring = true
|
||||||
|
closeAllPopups()
|
||||||
|
|
||||||
|
const currentCanvasJSON = getCanvasSnapshot()
|
||||||
|
const currentAtomSnapshot = getAtomSnapshot()
|
||||||
|
const currentSnapshot = {
|
||||||
|
canvasJSON: currentCanvasJSON,
|
||||||
|
atomSnapshot: currentAtomSnapshot,
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUndoStack = [...currentUndoStack]
|
||||||
|
const snapshot = newUndoStack.pop()
|
||||||
|
|
||||||
|
setUndoStack(newUndoStack)
|
||||||
|
setRedoStack((prev) => [...prev, currentSnapshot])
|
||||||
|
|
||||||
|
_restoreFromJSON(canvas, snapshot, restoreAtomSnapshot, rebuildCanvasAtoms, setCanvasZoom)
|
||||||
|
}, [canvas, getCanvasSnapshot, getAtomSnapshot, restoreAtomSnapshot, rebuildCanvasAtoms, closeAllPopups])
|
||||||
|
|
||||||
|
const redo = useCallback(() => {
|
||||||
|
const currentRedoStack = redoStackRef.current
|
||||||
|
if (!canvas || currentRedoStack.length === 0 || _isRestoring) return
|
||||||
|
|
||||||
|
_isRestoring = true
|
||||||
|
closeAllPopups()
|
||||||
|
|
||||||
|
const currentCanvasJSON = getCanvasSnapshot()
|
||||||
|
const currentAtomSnapshot = getAtomSnapshot()
|
||||||
|
const currentSnapshot = {
|
||||||
|
canvasJSON: currentCanvasJSON,
|
||||||
|
atomSnapshot: currentAtomSnapshot,
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRedoStack = [...currentRedoStack]
|
||||||
|
const snapshot = newRedoStack.pop()
|
||||||
|
|
||||||
|
setRedoStack(newRedoStack)
|
||||||
|
setUndoStack((prev) => [...prev, currentSnapshot])
|
||||||
|
|
||||||
|
_restoreFromJSON(canvas, snapshot, restoreAtomSnapshot, rebuildCanvasAtoms, setCanvasZoom)
|
||||||
|
}, [canvas, getCanvasSnapshot, getAtomSnapshot, restoreAtomSnapshot, rebuildCanvasAtoms, closeAllPopups])
|
||||||
|
|
||||||
|
return {
|
||||||
|
saveSnapshot,
|
||||||
|
undo,
|
||||||
|
redo,
|
||||||
|
canUndo: undoStack.length > 0,
|
||||||
|
canRedo: redoStack.length > 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -408,3 +408,16 @@ export const moduleRowColArrayState = atom({
|
|||||||
default: [],
|
default: [],
|
||||||
dangerouslyAllowMutability: true,
|
dangerouslyAllowMutability: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Undo/Redo 히스토리 스택
|
||||||
|
export const undoStackState = atom({
|
||||||
|
key: 'undoStackState',
|
||||||
|
default: [],
|
||||||
|
dangerouslyAllowMutability: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const redoStackState = atom({
|
||||||
|
key: 'redoStackState',
|
||||||
|
default: [],
|
||||||
|
dangerouslyAllowMutability: true,
|
||||||
|
})
|
||||||
|
|||||||
@ -152,6 +152,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.undo-redo-wrap{
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
margin-right: 5px;
|
||||||
|
button{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: #373737;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all .17s ease-in-out;
|
||||||
|
&:hover:not(.disabled){
|
||||||
|
background: #4B4B4B;
|
||||||
|
}
|
||||||
|
&.disabled{
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.vertical-horizontal{
|
.vertical-horizontal{
|
||||||
display: flex;
|
display: flex;
|
||||||
min-width: 170px;
|
min-width: 170px;
|
||||||
@ -1519,6 +1543,9 @@
|
|||||||
.btn-from{
|
.btn-from{
|
||||||
gap: 3px;
|
gap: 3px;
|
||||||
}
|
}
|
||||||
|
.undo-redo-wrap{
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
.vertical-horizontal{
|
.vertical-horizontal{
|
||||||
margin-right: 3px;
|
margin-right: 3px;
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
|
|||||||
@ -1071,7 +1071,9 @@ export function findAndRemoveClosestPoint(targetPoint, points) {
|
|||||||
export function polygonToTurfPolygon(object, current = false) {
|
export function polygonToTurfPolygon(object, current = false) {
|
||||||
let coordinates
|
let coordinates
|
||||||
coordinates = object.points.map((point) => [point.x, point.y])
|
coordinates = object.points.map((point) => [point.x, point.y])
|
||||||
if (current) coordinates = object.getCurrentPoints().map((point) => [point.x, point.y])
|
if (current && typeof object.getCurrentPoints === 'function') {
|
||||||
|
coordinates = object.getCurrentPoints().map((point) => [point.x, point.y])
|
||||||
|
}
|
||||||
coordinates.push(coordinates[0])
|
coordinates.push(coordinates[0])
|
||||||
return turf.polygon(
|
return turf.polygon(
|
||||||
[coordinates],
|
[coordinates],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user