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.
This commit is contained in:
Dariusz L
2025-06-26 03:22:18 +02:00
parent 63ab402154
commit cb142908ad
5 changed files with 192 additions and 35 deletions

View File

@@ -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
});
}
}

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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
};
}
}
}

View File

@@ -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}`);
}
}