Files
Comfyui-LayerForge/js/Canvas.js
Dariusz L cb142908ad 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.
2025-06-26 03:22:18 +02:00

444 lines
13 KiB
JavaScript

import {saveImage, getImage, removeImage} from "./db.js";
import {MaskTool} from "./Mask_tool.js";
import {CanvasState} from "./CanvasState.js";
import {CanvasInteractions} from "./CanvasInteractions.js";
import {CanvasLayers} from "./CanvasLayers.js";
import {CanvasRenderer} from "./CanvasRenderer.js";
import {CanvasIO} from "./CanvasIO.js";
import {createModuleLogger} from "./LoggerUtils.js";
import {generateUUID, snapToGrid, getSnapAdjustment, worldToLocal, localToWorld} from "./CommonUtils.js";
import {withErrorHandling, safeExecute} from "./ErrorHandler.js";
// Inicjalizacja loggera dla modułu Canvas
const log = createModuleLogger('Canvas');
export class Canvas {
constructor(node, widget) {
this.node = node;
this.widget = widget;
this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d');
this.width = 512;
this.height = 512;
this.layers = [];
this.selectedLayer = null;
this.selectedLayers = [];
this.onSelectionChange = null;
this.lastMousePosition = {x: 0, y: 0};
this.viewport = {
x: -(this.width / 4),
y: -(this.height / 4),
zoom: 0.8,
};
// Interaction state will be managed by CanvasInteractions module
this.offscreenCanvas = document.createElement('canvas');
this.offscreenCtx = this.offscreenCanvas.getContext('2d', {
alpha: false
});
this.renderAnimationFrame = null;
this.lastRenderTime = 0;
this.internalClipboard = [];
this.isMouseOver = false;
this.renderInterval = 1000 / 60;
this.isDirty = false;
this.dataInitialized = false;
this.pendingDataCheck = null;
this.maskTool = new MaskTool(this);
this.initCanvas();
this.canvasState = new CanvasState(this); // Nowy moduł zarządzania stanem
this.canvasInteractions = new CanvasInteractions(this); // Nowy moduł obsługi interakcji
this.canvasLayers = new CanvasLayers(this); // Nowy moduł operacji na warstwach
this.canvasRenderer = new CanvasRenderer(this); // Nowy moduł renderowania
this.canvasIO = new CanvasIO(this); // Nowy moduł operacji I/O
// Po utworzeniu CanvasInteractions, użyj jego interaction state
this.interaction = this.canvasInteractions.interaction;
this.setupEventListeners();
this.initNodeData();
// Przeniesione do CanvasLayers
this.blendModes = this.canvasLayers.blendModes;
this.selectedBlendMode = this.canvasLayers.selectedBlendMode;
this.blendOpacity = this.canvasLayers.blendOpacity;
this.isAdjustingOpacity = this.canvasLayers.isAdjustingOpacity;
this.layers = this.layers.map(layer => ({
...layer,
opacity: 1
}));
this.imageCache = new Map(); // Pamięć podręczna dla obrazów (imageId -> imageSrc)
// this.saveState(); // Wywołanie przeniesione do loadInitialState
}
async loadStateFromDB() {
return this.canvasState.loadStateFromDB();
}
async saveStateToDB(immediate = false) {
return this.canvasState.saveStateToDB(immediate);
}
async loadInitialState() {
log.info("Loading initial state for node:", this.node.id);
const loaded = await this.loadStateFromDB();
if (!loaded) {
log.info("No saved state found, initializing from node data.");
await this.initNodeData();
}
this.saveState(); // Save initial state to undo stack
}
saveState(replaceLast = false) {
this.canvasState.saveState(replaceLast);
}
undo() {
this.canvasState.undo();
}
redo() {
this.canvasState.redo();
}
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) {
// Pobierz informacje o historii odpowiednią dla aktualnego trybu
const historyInfo = this.canvasState.getHistoryInfo();
this.onHistoryChange({
canUndo: historyInfo.canUndo,
canRedo: historyInfo.canRedo
});
}
}
initCanvas() {
this.canvas.width = this.width;
this.canvas.height = this.height;
this.canvas.style.border = '1px solid black';
this.canvas.style.maxWidth = '100%';
this.canvas.style.backgroundColor = '#606060';
this.canvas.style.width = '100%';
this.canvas.style.height = '100%';
this.canvas.tabIndex = 0;
this.canvas.style.outline = 'none';
}
setupEventListeners() {
this.canvasInteractions.setupEventListeners();
}
updateSelection(newSelection) {
this.selectedLayers = newSelection || [];
this.selectedLayer = this.selectedLayers.length > 0 ? this.selectedLayers[this.selectedLayers.length - 1] : null;
if (this.onSelectionChange) {
this.onSelectionChange();
}
}
// Interaction methods moved to CanvasInteractions module
// Delegacja metod operacji na warstwach do CanvasLayers
async copySelectedLayers() {
return this.canvasLayers.copySelectedLayers();
}
pasteLayers() {
return this.canvasLayers.pasteLayers();
}
async handlePaste() {
return this.canvasLayers.handlePaste();
}
handleMouseMove(e) {
// Deleguj do CanvasInteractions
this.canvasInteractions.handleMouseMove(e);
}
handleMouseUp(e) {
// Deleguj do CanvasInteractions
this.canvasInteractions.handleMouseUp(e);
}
handleMouseLeave(e) {
// Deleguj do CanvasInteractions
this.canvasInteractions.handleMouseLeave(e);
}
handleWheel(e) {
// Deleguj do CanvasInteractions
this.canvasInteractions.handleWheel(e);
}
handleKeyDown(e) {
// Deleguj do CanvasInteractions
this.canvasInteractions.handleKeyDown(e);
}
handleKeyUp(e) {
// Deleguj do CanvasInteractions
this.canvasInteractions.handleKeyUp(e);
}
// Wszystkie metody interakcji zostały przeniesione do CanvasInteractions
// Pozostawiamy tylko metody pomocnicze używane przez CanvasInteractions
isRotationHandle(x, y) {
return this.canvasLayers.isRotationHandle(x, y);
}
async addLayerWithImage(image, layerProps = {}) {
return this.canvasLayers.addLayerWithImage(image, layerProps);
}
async addLayer(image) {
return this.addLayerWithImage(image);
}
async removeLayer(index) {
if (index >= 0 && index < this.layers.length) {
const layer = this.layers[index];
if (layer.imageId) {
// Usuń obraz z IndexedDB, jeśli nie jest używany przez inne warstwy
const isImageUsedElsewhere = this.layers.some((l, i) => i !== index && l.imageId === layer.imageId);
if (!isImageUsedElsewhere) {
await removeImage(layer.imageId);
this.imageCache.delete(layer.imageId); // Usuń z pamięci podręcznej
}
}
this.layers.splice(index, 1);
this.selectedLayer = this.layers[this.layers.length - 1] || null;
this.render();
}
}
getMouseWorldCoordinates(e) {
const rect = this.canvas.getBoundingClientRect();
const mouseX_DOM = e.clientX - rect.left;
const mouseY_DOM = e.clientY - rect.top;
const scaleX = this.offscreenCanvas.width / rect.width;
const scaleY = this.offscreenCanvas.height / rect.height;
const mouseX_Buffer = mouseX_DOM * scaleX;
const mouseY_Buffer = mouseY_DOM * scaleY;
const worldX = (mouseX_Buffer / this.viewport.zoom) + this.viewport.x;
const worldY = (mouseY_Buffer / this.viewport.zoom) + this.viewport.y;
return {x: worldX, y: worldY};
}
moveLayer(fromIndex, toIndex) {
return this.canvasLayers.moveLayer(fromIndex, toIndex);
}
resizeLayer(scale) {
this.selectedLayers.forEach(layer => {
layer.width *= scale;
layer.height *= scale;
});
this.render();
this.saveState();
}
rotateLayer(angle) {
this.selectedLayers.forEach(layer => {
layer.rotation += angle;
});
this.render();
this.saveState();
}
updateCanvasSize(width, height, saveHistory = true) {
return this.canvasLayers.updateCanvasSize(width, height, saveHistory);
}
render() {
this.canvasRenderer.render();
}
// Rendering methods moved to CanvasRenderer
getHandles(layer) {
return this.canvasLayers.getHandles(layer);
}
getHandleAtPosition(worldX, worldY) {
return this.canvasLayers.getHandleAtPosition(worldX, worldY);
}
async saveToServer(fileName) {
return this.canvasIO.saveToServer(fileName);
}
async getFlattenedCanvasAsBlob() {
return this.canvasLayers.getFlattenedCanvasAsBlob();
}
async getFlattenedSelectionAsBlob() {
return this.canvasLayers.getFlattenedSelectionAsBlob();
}
moveLayerUp() {
return this.canvasLayers.moveLayerUp();
}
moveLayerDown() {
return this.canvasLayers.moveLayerDown();
}
getLayerAtPosition(worldX, worldY) {
return this.canvasLayers.getLayerAtPosition(worldX, worldY);
}
getResizeHandle(x, y) {
return this.canvasLayers.getResizeHandle(x, y);
}
async mirrorHorizontal() {
return this.canvasLayers.mirrorHorizontal();
}
async mirrorVertical() {
return this.canvasLayers.mirrorVertical();
}
async getLayerImageData(layer) {
return this.canvasLayers.getLayerImageData(layer);
}
addMattedLayer(image, mask) {
return this.canvasLayers.addMattedLayer(image, mask);
}
processInputData(nodeData) {
if (nodeData.input_image) {
this.addInputImage(nodeData.input_image);
}
if (nodeData.input_mask) {
this.addInputMask(nodeData.input_mask);
}
}
addInputImage(imageData) {
const layer = new ImageLayer(imageData);
this.layers.push(layer);
this.updateCanvas();
}
addInputMask(maskData) {
if (this.inputImage) {
const mask = new MaskLayer(maskData);
mask.linkToLayer(this.inputImage);
this.masks.push(mask);
this.updateCanvas();
}
}
async addInputToCanvas(inputImage, inputMask) {
return this.canvasIO.addInputToCanvas(inputImage, inputMask);
}
async convertTensorToImage(tensor) {
return this.canvasIO.convertTensorToImage(tensor);
}
async convertTensorToMask(tensor) {
return this.canvasIO.convertTensorToMask(tensor);
}
async initNodeData() {
return this.canvasIO.initNodeData();
}
scheduleDataCheck() {
return this.canvasIO.scheduleDataCheck();
}
async processImageData(imageData) {
return this.canvasIO.processImageData(imageData);
}
addScaledLayer(image, scale) {
return this.canvasIO.addScaledLayer(image, scale);
}
convertTensorToImageData(tensor) {
return this.canvasIO.convertTensorToImageData(tensor);
}
async createImageFromData(imageData) {
return this.canvasIO.createImageFromData(imageData);
}
async retryDataLoad(maxRetries = 3, delay = 1000) {
return this.canvasIO.retryDataLoad(maxRetries, delay);
}
async processMaskData(maskData) {
return this.canvasIO.processMaskData(maskData);
}
async loadImageFromCache(base64Data) {
return this.canvasIO.loadImageFromCache(base64Data);
}
async importImage(cacheData) {
return this.canvasIO.importImage(cacheData);
}
async importLatestImage() {
return this.canvasIO.importLatestImage();
}
showBlendModeMenu(x, y) {
return this.canvasLayers.showBlendModeMenu(x, y);
}
handleBlendModeSelection(mode) {
return this.canvasLayers.handleBlendModeSelection(mode);
}
showOpacitySlider(mode) {
return this.canvasLayers.showOpacitySlider(mode);
}
applyBlendMode(mode, opacity) {
return this.canvasLayers.applyBlendMode(mode, opacity);
}
}