From cb142908ad7277f09729fa6453d1288c9f1274d7 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Thu, 26 Jun 2025 03:22:18 +0200 Subject: [PATCH] Add separate undo/redo history for mask and layers Refactors CanvasState to maintain independent undo/redo stacks for mask editing and layer editing. Updates all relevant logic to use the correct history depending on the active mode, ensuring undo/redo and history buttons work as expected in both modes. MaskTool now saves history on activation, clear, and mouse up, and history info is reported per mode. Improves user experience when switching between mask and layer editing. --- js/Canvas.js | 6 +- js/CanvasInteractions.js | 8 +- js/CanvasRenderer.js | 2 + js/CanvasState.js | 168 ++++++++++++++++++++++++++++++++------- js/Mask_tool.js | 43 +++++++++- 5 files changed, 192 insertions(+), 35 deletions(-) diff --git a/js/Canvas.js b/js/Canvas.js index 683446e..470a46b 100644 --- a/js/Canvas.js +++ b/js/Canvas.js @@ -119,9 +119,11 @@ export class Canvas { updateHistoryButtons() { if (this.onHistoryChange) { + // Pobierz informacje o historii odpowiednią dla aktualnego trybu + const historyInfo = this.canvasState.getHistoryInfo(); this.onHistoryChange({ - canUndo: this.canvasState.undoStack.length > 1, - canRedo: this.canvasState.redoStack.length > 0 + canUndo: historyInfo.canUndo, + canRedo: historyInfo.canRedo }); } } diff --git a/js/CanvasInteractions.js b/js/CanvasInteractions.js index 03e09dc..9c554a5 100644 --- a/js/CanvasInteractions.js +++ b/js/CanvasInteractions.js @@ -156,7 +156,7 @@ export class CanvasInteractions { return; } this.canvas.maskTool.handleMouseUp(); - this.canvas.saveState(); + // Nie wywołujemy saveState - to już jest obsługiwane w MaskTool this.canvas.render(); return; } @@ -266,7 +266,11 @@ export class CanvasInteractions { this.canvas.viewport.y = worldCoords.y - (mouseBufferY / this.canvas.viewport.zoom); } this.canvas.render(); - this.canvas.saveState(true); + + // Nie zapisujemy stanu podczas scrollowania w trybie maski + if (!this.canvas.maskTool.isActive) { + this.canvas.saveState(true); + } } handleKeyDown(e) { diff --git a/js/CanvasRenderer.js b/js/CanvasRenderer.js index 947c64c..bc61fef 100644 --- a/js/CanvasRenderer.js +++ b/js/CanvasRenderer.js @@ -90,12 +90,14 @@ export class CanvasRenderer { // W trybie maski pokazuj maskę z przezroczystością 0.5 ctx.globalCompositeOperation = 'source-over'; ctx.globalAlpha = 0.5; + // Rysuj maskę w pozycji (0,0) - będzie dopasowana do obszaru canvasu ctx.drawImage(maskImage, 0, 0); ctx.globalAlpha = 1.0; } else if (maskImage) { // W trybie warstw pokazuj maskę jako widoczną, ale nieedytowalną ctx.globalCompositeOperation = 'source-over'; ctx.globalAlpha = 1.0; + // Rysuj maskę w pozycji (0,0) - będzie dopasowana do obszaru canvasu ctx.drawImage(maskImage, 0, 0); ctx.globalAlpha = 1.0; } diff --git a/js/CanvasState.js b/js/CanvasState.js index 76dabc0..4f539f1 100644 --- a/js/CanvasState.js +++ b/js/CanvasState.js @@ -9,8 +9,11 @@ const log = createModuleLogger('CanvasState'); export class CanvasState { constructor(canvas) { this.canvas = canvas; - this.undoStack = []; - this.redoStack = []; + // Osobne stosy dla trybu warstw i trybu maski + this.layersUndoStack = []; + this.layersRedoStack = []; + this.maskUndoStack = []; + this.maskRedoStack = []; this.historyLimit = 100; this.saveTimeout = null; this.lastSavedStateSignature = null; @@ -253,25 +256,34 @@ export class CanvasState { } saveState(replaceLast = false) { - if (replaceLast && this.undoStack.length > 0) { - this.undoStack.pop(); + // Sprawdź czy jesteśmy w trybie maski + if (this.canvas.maskTool && this.canvas.maskTool.isActive) { + this.saveMaskState(replaceLast); + } else { + this.saveLayersState(replaceLast); + } + } + + saveLayersState(replaceLast = false) { + if (replaceLast && this.layersUndoStack.length > 0) { + this.layersUndoStack.pop(); } const currentState = cloneLayers(this.canvas.layers); - if (this.undoStack.length > 0) { - const lastState = this.undoStack[this.undoStack.length - 1]; + if (this.layersUndoStack.length > 0) { + const lastState = this.layersUndoStack[this.layersUndoStack.length - 1]; if (getStateSignature(currentState) === getStateSignature(lastState)) { return; } } - this.undoStack.push(currentState); + this.layersUndoStack.push(currentState); - if (this.undoStack.length > this.historyLimit) { - this.undoStack.shift(); + if (this.layersUndoStack.length > this.historyLimit) { + this.layersUndoStack.shift(); } - this.redoStack = []; + this.layersRedoStack = []; this.canvas.updateHistoryButtons(); // Użyj debounce dla częstych zapisów @@ -279,33 +291,124 @@ export class CanvasState { this._debouncedSave(); } + saveMaskState(replaceLast = false) { + if (!this.canvas.maskTool) return; + + if (replaceLast && this.maskUndoStack.length > 0) { + this.maskUndoStack.pop(); + } + + // Klonuj aktualny stan maski + const maskCanvas = this.canvas.maskTool.getMask(); + const clonedCanvas = document.createElement('canvas'); + clonedCanvas.width = maskCanvas.width; + clonedCanvas.height = maskCanvas.height; + const clonedCtx = clonedCanvas.getContext('2d'); + clonedCtx.drawImage(maskCanvas, 0, 0); + + this.maskUndoStack.push(clonedCanvas); + + if (this.maskUndoStack.length > this.historyLimit) { + this.maskUndoStack.shift(); + } + this.maskRedoStack = []; + this.canvas.updateHistoryButtons(); + } + undo() { - if (this.undoStack.length <= 1) return; - const currentState = this.undoStack.pop(); - this.redoStack.push(currentState); - const prevState = this.undoStack[this.undoStack.length - 1]; + // Sprawdź czy jesteśmy w trybie maski + if (this.canvas.maskTool && this.canvas.maskTool.isActive) { + this.undoMaskState(); + } else { + this.undoLayersState(); + } + } + + redo() { + // Sprawdź czy jesteśmy w trybie maski + if (this.canvas.maskTool && this.canvas.maskTool.isActive) { + this.redoMaskState(); + } else { + this.redoLayersState(); + } + } + + undoLayersState() { + if (this.layersUndoStack.length <= 1) return; + + const currentState = this.layersUndoStack.pop(); + this.layersRedoStack.push(currentState); + const prevState = this.layersUndoStack[this.layersUndoStack.length - 1]; this.canvas.layers = cloneLayers(prevState); this.canvas.updateSelectionAfterHistory(); this.canvas.render(); this.canvas.updateHistoryButtons(); } - redo() { - if (this.redoStack.length === 0) return; - const nextState = this.redoStack.pop(); - this.undoStack.push(nextState); + redoLayersState() { + if (this.layersRedoStack.length === 0) return; + + const nextState = this.layersRedoStack.pop(); + this.layersUndoStack.push(nextState); this.canvas.layers = cloneLayers(nextState); this.canvas.updateSelectionAfterHistory(); this.canvas.render(); this.canvas.updateHistoryButtons(); } + undoMaskState() { + if (!this.canvas.maskTool || this.maskUndoStack.length <= 1) return; + + const currentState = this.maskUndoStack.pop(); + this.maskRedoStack.push(currentState); + + if (this.maskUndoStack.length > 0) { + const prevState = this.maskUndoStack[this.maskUndoStack.length - 1]; + // Przywróć poprzedni stan maski + const maskCanvas = this.canvas.maskTool.getMask(); + const maskCtx = maskCanvas.getContext('2d'); + + // Wyczyść obecną maskę + maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height); + // Przywróć poprzedni stan + maskCtx.drawImage(prevState, 0, 0); + + this.canvas.render(); + } + + this.canvas.updateHistoryButtons(); + } + + redoMaskState() { + if (!this.canvas.maskTool || this.maskRedoStack.length === 0) return; + + const nextState = this.maskRedoStack.pop(); + this.maskUndoStack.push(nextState); + + // Przywróć następny stan maski + const maskCanvas = this.canvas.maskTool.getMask(); + const maskCtx = maskCanvas.getContext('2d'); + + // Wyczyść obecną maskę + maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height); + // Przywróć następny stan + maskCtx.drawImage(nextState, 0, 0); + + this.canvas.render(); + this.canvas.updateHistoryButtons(); + } + /** * Czyści historię undo/redo */ clearHistory() { - this.undoStack = []; - this.redoStack = []; + if (this.canvas.maskTool && this.canvas.maskTool.isActive) { + this.maskUndoStack = []; + this.maskRedoStack = []; + } else { + this.layersUndoStack = []; + this.layersRedoStack = []; + } this.canvas.updateHistoryButtons(); log.info("History cleared"); } @@ -315,12 +418,23 @@ export class CanvasState { * @returns {Object} Informacje o historii */ getHistoryInfo() { - return { - undoCount: this.undoStack.length, - redoCount: this.redoStack.length, - canUndo: this.undoStack.length > 1, - canRedo: this.redoStack.length > 0, - historyLimit: this.historyLimit - }; + // Zwraca dane historii w zależności od aktywnego trybu + if (this.canvas.maskTool && this.canvas.maskTool.isActive) { + return { + undoCount: this.maskUndoStack.length, + redoCount: this.maskRedoStack.length, + canUndo: this.maskUndoStack.length > 1, + canRedo: this.maskRedoStack.length > 0, + historyLimit: this.historyLimit + }; + } else { + return { + undoCount: this.layersUndoStack.length, + redoCount: this.layersRedoStack.length, + canUndo: this.layersUndoStack.length > 1, + canRedo: this.layersRedoStack.length > 0, + historyLimit: this.historyLimit + }; + } } } diff --git a/js/Mask_tool.js b/js/Mask_tool.js index 4ef7bf9..87ed1e8 100644 --- a/js/Mask_tool.js +++ b/js/Mask_tool.js @@ -27,18 +27,32 @@ export class MaskTool { initMaskCanvas() { this.maskCanvas.width = this.mainCanvas.width; this.maskCanvas.height = this.mainCanvas.height; - this.clear(); + // Wyczyść canvas bez zapisywania do historii + this.maskCtx.clearRect(0, 0, this.maskCanvas.width, this.maskCanvas.height); } activate() { this.isActive = true; this.canvasInstance.interaction.mode = 'drawingMask'; + + // Zapisz początkowy stan maski tylko jeśli historia jest pusta + if (this.canvasInstance.canvasState && this.canvasInstance.canvasState.maskUndoStack.length === 0) { + this.canvasInstance.canvasState.saveMaskState(); + } + + // Aktualizuj przyciski historii po przełączeniu na tryb maski + this.canvasInstance.updateHistoryButtons(); + log.info("Mask tool activated"); } deactivate() { this.isActive = false; this.canvasInstance.interaction.mode = 'none'; + + // Aktualizuj przyciski historii po przełączeniu z trybu maski + this.canvasInstance.updateHistoryButtons(); + log.info("Mask tool deactivated"); } @@ -65,8 +79,18 @@ export class MaskTool { handleMouseUp() { if (!this.isActive) return; - this.isDrawing = false; - this.lastPosition = null; + + // Jeśli narzędzie rysowało, zapisz stan maski + if (this.isDrawing) { + // Zakończ rysowanie + this.isDrawing = false; + this.lastPosition = null; + + // Zapisz stan maski do historii + if (this.canvasInstance.canvasState) { + this.canvasInstance.canvasState.saveMaskState(); + } + } } draw(worldCoords) { @@ -99,6 +123,11 @@ export class MaskTool { clear() { this.maskCtx.clearRect(0, 0, this.maskCanvas.width, this.maskCanvas.height); + + // Zapisz stan po wyczyszczeniu maski tylko jeśli narzędzie jest aktywne + if (this.isActive && this.canvasInstance.canvasState) { + this.canvasInstance.canvasState.saveMaskState(); + } } getMask() { @@ -142,6 +171,12 @@ export class MaskTool { this.maskCanvas.width = width; this.maskCanvas.height = height; this.maskCtx = this.maskCanvas.getContext('2d'); - this.maskCtx.drawImage(oldMask, 0, 0); + + // Zachowaj zawartość starej maski + if (oldMask.width > 0 && oldMask.height > 0) { + this.maskCtx.drawImage(oldMask, 0, 0); + } + + log.info(`Mask canvas resized to ${width}x${height}`); } }