mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-22 05:02:11 -03:00
Replaces setPreviewVisibility(false) with direct assignment to previewVisible in Canvas and Canvas.ts. Adds initialization of preview state based on widget value in CanvasView and CanvasView.ts to ensure correct preview visibility on widget creation.
481 lines
18 KiB
JavaScript
481 lines
18 KiB
JavaScript
// @ts-ignore
|
|
import { api } from "../../scripts/api.js";
|
|
import { MaskTool } from "./MaskTool.js";
|
|
import { CanvasState } from "./CanvasState.js";
|
|
import { CanvasInteractions } from "./CanvasInteractions.js";
|
|
import { CanvasLayers } from "./CanvasLayers.js";
|
|
import { CanvasLayersPanel } from "./CanvasLayersPanel.js";
|
|
import { CanvasRenderer } from "./CanvasRenderer.js";
|
|
import { CanvasIO } from "./CanvasIO.js";
|
|
import { ImageReferenceManager } from "./ImageReferenceManager.js";
|
|
import { BatchPreviewManager } from "./BatchPreviewManager.js";
|
|
import { createModuleLogger } from "./utils/LoggerUtils.js";
|
|
import { debounce } from "./utils/CommonUtils.js";
|
|
import { CanvasMask } from "./CanvasMask.js";
|
|
import { CanvasSelection } from "./CanvasSelection.js";
|
|
const useChainCallback = (original, next) => {
|
|
if (original === undefined || original === null) {
|
|
return next;
|
|
}
|
|
return function (...args) {
|
|
const originalReturn = original.apply(this, args);
|
|
const nextReturn = next.apply(this, args);
|
|
return nextReturn === undefined ? originalReturn : nextReturn;
|
|
};
|
|
};
|
|
const log = createModuleLogger('Canvas');
|
|
/**
|
|
* Canvas - Fasada dla systemu rysowania
|
|
*
|
|
* Klasa Canvas pełni rolę fasady, oferując uproszczony interfejs wysokiego poziomu
|
|
* dla złożonego systemu rysowania. Zamiast eksponować wszystkie metody modułów,
|
|
* udostępnia tylko kluczowe operacje i umożliwia bezpośredni dostęp do modułów
|
|
* gdy potrzebna jest bardziej szczegółowa kontrola.
|
|
*/
|
|
export class Canvas {
|
|
constructor(node, widget, callbacks = {}) {
|
|
this.node = node;
|
|
this.widget = widget;
|
|
this.canvas = document.createElement('canvas');
|
|
const ctx = this.canvas.getContext('2d', { willReadFrequently: true });
|
|
if (!ctx)
|
|
throw new Error("Could not create canvas context");
|
|
this.ctx = ctx;
|
|
this.width = 512;
|
|
this.height = 512;
|
|
this.layers = [];
|
|
this.onStateChange = callbacks.onStateChange;
|
|
this.onHistoryChange = callbacks.onHistoryChange;
|
|
this.lastMousePosition = { x: 0, y: 0 };
|
|
this.viewport = {
|
|
x: -(this.width / 4),
|
|
y: -(this.height / 4),
|
|
zoom: 0.8,
|
|
};
|
|
this.offscreenCanvas = document.createElement('canvas');
|
|
this.offscreenCtx = this.offscreenCanvas.getContext('2d', {
|
|
alpha: false
|
|
});
|
|
this.dataInitialized = false;
|
|
this.pendingDataCheck = null;
|
|
this.imageCache = new Map();
|
|
this.requestSaveState = () => { };
|
|
this.maskTool = new MaskTool(this, { onStateChange: this.onStateChange });
|
|
this.canvasMask = new CanvasMask(this);
|
|
this.canvasState = new CanvasState(this);
|
|
this.canvasSelection = new CanvasSelection(this);
|
|
this.canvasInteractions = new CanvasInteractions(this);
|
|
this.canvasLayers = new CanvasLayers(this);
|
|
this.canvasLayersPanel = new CanvasLayersPanel(this);
|
|
this.canvasRenderer = new CanvasRenderer(this);
|
|
this.canvasIO = new CanvasIO(this);
|
|
this.imageReferenceManager = new ImageReferenceManager(this);
|
|
this.batchPreviewManagers = [];
|
|
this.pendingBatchContext = null;
|
|
this.interaction = this.canvasInteractions.interaction;
|
|
this.previewVisible = false;
|
|
this.isMouseOver = false;
|
|
this._initializeModules();
|
|
this._setupCanvas();
|
|
log.debug('Canvas widget element:', this.node);
|
|
log.info('Canvas initialized', {
|
|
nodeId: this.node.id,
|
|
dimensions: { width: this.width, height: this.height },
|
|
viewport: this.viewport
|
|
});
|
|
this.previewVisible = false;
|
|
}
|
|
async waitForWidget(name, node, interval = 100, timeout = 20000) {
|
|
const startTime = Date.now();
|
|
return new Promise((resolve, reject) => {
|
|
const check = () => {
|
|
const widget = node.widgets.find((w) => w.name === name);
|
|
if (widget) {
|
|
resolve(widget);
|
|
}
|
|
else if (Date.now() - startTime > timeout) {
|
|
reject(new Error(`Widget "${name}" not found within timeout.`));
|
|
}
|
|
else {
|
|
setTimeout(check, interval);
|
|
}
|
|
};
|
|
check();
|
|
});
|
|
}
|
|
/**
|
|
* Kontroluje widoczność podglądu canvas
|
|
* @param {boolean} visible - Czy podgląd ma być widoczny
|
|
*/
|
|
async setPreviewVisibility(visible) {
|
|
this.previewVisible = visible;
|
|
log.info("Canvas preview visibility set to:", visible);
|
|
const imagePreviewWidget = await this.waitForWidget("$$canvas-image-preview", this.node);
|
|
if (imagePreviewWidget) {
|
|
log.debug("Found $$canvas-image-preview widget, controlling visibility");
|
|
if (visible) {
|
|
if (imagePreviewWidget.options) {
|
|
imagePreviewWidget.options.hidden = false;
|
|
}
|
|
if ('visible' in imagePreviewWidget) {
|
|
imagePreviewWidget.visible = true;
|
|
}
|
|
if ('hidden' in imagePreviewWidget) {
|
|
imagePreviewWidget.hidden = false;
|
|
}
|
|
imagePreviewWidget.computeSize = function () {
|
|
return [0, 250]; // Szerokość 0 (auto), wysokość 250
|
|
};
|
|
}
|
|
else {
|
|
if (imagePreviewWidget.options) {
|
|
imagePreviewWidget.options.hidden = true;
|
|
}
|
|
if ('visible' in imagePreviewWidget) {
|
|
imagePreviewWidget.visible = false;
|
|
}
|
|
if ('hidden' in imagePreviewWidget) {
|
|
imagePreviewWidget.hidden = true;
|
|
}
|
|
imagePreviewWidget.computeSize = function () {
|
|
return [0, 0]; // Szerokość 0, wysokość 0
|
|
};
|
|
}
|
|
this.render();
|
|
}
|
|
else {
|
|
log.warn("$$canvas-image-preview widget not found in Canvas.js");
|
|
}
|
|
}
|
|
/**
|
|
* Inicjalizuje moduły systemu canvas
|
|
* @private
|
|
*/
|
|
_initializeModules() {
|
|
log.debug('Initializing Canvas modules...');
|
|
// Stwórz opóźnioną wersję funkcji zapisu stanu
|
|
this.requestSaveState = debounce(() => this.saveState(), 500);
|
|
this._setupAutoRefreshHandlers();
|
|
log.debug('Canvas modules initialized successfully');
|
|
}
|
|
/**
|
|
* Konfiguruje podstawowe właściwości canvas
|
|
* @private
|
|
*/
|
|
_setupCanvas() {
|
|
this.initCanvas();
|
|
this.canvasInteractions.setupEventListeners();
|
|
this.canvasIO.initNodeData();
|
|
this.layers = this.layers.map((layer) => ({
|
|
...layer,
|
|
opacity: 1
|
|
}));
|
|
}
|
|
/**
|
|
* Ładuje stan canvas z bazy danych
|
|
*/
|
|
async loadInitialState() {
|
|
log.info("Loading initial state for node:", this.node.id);
|
|
const loaded = await this.canvasState.loadStateFromDB();
|
|
if (!loaded) {
|
|
log.info("No saved state found, initializing from node data.");
|
|
await this.canvasIO.initNodeData();
|
|
}
|
|
this.saveState();
|
|
this.render();
|
|
// Dodaj to wywołanie, aby panel renderował się po załadowaniu stanu
|
|
if (this.canvasLayersPanel) {
|
|
this.canvasLayersPanel.onLayersChanged();
|
|
}
|
|
}
|
|
/**
|
|
* Zapisuje obecny stan
|
|
* @param {boolean} replaceLast - Czy zastąpić ostatni stan w historii
|
|
*/
|
|
saveState(replaceLast = false) {
|
|
log.debug('Saving canvas state', { replaceLast, layersCount: this.layers.length });
|
|
this.canvasState.saveState(replaceLast);
|
|
this.incrementOperationCount();
|
|
this._notifyStateChange();
|
|
}
|
|
/**
|
|
* Cofnij ostatnią operację
|
|
*/
|
|
undo() {
|
|
log.info('Performing undo operation');
|
|
const historyInfo = this.canvasState.getHistoryInfo();
|
|
log.debug('History state before undo:', historyInfo);
|
|
this.canvasState.undo();
|
|
this.incrementOperationCount();
|
|
this._notifyStateChange();
|
|
// Powiadom panel warstw o zmianie stanu warstw
|
|
if (this.canvasLayersPanel) {
|
|
this.canvasLayersPanel.onLayersChanged();
|
|
this.canvasLayersPanel.onSelectionChanged();
|
|
}
|
|
log.debug('Undo completed, layers count:', this.layers.length);
|
|
}
|
|
/**
|
|
* Ponów cofniętą operację
|
|
*/
|
|
redo() {
|
|
log.info('Performing redo operation');
|
|
const historyInfo = this.canvasState.getHistoryInfo();
|
|
log.debug('History state before redo:', historyInfo);
|
|
this.canvasState.redo();
|
|
this.incrementOperationCount();
|
|
this._notifyStateChange();
|
|
// Powiadom panel warstw o zmianie stanu warstw
|
|
if (this.canvasLayersPanel) {
|
|
this.canvasLayersPanel.onLayersChanged();
|
|
this.canvasLayersPanel.onSelectionChanged();
|
|
}
|
|
log.debug('Redo completed, layers count:', this.layers.length);
|
|
}
|
|
/**
|
|
* Renderuje canvas
|
|
*/
|
|
render() {
|
|
this.canvasRenderer.render();
|
|
}
|
|
/**
|
|
* Dodaje warstwę z obrazem
|
|
* @param {Image} image - Obraz do dodania
|
|
* @param {Object} layerProps - Właściwości warstwy
|
|
* @param {string} addMode - Tryb dodawania
|
|
*/
|
|
async addLayer(image, layerProps = {}, addMode = 'default') {
|
|
const result = await this.canvasLayers.addLayerWithImage(image, layerProps, addMode);
|
|
// Powiadom panel warstw o dodaniu nowej warstwy
|
|
if (this.canvasLayersPanel) {
|
|
this.canvasLayersPanel.onLayersChanged();
|
|
}
|
|
return result;
|
|
}
|
|
/**
|
|
* Usuwa wybrane warstwy
|
|
*/
|
|
removeLayersByIds(layerIds) {
|
|
if (!layerIds || layerIds.length === 0)
|
|
return;
|
|
const initialCount = this.layers.length;
|
|
this.saveState();
|
|
this.layers = this.layers.filter((l) => !layerIds.includes(l.id));
|
|
// If the current selection was part of the removal, clear it
|
|
const newSelection = this.canvasSelection.selectedLayers.filter((l) => !layerIds.includes(l.id));
|
|
this.canvasSelection.updateSelection(newSelection);
|
|
this.render();
|
|
this.saveState();
|
|
if (this.canvasLayersPanel) {
|
|
this.canvasLayersPanel.onLayersChanged();
|
|
}
|
|
log.info(`Removed ${initialCount - this.layers.length} layers by ID.`);
|
|
}
|
|
removeSelectedLayers() {
|
|
return this.canvasSelection.removeSelectedLayers();
|
|
}
|
|
/**
|
|
* Duplikuje zaznaczone warstwy (w pamięci, bez zapisu stanu)
|
|
*/
|
|
duplicateSelectedLayers() {
|
|
return this.canvasSelection.duplicateSelectedLayers();
|
|
}
|
|
/**
|
|
* Aktualizuje zaznaczenie warstw i powiadamia wszystkie komponenty.
|
|
* To jest "jedyne źródło prawdy" o zmianie zaznaczenia.
|
|
* @param {Array} newSelection - Nowa lista zaznaczonych warstw
|
|
*/
|
|
updateSelection(newSelection) {
|
|
return this.canvasSelection.updateSelection(newSelection);
|
|
}
|
|
/**
|
|
* Logika aktualizacji zaznaczenia, wywoływana przez panel warstw.
|
|
*/
|
|
updateSelectionLogic(layer, isCtrlPressed, isShiftPressed, index) {
|
|
return this.canvasSelection.updateSelectionLogic(layer, isCtrlPressed, isShiftPressed, index);
|
|
}
|
|
/**
|
|
* Zmienia rozmiar obszaru wyjściowego
|
|
* @param {number} width - Nowa szerokość
|
|
* @param {number} height - Nowa wysokość
|
|
* @param {boolean} saveHistory - Czy zapisać w historii
|
|
*/
|
|
updateOutputAreaSize(width, height, saveHistory = true) {
|
|
return this.canvasLayers.updateOutputAreaSize(width, height, saveHistory);
|
|
}
|
|
/**
|
|
* Eksportuje spłaszczony canvas jako blob
|
|
*/
|
|
async getFlattenedCanvasAsBlob() {
|
|
return this.canvasLayers.getFlattenedCanvasAsBlob();
|
|
}
|
|
/**
|
|
* Eksportuje spłaszczony canvas z maską jako kanałem alpha
|
|
*/
|
|
async getFlattenedCanvasWithMaskAsBlob() {
|
|
return this.canvasLayers.getFlattenedCanvasWithMaskAsBlob();
|
|
}
|
|
/**
|
|
* Importuje najnowszy obraz
|
|
*/
|
|
async importLatestImage() {
|
|
return this.canvasIO.importLatestImage();
|
|
}
|
|
_setupAutoRefreshHandlers() {
|
|
let lastExecutionStartTime = 0;
|
|
// Helper function to get auto-refresh value from node widget
|
|
const getAutoRefreshValue = () => {
|
|
const widget = this.node.widgets.find((w) => w.name === 'auto_refresh_after_generation');
|
|
return widget ? widget.value : false;
|
|
};
|
|
const handleExecutionStart = () => {
|
|
if (getAutoRefreshValue()) {
|
|
lastExecutionStartTime = Date.now();
|
|
// Store a snapshot of the context for the upcoming batch
|
|
this.pendingBatchContext = {
|
|
// For the menu position
|
|
spawnPosition: {
|
|
x: this.width / 2,
|
|
y: this.height
|
|
},
|
|
// For the image placement
|
|
outputArea: {
|
|
x: 0,
|
|
y: 0,
|
|
width: this.width,
|
|
height: this.height
|
|
}
|
|
};
|
|
log.debug(`Execution started, pending batch context captured:`, this.pendingBatchContext);
|
|
this.render(); // Trigger render to show the pending outline immediately
|
|
}
|
|
};
|
|
const handleExecutionSuccess = async () => {
|
|
if (getAutoRefreshValue()) {
|
|
log.info('Auto-refresh triggered, importing latest images.');
|
|
if (!this.pendingBatchContext) {
|
|
log.warn("execution_start did not fire, cannot process batch. Awaiting next execution.");
|
|
return;
|
|
}
|
|
// Use the captured output area for image import
|
|
const newLayers = await this.canvasIO.importLatestImages(lastExecutionStartTime, this.pendingBatchContext.outputArea);
|
|
if (newLayers && newLayers.length > 1) {
|
|
const newManager = new BatchPreviewManager(this, this.pendingBatchContext.spawnPosition, this.pendingBatchContext.outputArea);
|
|
this.batchPreviewManagers.push(newManager);
|
|
newManager.show(newLayers);
|
|
}
|
|
// Consume the context
|
|
this.pendingBatchContext = null;
|
|
// Final render to clear the outline if it was the last one
|
|
this.render();
|
|
}
|
|
};
|
|
api.addEventListener('execution_start', handleExecutionStart);
|
|
api.addEventListener('execution_success', handleExecutionSuccess);
|
|
this.node.onRemoved = useChainCallback(this.node.onRemoved, () => {
|
|
log.info('Node removed, cleaning up auto-refresh listeners.');
|
|
api.removeEventListener('execution_start', handleExecutionStart);
|
|
api.removeEventListener('execution_success', handleExecutionSuccess);
|
|
});
|
|
log.debug('Auto-refresh handlers setup complete, reading from node widget: auto_refresh_after_generation');
|
|
}
|
|
/**
|
|
* Uruchamia edytor masek
|
|
* @param {Image|HTMLCanvasElement|null} predefinedMask - Opcjonalna maska do nałożenia po otwarciu editora
|
|
* @param {boolean} sendCleanImage - Czy wysłać czysty obraz (bez maski) do editora
|
|
*/
|
|
async startMaskEditor(predefinedMask = null, sendCleanImage = true) {
|
|
return this.canvasMask.startMaskEditor(predefinedMask, sendCleanImage);
|
|
}
|
|
/**
|
|
* Inicjalizuje podstawowe właściwości canvas
|
|
*/
|
|
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';
|
|
}
|
|
/**
|
|
* Pobiera współrzędne myszy w układzie świata
|
|
* @param {MouseEvent} e - Zdarzenie myszy
|
|
*/
|
|
getMouseWorldCoordinates(e) {
|
|
const rect = this.canvas.getBoundingClientRect();
|
|
const mouseX_DOM = e.clientX - rect.left;
|
|
const mouseY_DOM = e.clientY - rect.top;
|
|
if (!this.offscreenCanvas)
|
|
throw new Error("Offscreen canvas not initialized");
|
|
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 };
|
|
}
|
|
/**
|
|
* Pobiera współrzędne myszy w układzie widoku
|
|
* @param {MouseEvent} e - Zdarzenie myszy
|
|
*/
|
|
getMouseViewCoordinates(e) {
|
|
const rect = this.canvas.getBoundingClientRect();
|
|
const mouseX_DOM = e.clientX - rect.left;
|
|
const mouseY_DOM = e.clientY - rect.top;
|
|
const scaleX = this.canvas.width / rect.width;
|
|
const scaleY = this.canvas.height / rect.height;
|
|
const mouseX_Canvas = mouseX_DOM * scaleX;
|
|
const mouseY_Canvas = mouseY_DOM * scaleY;
|
|
return { x: mouseX_Canvas, y: mouseY_Canvas };
|
|
}
|
|
/**
|
|
* Aktualizuje zaznaczenie po operacji historii
|
|
*/
|
|
updateSelectionAfterHistory() {
|
|
return this.canvasSelection.updateSelectionAfterHistory();
|
|
}
|
|
/**
|
|
* Aktualizuje przyciski historii
|
|
*/
|
|
updateHistoryButtons() {
|
|
if (this.onHistoryChange) {
|
|
const historyInfo = this.canvasState.getHistoryInfo();
|
|
this.onHistoryChange({
|
|
canUndo: historyInfo.canUndo,
|
|
canRedo: historyInfo.canRedo
|
|
});
|
|
}
|
|
}
|
|
/**
|
|
* Zwiększa licznik operacji (dla garbage collection)
|
|
*/
|
|
incrementOperationCount() {
|
|
if (this.imageReferenceManager) {
|
|
this.imageReferenceManager.incrementOperationCount();
|
|
}
|
|
}
|
|
/**
|
|
* Czyści zasoby canvas
|
|
*/
|
|
destroy() {
|
|
if (this.imageReferenceManager) {
|
|
this.imageReferenceManager.destroy();
|
|
}
|
|
log.info("Canvas destroyed");
|
|
}
|
|
/**
|
|
* Powiadamia o zmianie stanu
|
|
* @private
|
|
*/
|
|
_notifyStateChange() {
|
|
if (this.onStateChange) {
|
|
this.onStateChange();
|
|
}
|
|
}
|
|
}
|