mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-25 06:22:14 -03:00
Add undo/redo functionality to Canvas
Implemented undo and redo history stacks in Canvas.js, including keyboard shortcuts (Ctrl+Z, Ctrl+Y/Shift+Z), and state saving on relevant actions. Added Undo and Redo buttons to the UI in Canvas_view.js, with dynamic enable/disable based on history state.
This commit is contained in:
122
js/Canvas.js
122
js/Canvas.js
@@ -74,6 +74,85 @@ export class Canvas {
|
|||||||
...layer,
|
...layer,
|
||||||
opacity: 1
|
opacity: 1
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
this.undoStack = [];
|
||||||
|
this.redoStack = [];
|
||||||
|
this.historyLimit = 100;
|
||||||
|
|
||||||
|
this.saveState();
|
||||||
|
}
|
||||||
|
|
||||||
|
cloneLayers(layers) {
|
||||||
|
return layers.map(layer => {
|
||||||
|
const newLayer = { ...layer };
|
||||||
|
// Obiekty Image nie są klonowane, aby oszczędzać pamięć.
|
||||||
|
// Zakładamy, że same dane obrazu się nie zmieniają.
|
||||||
|
return newLayer;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
saveState(replaceLast = false) {
|
||||||
|
if (replaceLast && this.undoStack.length > 0) {
|
||||||
|
this.undoStack.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentState = this.cloneLayers(this.layers);
|
||||||
|
|
||||||
|
if (this.undoStack.length > 0) {
|
||||||
|
const lastState = this.undoStack[this.undoStack.length - 1];
|
||||||
|
if (JSON.stringify(currentState) === JSON.stringify(lastState)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.undoStack.push(currentState);
|
||||||
|
|
||||||
|
if (this.undoStack.length > this.historyLimit) {
|
||||||
|
this.undoStack.shift();
|
||||||
|
}
|
||||||
|
this.redoStack = [];
|
||||||
|
this.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];
|
||||||
|
this.layers = this.cloneLayers(prevState);
|
||||||
|
this.updateSelectionAfterHistory();
|
||||||
|
this.render();
|
||||||
|
this.updateHistoryButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
redo() {
|
||||||
|
if (this.redoStack.length === 0) return;
|
||||||
|
const nextState = this.redoStack.pop();
|
||||||
|
this.undoStack.push(nextState);
|
||||||
|
this.layers = this.cloneLayers(nextState);
|
||||||
|
this.updateSelectionAfterHistory();
|
||||||
|
this.render();
|
||||||
|
this.updateHistoryButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSelectionAfterHistory() {
|
||||||
|
const newSelectedLayers = [];
|
||||||
|
if (this.selectedLayers) {
|
||||||
|
this.selectedLayers.forEach(sl => {
|
||||||
|
const found = this.layers.find(l => l.id === sl.id);
|
||||||
|
if(found) newSelectedLayers.push(found);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.updateSelection(newSelectedLayers);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateHistoryButtons() {
|
||||||
|
if (this.onHistoryChange) {
|
||||||
|
this.onHistoryChange({
|
||||||
|
canUndo: this.undoStack.length > 1,
|
||||||
|
canRedo: this.redoStack.length > 0
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initCanvas() {
|
initCanvas() {
|
||||||
@@ -130,8 +209,6 @@ export class Canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleMouseDown(e) {
|
handleMouseDown(e) {
|
||||||
|
|
||||||
|
|
||||||
this.canvas.focus();
|
this.canvas.focus();
|
||||||
|
|
||||||
const currentTime = Date.now();
|
const currentTime = Date.now();
|
||||||
@@ -195,7 +272,7 @@ export class Canvas {
|
|||||||
|
|
||||||
pasteLayers() {
|
pasteLayers() {
|
||||||
if (this.internalClipboard.length === 0) return;
|
if (this.internalClipboard.length === 0) return;
|
||||||
|
this.saveState();
|
||||||
const newLayers = [];
|
const newLayers = [];
|
||||||
const pasteOffset = 20;
|
const pasteOffset = 20;
|
||||||
|
|
||||||
@@ -297,6 +374,8 @@ export class Canvas {
|
|||||||
|
|
||||||
|
|
||||||
handleMouseUp(e) {
|
handleMouseUp(e) {
|
||||||
|
const interactionEnded = this.interaction.mode !== 'none' && this.interaction.mode !== 'panning';
|
||||||
|
|
||||||
if (this.interaction.mode === 'resizingCanvas') {
|
if (this.interaction.mode === 'resizingCanvas') {
|
||||||
this.finalizeCanvasResize();
|
this.finalizeCanvasResize();
|
||||||
} else if (this.interaction.mode === 'movingCanvas') {
|
} else if (this.interaction.mode === 'movingCanvas') {
|
||||||
@@ -304,6 +383,10 @@ export class Canvas {
|
|||||||
}
|
}
|
||||||
this.resetInteractionState();
|
this.resetInteractionState();
|
||||||
this.render();
|
this.render();
|
||||||
|
|
||||||
|
if (interactionEnded) {
|
||||||
|
this.saveState();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -395,24 +478,45 @@ export class Canvas {
|
|||||||
this.interaction.isAltPressed = true;
|
this.interaction.isAltPressed = true;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
if (e.ctrlKey && e.key.toLowerCase() === 'c') {
|
|
||||||
|
if (e.ctrlKey) {
|
||||||
|
if (e.key.toLowerCase() === 'z') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.shiftKey) {
|
||||||
|
this.redo();
|
||||||
|
} else {
|
||||||
|
this.undo();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key.toLowerCase() === 'y') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.redo();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key.toLowerCase() === 'c') {
|
||||||
if (this.selectedLayers.length > 0) {
|
if (this.selectedLayers.length > 0) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.copySelectedLayers();
|
this.copySelectedLayers();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
if (e.key.toLowerCase() === 'v') {
|
||||||
if (e.ctrlKey && e.key.toLowerCase() === 'v') {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.handlePaste();
|
this.handlePaste();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.selectedLayer) {
|
if (this.selectedLayer) {
|
||||||
if (e.key === 'Delete') {
|
if (e.key === 'Delete') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
this.saveState();
|
||||||
this.layers = this.layers.filter(l => !this.selectedLayers.includes(l));
|
this.layers = this.layers.filter(l => !this.selectedLayers.includes(l));
|
||||||
this.updateSelection([]);
|
this.updateSelection([]);
|
||||||
this.render();
|
this.render();
|
||||||
@@ -444,6 +548,7 @@ export class Canvas {
|
|||||||
|
|
||||||
if (needsRender) {
|
if (needsRender) {
|
||||||
this.render();
|
this.render();
|
||||||
|
this.saveState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -906,6 +1011,7 @@ export class Canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateCanvasSize(width, height) {
|
updateCanvasSize(width, height) {
|
||||||
|
this.saveState();
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
|
|
||||||
@@ -1531,6 +1637,7 @@ export class Canvas {
|
|||||||
});
|
});
|
||||||
this.layers.forEach((layer, i) => layer.zIndex = i);
|
this.layers.forEach((layer, i) => layer.zIndex = i);
|
||||||
this.render();
|
this.render();
|
||||||
|
this.saveState();
|
||||||
}
|
}
|
||||||
|
|
||||||
moveLayerDown() {
|
moveLayerDown() {
|
||||||
@@ -1548,6 +1655,7 @@ export class Canvas {
|
|||||||
});
|
});
|
||||||
this.layers.forEach((layer, i) => layer.zIndex = i);
|
this.layers.forEach((layer, i) => layer.zIndex = i);
|
||||||
this.render();
|
this.render();
|
||||||
|
this.saveState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1619,6 +1727,7 @@ export class Canvas {
|
|||||||
newImage.onload = () => {
|
newImage.onload = () => {
|
||||||
layer.image = newImage;
|
layer.image = newImage;
|
||||||
this.render();
|
this.render();
|
||||||
|
this.saveState();
|
||||||
};
|
};
|
||||||
newImage.src = tempCanvas.toDataURL();
|
newImage.src = tempCanvas.toDataURL();
|
||||||
});
|
});
|
||||||
@@ -1641,6 +1750,7 @@ export class Canvas {
|
|||||||
newImage.onload = () => {
|
newImage.onload = () => {
|
||||||
layer.image = newImage;
|
layer.image = newImage;
|
||||||
this.render();
|
this.render();
|
||||||
|
this.saveState();
|
||||||
};
|
};
|
||||||
newImage.src = tempCanvas.toDataURL();
|
newImage.src = tempCanvas.toDataURL();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -519,6 +519,18 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
$el("button.painter-button", {
|
||||||
|
id: `undo-button-${node.id}`,
|
||||||
|
textContent: "Undo",
|
||||||
|
disabled: true,
|
||||||
|
onclick: () => canvas.undo()
|
||||||
|
}),
|
||||||
|
$el("button.painter-button", {
|
||||||
|
id: `redo-button-${node.id}`,
|
||||||
|
textContent: "Redo",
|
||||||
|
disabled: true,
|
||||||
|
onclick: () => canvas.redo()
|
||||||
|
}),
|
||||||
$el("button.painter-button.requires-selection.matting-button", {
|
$el("button.painter-button.requires-selection.matting-button", {
|
||||||
textContent: "Matting",
|
textContent: "Matting",
|
||||||
onclick: async () => {
|
onclick: async () => {
|
||||||
@@ -600,7 +612,18 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
canvas.onSelectionChange = updateButtonStates;
|
canvas.onSelectionChange = updateButtonStates;
|
||||||
|
|
||||||
|
const undoButton = controlPanel.querySelector(`#undo-button-${node.id}`);
|
||||||
|
const redoButton = controlPanel.querySelector(`#redo-button-${node.id}`);
|
||||||
|
|
||||||
|
canvas.onHistoryChange = ({ canUndo, canRedo }) => {
|
||||||
|
if(undoButton) undoButton.disabled = !canUndo;
|
||||||
|
if(redoButton) redoButton.disabled = !canRedo;
|
||||||
|
};
|
||||||
|
|
||||||
updateButtonStates();
|
updateButtonStates();
|
||||||
|
canvas.updateHistoryButtons();
|
||||||
|
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver((entries) => {
|
const resizeObserver = new ResizeObserver((entries) => {
|
||||||
const controlsHeight = entries[0].target.offsetHeight;
|
const controlsHeight = entries[0].target.offsetHeight;
|
||||||
@@ -621,9 +644,14 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const addUpdateToButton = (button) => {
|
const addUpdateToButton = (button) => {
|
||||||
|
if (button.textContent === "Undo" || button.textContent === "Redo") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const origClick = button.onclick;
|
const origClick = button.onclick;
|
||||||
button.onclick = async (...args) => {
|
button.onclick = async (...args) => {
|
||||||
await origClick?.(...args);
|
if (origClick) {
|
||||||
|
await origClick(...args);
|
||||||
|
}
|
||||||
await updateOutput();
|
await updateOutput();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user