mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-21 20:52:12 -03:00
442 lines
12 KiB
JavaScript
442 lines
12 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) {
|
|
this.onHistoryChange({
|
|
canUndo: this.canvasState.undoStack.length > 1,
|
|
canRedo: this.canvasState.redoStack.length > 0
|
|
});
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|