diff --git a/src/hooks/common/useCommonUtils.js b/src/hooks/common/useCommonUtils.js index db6f4b5b..5225e50f 100644 --- a/src/hooks/common/useCommonUtils.js +++ b/src/hooks/common/useCommonUtils.js @@ -619,7 +619,8 @@ export function useCommonUtils() { } const buttonAct = dormerName == BATCH_TYPE.TRIANGLE_DORMER ? 3 : 4 - applyDormers(dormerParams, buttonAct) + // [undo 중복 fix] copyObject 에서 이미 saveSnapshot 을 호출했으므로 applyDormers 의 스냅샷은 건너뛴다. + applyDormers(dormerParams, buttonAct, true) } else if (obj.name === 'roof' && obj.type === 'QPolygon') { // roof(QPolygon) 객체는 순환 참조(lines[].parent -> polygon)로 인해 // fabric.clone() 사용 시 Maximum call stack size exceeded 에러 발생 diff --git a/src/hooks/object/useObjectBatch.js b/src/hooks/object/useObjectBatch.js index 0a295e5d..2c485f92 100644 --- a/src/hooks/object/useObjectBatch.js +++ b/src/hooks/object/useObjectBatch.js @@ -313,7 +313,7 @@ export function useObjectBatch({ isHidden, setIsHidden }) { * @param {*} surfaceShapePolygons * @returns */ - const applyDormers = ({ height, width, pitch, offsetRef, offsetWidthRef, directionRef }, buttonAct) => { + const applyDormers = ({ height, width, pitch, offsetRef, offsetWidthRef, directionRef }, buttonAct, skipSnapshot = false) => { const dormerName = buttonAct === 3 ? BATCH_TYPE.TRIANGLE_DORMER : BATCH_TYPE.PENTAGON_DORMER const dormerTempName = buttonAct === 3 ? BATCH_TYPE.TRIANGLE_DORMER_TEMP : BATCH_TYPE.PENTAGON_DORMER_TEMP @@ -331,7 +331,9 @@ export function useObjectBatch({ isHidden, setIsHidden }) { return } - saveSnapshot() + // [undo 중복 fix] 도머 복사(commonCopyObject)는 copyObject 에서 이미 saveSnapshot 을 호출한 뒤 + // applyDormers 를 재사용하므로, 복사 경로에서는 skipSnapshot=true 로 중복 스냅샷을 막는다. + if (!skipSnapshot) saveSnapshot() let theta = 0 @@ -1602,7 +1604,6 @@ export function useObjectBatch({ isHidden, setIsHidden }) { } const dormerOffset = (arrow1, arrow2, length1, length2) => { - saveSnapshot() length1 = parseInt(length1) / 10 length2 = parseInt(length2) / 10 @@ -1631,22 +1632,17 @@ export function useObjectBatch({ isHidden, setIsHidden }) { } } + // [좌표 검증 fix] moveObjectBatch 와 동일하게 setCoords()+getCurrentPoints() 로 실제 이동된 + // 월드좌표를 검사한다. 과거처럼 groupPoints+delta 로 검사하면 2배 이동된 좌표가 되어 + // 도머가 지붕 내부에 있어도 within=false → '지붕내 설치' alert 오발생. + dormer.setCoords() const parentSurface = canvas?.getObjects().filter((item) => item.name === POLYGON_TYPE.ROOF && item.id === dormer.parentId)[0] const turfSurface = pointsToTurfPolygon(parentSurface.getCurrentPoints()) let turfDormer = pointsToTurfPolygon(rectToPolygon(dormer)) let currentPoints = [] if (dormer.name === BATCH_TYPE.TRIANGLE_DORMER || dormer.name === BATCH_TYPE.PENTAGON_DORMER) { - const calcLeft = dormer.left - left - const calcTop = dormer.top - top - - currentPoints = dormer.groupPoints.map((item) => { - return { - x: item.x + calcLeft, - y: item.y + calcTop, - } - }) - + currentPoints = dormer.getCurrentPoints() turfDormer = pointsToTurfPolygon(currentPoints) } @@ -1659,10 +1655,15 @@ export function useObjectBatch({ isHidden, setIsHidden }) { //위치 원복` dormer.left = left dormer.top = top + dormer.setCoords() return } + // [undo 중복 fix] 검증 통과 후에만 스냅샷을 저장한다. 검증 실패(원복) 시에도 스냅샷이 + // 쌓이면 도머 작업 undo 횟수가 불필요하게 늘어난다. + saveSnapshot() + if (dormer.type === 'group') { dormer.set({ groupPoints: currentPoints }) const newDormer = reGroupObject(dormer) diff --git a/src/hooks/roofcover/useAuxiliaryDrawing.js b/src/hooks/roofcover/useAuxiliaryDrawing.js index 0a0102e4..62dab535 100644 --- a/src/hooks/roofcover/useAuxiliaryDrawing.js +++ b/src/hooks/roofcover/useAuxiliaryDrawing.js @@ -185,6 +185,13 @@ export function useAuxiliaryDrawing(id, isUseEffect = true) { name: 'auxiliaryLine', }) + // [복사본 roofId 유실 fix] addLine 이 내부에서 attributes 를 {} 로 초기화하므로, + // 원본 attributes(roofId 등)를 복사해 복원한다. 미복원 시 복사본의 roofId 가 없어 + // 우클릭 '보조선 전체삭제' 등 roofId 기반 처리가 누락된다. + if (newLine) { + newLine.attributes = { ...object.attributes } + } + // roofs의 innerLines에서 object와 같은 것을 찾아서 newLine으로 변경 const roofs = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF) roofs.forEach((roof) => { diff --git a/src/hooks/roofcover/useMovementSetting.js b/src/hooks/roofcover/useMovementSetting.js index e9231178..cf06ba57 100644 --- a/src/hooks/roofcover/useMovementSetting.js +++ b/src/hooks/roofcover/useMovementSetting.js @@ -156,23 +156,7 @@ export function useMovementSetting(id) { canvas.remove(CONFIRM_LINE_REF.current) CONFIRM_LINE_REF.current = null } - const roofs = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF) - roofs.forEach((roof) => { - roof.set({ stroke: '#1083E3' }) - roof.innerLines.forEach((line) => line.set({ selectable: true, strokeWidth: 2, stroke: '#1083E3' })) - }) - const walls = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.WALL) - walls.forEach((wall) => { - if (wall.baseLines.length === 0) { - wall.baseLines = canvas.getObjects().filter((obj) => obj.name === 'baseLine' && obj.attributes.wallId === wall.id) - } - wall.baseLines.forEach((baseLine) => { - baseLine.set({ selectable: false, visible: false }) - }) - }) - const outerLines = canvas.getObjects().filter((obj) => obj.name === 'outerLine') - outerLines.forEach((line) => line.set({ visible: true })) - canvas.renderAll() + restoreLineStyles() initEvent() } }, []) @@ -230,6 +214,28 @@ export function useMovementSetting(id) { canvas.renderAll() }, [currentObject]) + /** 동선이동/형올림내림 적용 전(평상) 선 스타일로 되돌린다. + * saveSnapshot 직전과 팝업 cleanup 양쪽에서 호출해, undo 시 강조색이 스냅샷에 박혀 복원되지 않도록 한다. */ + const restoreLineStyles = () => { + const roofs = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF) + roofs.forEach((roof) => { + roof.set({ stroke: '#1083E3' }) + roof.innerLines.forEach((line) => line.set({ selectable: true, strokeWidth: 2, stroke: '#1083E3' })) + }) + const walls = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.WALL) + walls.forEach((wall) => { + if (wall.baseLines.length === 0) { + wall.baseLines = canvas.getObjects().filter((obj) => obj.name === 'baseLine' && obj.attributes.wallId === wall.id) + } + wall.baseLines.forEach((baseLine) => { + baseLine.set({ selectable: false, visible: false }) + }) + }) + const outerLines = canvas.getObjects().filter((obj) => obj.name === 'outerLine') + outerLines.forEach((line) => line.set({ visible: true })) + canvas.renderAll() + } + const clearRef = () => { if (type === TYPE.FLOW_LINE) { // 안전한 ref 접근 @@ -534,6 +540,10 @@ export function useMovementSetting(id) { canvas.renderAll() } + // [undo 색상 fix] 스냅샷은 동선이동/형올림내림 적용 직전 상태를 저장한다. + // 팝업 오픈 시 입힌 강조색이 스냅샷에 박히면 undo 시 강조색이 복원되므로, + // 스냅샷 저장 전에 평상 선 스타일로 되돌린다. + restoreLineStyles() saveSnapshot() const target = selectedObject.current !== null ? selectedObject.current : CONFIRM_LINE_REF.current?.target if (!target) return diff --git a/src/hooks/useContextMenu.js b/src/hooks/useContextMenu.js index 4e56d93e..20a9f0f1 100644 --- a/src/hooks/useContextMenu.js +++ b/src/hooks/useContextMenu.js @@ -469,15 +469,25 @@ export function useContextMenu() { innerLines.length = 0 } - // 확정되지 않은 보조선 - const notFixedAuxiliaryLines = canvas.getObjects().filter((obj) => obj.name === 'auxiliaryLine' && !obj.isAuxiliaryFixed) - - notFixedAuxiliaryLines.forEach((line) => { + // [전체삭제 누락 fix] 기존에는 미확정 보조선(!isAuxiliaryFixed)만 삭제해, + // 복사본/미확정 보조선을 우클릭하면 currentObject.roofId 가 위 innerLines 와 어긋나 + // 확정된 보조선이 누락되었다. 확정 여부와 무관하게 모든 보조선을 삭제한다. + const auxiliaryLines = canvas.getObjects().filter((obj) => obj.name === 'auxiliaryLine') + auxiliaryLines.forEach((line) => { canvas.remove(line) }) - //동이동현이동 - const eaveHelpLines = canvas.getObjects().filter((obj) => obj.name === 'eaveHelpLine' && obj.parentId === currentObject.attributes.roofId) + // 지붕 innerLines 에 남은 보조선/eaveHelpLine 참조 정리 (stale 부활 방지, 용마루 hip·ridge 는 보존) + canvas.getObjects().forEach((obj) => { + if (Array.isArray(obj.innerLines) && obj.innerLines.length > 0) { + obj.innerLines = obj.innerLines.filter( + (line) => line.name !== 'auxiliaryLine' && line.name !== 'eaveHelpLine' && line.lineName !== 'eaveHelpLine', + ) + } + }) + + //동이동현이동 - 모든 eaveHelpLine 삭제 (roofId 무관, name/lineName 두 식별자 모두 검사) + const eaveHelpLines = canvas.getObjects().filter((obj) => obj.name === 'eaveHelpLine' || obj.lineName === 'eaveHelpLine') eaveHelpLines.forEach((line) => { canvas.remove(line) })