project migration to typescript

Project migration to typescript
This commit is contained in:
Dariusz L
2025-07-04 04:22:51 +02:00
parent 3e4cdf10bc
commit 5adc77471f
60 changed files with 12565 additions and 3021 deletions

View File

@@ -1,69 +1,55 @@
import {app} from "../../scripts/app.js";
import {api} from "../../scripts/api.js";
import {$el} from "../../scripts/ui.js";
// @ts-ignore
import { app } from "../../scripts/app.js";
// @ts-ignore
import { $el } from "../../scripts/ui.js";
import { addStylesheet, getUrl, loadTemplate } from "./utils/ResourceManager.js";
import {Canvas} from "./Canvas.js";
import {clearAllCanvasStates} from "./db.js";
import {ImageCache} from "./ImageCache.js";
import {generateUniqueFileName} from "./utils/CommonUtils.js";
import {createModuleLogger} from "./utils/LoggerUtils.js";
import { Canvas } from "./Canvas.js";
import { clearAllCanvasStates } from "./db.js";
import { ImageCache } from "./ImageCache.js";
import { createModuleLogger } from "./utils/LoggerUtils.js";
const log = createModuleLogger('Canvas_view');
async function createCanvasWidget(node, widget, app) {
const canvas = new Canvas(node, widget, {
onStateChange: () => updateOutput()
onStateChange: () => updateOutput(node, canvas)
});
const imageCache = new ImageCache();
const helpTooltip = $el("div.painter-tooltip", {
id: `painter-help-tooltip-${node.id}`,
});
const [standardShortcuts, maskShortcuts, systemClipboardTooltip, clipspaceClipboardTooltip] = await Promise.all([
loadTemplate('./templates/standard_shortcuts.html', import.meta.url),
loadTemplate('./templates/mask_shortcuts.html', import.meta.url),
loadTemplate('./templates/system_clipboard_tooltip.html', import.meta.url),
loadTemplate('./templates/clipspace_clipboard_tooltip.html', import.meta.url)
loadTemplate('./templates/standard_shortcuts.html'),
loadTemplate('./templates/mask_shortcuts.html'),
loadTemplate('./templates/system_clipboard_tooltip.html'),
loadTemplate('./templates/clipspace_clipboard_tooltip.html')
]);
document.body.appendChild(helpTooltip);
// Helper function for tooltip positioning
const showTooltip = (buttonElement, content) => {
helpTooltip.innerHTML = content;
helpTooltip.style.visibility = 'hidden';
helpTooltip.style.display = 'block';
const buttonRect = buttonElement.getBoundingClientRect();
const tooltipRect = helpTooltip.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let left = buttonRect.left;
let top = buttonRect.bottom + 5;
if (left + tooltipRect.width > viewportWidth) {
left = viewportWidth - tooltipRect.width - 10;
}
if (top + tooltipRect.height > viewportHeight) {
top = buttonRect.top - tooltipRect.height - 5;
}
if (left < 10) left = 10;
if (top < 10) top = 10;
if (left < 10)
left = 10;
if (top < 10)
top = 10;
helpTooltip.style.left = `${left}px`;
helpTooltip.style.top = `${top}px`;
helpTooltip.style.visibility = 'visible';
};
const hideTooltip = () => {
helpTooltip.style.display = 'none';
};
const controlPanel = $el("div.painterControlPanel", {}, [
$el("div.controls.painter-controls", {
style: {
@@ -73,18 +59,13 @@ async function createCanvasWidget(node, widget, app) {
right: "0",
zIndex: "10",
},
onresize: (entries) => {
const controlsHeight = entries[0].target.offsetHeight;
canvasContainer.style.top = (controlsHeight + 10) + "px";
}
}, [
$el("div.painter-button-group", {}, [
$el("button.painter-button", {
id: `open-editor-btn-${node.id}`,
textContent: "⛶",
title: "Open in Editor",
style: {minWidth: "40px", maxWidth: "40px", fontWeight: "bold"},
style: { minWidth: "40px", maxWidth: "40px", fontWeight: "bold" },
}),
$el("button.painter-button", {
textContent: "?",
@@ -104,21 +85,26 @@ async function createCanvasWidget(node, widget, app) {
textContent: "Add Image",
title: "Add image from file",
onclick: () => {
const fitOnAddWidget = node.widgets.find(w => w.name === "fit_on_add");
const fitOnAddWidget = node.widgets.find((w) => w.name === "fit_on_add");
const addMode = fitOnAddWidget && fitOnAddWidget.value ? 'fit' : 'center';
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.multiple = true;
input.onchange = async (e) => {
for (const file of e.target.files) {
const target = e.target;
if (!target.files)
return;
for (const file of target.files) {
const reader = new FileReader();
reader.onload = (event) => {
const img = new Image();
img.onload = () => {
canvas.addLayer(img, {}, addMode);
};
img.src = event.target.result;
if (event.target?.result) {
img.src = event.target.result;
}
};
reader.readAsDataURL(file);
}
@@ -136,8 +122,7 @@ async function createCanvasWidget(node, widget, app) {
textContent: "Paste Image",
title: "Paste image from clipboard",
onclick: () => {
const fitOnAddWidget = node.widgets.find(w => w.name === "fit_on_add");
const fitOnAddWidget = node.widgets.find((w) => w.name === "fit_on_add");
const addMode = fitOnAddWidget && fitOnAddWidget.value ? 'fit' : 'center';
canvas.canvasLayers.handlePaste(addMode);
}
@@ -158,7 +143,8 @@ async function createCanvasWidget(node, widget, app) {
button.textContent = "📋 Clipspace";
button.title = "Toggle clipboard source: ComfyUI Clipspace";
button.style.backgroundColor = "#4a6cd4";
} else {
}
else {
canvas.canvasLayers.clipboardPreference = 'system';
button.textContent = "📋 System";
button.title = "Toggle clipboard source: System Clipboard";
@@ -175,7 +161,6 @@ async function createCanvasWidget(node, widget, app) {
})
]),
]),
$el("div.painter-separator"),
$el("div.painter-button-group", {}, [
$el("button.painter-button", {
@@ -207,7 +192,7 @@ async function createCanvasWidget(node, widget, app) {
$el("input", {
type: "number",
id: "canvas-width",
value: canvas.width,
value: String(canvas.width),
min: "1",
max: "4096"
})
@@ -228,7 +213,7 @@ async function createCanvasWidget(node, widget, app) {
$el("input", {
type: "number",
id: "canvas-height",
value: canvas.height,
value: String(canvas.height),
min: "1",
max: "4096"
})
@@ -249,15 +234,14 @@ async function createCanvasWidget(node, widget, app) {
])
]);
document.body.appendChild(dialog);
document.getElementById('confirm-size').onclick = () => {
const width = parseInt(document.getElementById('canvas-width').value) || canvas.width;
const height = parseInt(document.getElementById('canvas-height').value) || canvas.height;
const widthInput = document.getElementById('canvas-width');
const heightInput = document.getElementById('canvas-height');
const width = parseInt(widthInput.value) || canvas.width;
const height = parseInt(heightInput.value) || canvas.height;
canvas.updateOutputAreaSize(width, height);
document.body.removeChild(dialog);
};
document.getElementById('cancel-size').onclick = () => {
document.body.removeChild(dialog);
};
@@ -284,7 +268,6 @@ async function createCanvasWidget(node, widget, app) {
onclick: () => canvas.canvasLayers.fuseLayers()
}),
]),
$el("div.painter-separator"),
$el("div.painter-button-group", {}, [
$el("button.painter-button.requires-selection", {
@@ -313,7 +296,6 @@ async function createCanvasWidget(node, widget, app) {
onclick: () => canvas.canvasLayers.mirrorVertical()
}),
]),
$el("div.painter-separator"),
$el("div.painter-button-group", {}, [
$el("button.painter-button.requires-selection.matting-button", {
@@ -321,26 +303,23 @@ async function createCanvasWidget(node, widget, app) {
title: "Perform background removal on the selected layer",
onclick: async (e) => {
const button = e.target.closest('.matting-button');
if (button.classList.contains('loading')) return;
if (button.classList.contains('loading'))
return;
const spinner = $el("div.matting-spinner");
button.appendChild(spinner);
button.classList.add('loading');
try {
if (canvas.canvasSelection.selectedLayers.length !== 1) throw new Error("Please select exactly one image layer for matting.");
if (canvas.canvasSelection.selectedLayers.length !== 1)
throw new Error("Please select exactly one image layer for matting.");
const selectedLayer = canvas.canvasSelection.selectedLayers[0];
const selectedLayerIndex = canvas.layers.indexOf(selectedLayer);
const imageData = await canvas.canvasLayers.getLayerImageData(selectedLayer);
const response = await fetch("/matting", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({image: imageData})
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ image: imageData })
});
const result = await response.json();
if (!response.ok) {
let errorMsg = `Server error: ${response.status} - ${response.statusText}`;
if (result && result.error) {
@@ -351,16 +330,18 @@ async function createCanvasWidget(node, widget, app) {
const mattedImage = new Image();
mattedImage.src = result.matted_image;
await mattedImage.decode();
const newLayer = {...selectedLayer, image: mattedImage};
const newLayer = { ...selectedLayer, image: mattedImage };
delete newLayer.imageId;
canvas.layers[selectedLayerIndex] = newLayer;
canvas.canvasSelection.updateSelection([newLayer]);
canvas.render();
canvas.saveState();
} catch (error) {
}
catch (error) {
log.error("Matting error:", error);
alert(`Matting process failed:\n\n${error.message}`);
} finally {
}
finally {
button.classList.remove('loading');
button.removeChild(spinner);
}
@@ -382,7 +363,7 @@ async function createCanvasWidget(node, widget, app) {
}),
]),
$el("div.painter-separator"),
$el("div.painter-button-group", {id: "mask-controls"}, [
$el("div.painter-button-group", { id: "mask-controls" }, [
$el("button.painter-button.primary", {
id: `toggle-mask-btn-${node.id}`,
textContent: "Show Mask",
@@ -391,11 +372,11 @@ async function createCanvasWidget(node, widget, app) {
const button = e.target;
canvas.maskTool.toggleOverlayVisibility();
canvas.render();
if (canvas.maskTool.isOverlayVisible) {
button.classList.add('primary');
button.textContent = "Show Mask";
} else {
}
else {
button.classList.remove('primary');
button.textContent = "Hide Mask";
}
@@ -405,7 +386,7 @@ async function createCanvasWidget(node, widget, app) {
textContent: "Edit Mask",
title: "Open the current canvas view in the mask editor",
onclick: () => {
canvas.startMaskEditor();
canvas.startMaskEditor(null, true);
}
}),
$el("button.painter-button", {
@@ -415,22 +396,21 @@ async function createCanvasWidget(node, widget, app) {
onclick: () => {
const maskBtn = controlPanel.querySelector('#mask-mode-btn');
const maskControls = controlPanel.querySelector('#mask-controls');
if (canvas.maskTool.isActive) {
canvas.maskTool.deactivate();
maskBtn.classList.remove('primary');
maskControls.querySelectorAll('.mask-control').forEach(c => c.style.display = 'none');
} else {
maskControls.querySelectorAll('.mask-control').forEach((c) => c.style.display = 'none');
}
else {
canvas.maskTool.activate();
maskBtn.classList.add('primary');
maskControls.querySelectorAll('.mask-control').forEach(c => c.style.display = 'flex');
maskControls.querySelectorAll('.mask-control').forEach((c) => c.style.display = 'flex');
}
setTimeout(() => canvas.render(), 0);
}
}),
$el("div.painter-slider-container.mask-control", {style: {display: 'none'}}, [
$el("label", {for: "brush-size-slider", textContent: "Size:"}),
$el("div.painter-slider-container.mask-control", { style: { display: 'none' } }, [
$el("label", { for: "brush-size-slider", textContent: "Size:" }),
$el("input", {
id: "brush-size-slider",
type: "range",
@@ -440,8 +420,8 @@ async function createCanvasWidget(node, widget, app) {
oninput: (e) => canvas.maskTool.setBrushSize(parseInt(e.target.value))
})
]),
$el("div.painter-slider-container.mask-control", {style: {display: 'none'}}, [
$el("label", {for: "brush-strength-slider", textContent: "Strength:"}),
$el("div.painter-slider-container.mask-control", { style: { display: 'none' } }, [
$el("label", { for: "brush-strength-slider", textContent: "Strength:" }),
$el("input", {
id: "brush-strength-slider",
type: "range",
@@ -452,8 +432,8 @@ async function createCanvasWidget(node, widget, app) {
oninput: (e) => canvas.maskTool.setBrushStrength(parseFloat(e.target.value))
})
]),
$el("div.painter-slider-container.mask-control", {style: {display: 'none'}}, [
$el("label", {for: "brush-hardness-slider", textContent: "Hardness:"}),
$el("div.painter-slider-container.mask-control", { style: { display: 'none' } }, [
$el("label", { for: "brush-hardness-slider", textContent: "Hardness:" }),
$el("input", {
id: "brush-hardness-slider",
type: "range",
@@ -467,7 +447,7 @@ async function createCanvasWidget(node, widget, app) {
$el("button.painter-button.mask-control", {
textContent: "Clear Mask",
title: "Clear the entire mask",
style: {display: 'none'},
style: { display: 'none' },
onclick: () => {
if (confirm("Are you sure you want to clear the mask?")) {
canvas.maskTool.clear();
@@ -476,25 +456,22 @@ async function createCanvasWidget(node, widget, app) {
}
})
]),
$el("div.painter-separator"),
$el("div.painter-button-group", {}, [
$el("button.painter-button", {
textContent: "Run GC",
title: "Run Garbage Collection to clean unused images",
style: {backgroundColor: "#4a7c59", borderColor: "#3a6c49"},
style: { backgroundColor: "#4a7c59", borderColor: "#3a6c49" },
onclick: async () => {
try {
const stats = canvas.imageReferenceManager.getStats();
log.info("GC Stats before cleanup:", stats);
await canvas.imageReferenceManager.manualGarbageCollection();
const newStats = canvas.imageReferenceManager.getStats();
log.info("GC Stats after cleanup:", newStats);
alert(`Garbage collection completed!\nTracked images: ${newStats.trackedImages}\nTotal references: ${newStats.totalReferences}\nOperations: ${canvas.imageReferenceManager.operationCount}/${canvas.imageReferenceManager.operationThreshold}`);
} catch (e) {
}
catch (e) {
log.error("Failed to run garbage collection:", e);
alert("Error running garbage collection. Check the console for details.");
}
@@ -503,13 +480,14 @@ async function createCanvasWidget(node, widget, app) {
$el("button.painter-button", {
textContent: "Clear Cache",
title: "Clear all saved canvas states from browser storage",
style: {backgroundColor: "#c54747", borderColor: "#a53737"},
style: { backgroundColor: "#c54747", borderColor: "#a53737" },
onclick: async () => {
if (confirm("Are you sure you want to clear all saved canvas states? This action cannot be undone.")) {
try {
await clearAllCanvasStates();
alert("Canvas cache cleared successfully!");
} catch (e) {
}
catch (e) {
log.error("Failed to clear canvas cache:", e);
alert("Error clearing canvas cache. Check the console for details.");
}
@@ -520,17 +498,16 @@ async function createCanvasWidget(node, widget, app) {
]),
$el("div.painter-separator")
]);
const updateButtonStates = () => {
const selectionCount = canvas.canvasSelection.selectedLayers.length;
const hasSelection = selectionCount > 0;
controlPanel.querySelectorAll('.requires-selection').forEach(btn => {
// Special handling for Fuse button - requires at least 2 layers
if (btn.textContent === 'Fuse') {
btn.disabled = selectionCount < 2;
} else {
btn.disabled = !hasSelection;
controlPanel.querySelectorAll('.requires-selection').forEach((btn) => {
const button = btn;
if (button.textContent === 'Fuse') {
button.disabled = selectionCount < 2;
}
else {
button.disabled = !hasSelection;
}
});
const mattingBtn = controlPanel.querySelector('.matting-button');
@@ -538,91 +515,78 @@ async function createCanvasWidget(node, widget, app) {
mattingBtn.disabled = selectionCount !== 1;
}
};
canvas.canvasSelection.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;
canvas.onHistoryChange = ({ canUndo, canRedo }) => {
if (undoButton)
undoButton.disabled = !canUndo;
if (redoButton)
redoButton.disabled = !canRedo;
};
updateButtonStates();
canvas.updateHistoryButtons();
const triggerWidget = node.widgets.find(w => w.name === "trigger");
const updateOutput = async () => {
triggerWidget.value = (triggerWidget.value + 1) % 99999999;
const updateOutput = async (node, canvas) => {
const triggerWidget = node.widgets.find((w) => w.name === "trigger");
if (triggerWidget) {
triggerWidget.value = (triggerWidget.value + 1) % 99999999;
}
try {
const new_preview = new Image();
const blob = await canvas.getFlattenedCanvasWithMaskAsBlob();
const blob = await canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob();
if (blob) {
new_preview.src = URL.createObjectURL(blob);
await new Promise(r => new_preview.onload = r);
node.imgs = [new_preview];
} else {
}
else {
node.imgs = [];
}
} catch (error) {
}
catch (error) {
console.error("Error updating node preview:", error);
}
};
// Tworzenie panelu warstw
const layersPanel = canvas.canvasLayersPanel.createPanelStructure();
const canvasContainer = $el("div.painterCanvasContainer.painter-container", {
style: {
position: "absolute",
top: "60px", // Wartość początkowa, zostanie nadpisana przez ResizeObserver
top: "60px",
left: "10px",
right: "270px",
bottom: "10px",
overflow: "hidden"
}
}, [canvas.canvas]);
// Kontener dla panelu warstw
const layersPanelContainer = $el("div.painterLayersPanelContainer", {
style: {
position: "absolute",
top: "60px", // Wartość początkowa, zostanie nadpisana przez ResizeObserver
top: "60px",
right: "10px",
width: "250px",
bottom: "10px",
overflow: "hidden"
}
}, [layersPanel]);
const resizeObserver = new ResizeObserver((entries) => {
const controlsHeight = entries[0].target.offsetHeight;
const newTop = (controlsHeight + 10) + "px";
canvasContainer.style.top = newTop;
layersPanelContainer.style.top = newTop;
});
resizeObserver.observe(controlPanel.querySelector('.controls'));
const controlsElement = controlPanel.querySelector('.controls');
if (controlsElement) {
resizeObserver.observe(controlsElement);
}
canvas.canvas.addEventListener('focus', () => {
canvasContainer.classList.add('has-focus');
});
canvas.canvas.addEventListener('blur', () => {
canvasContainer.classList.remove('has-focus');
});
node.onResize = function () {
canvas.render();
};
const mainContainer = $el("div.painterMainContainer", {
style: {
position: "relative",
@@ -630,25 +594,19 @@ async function createCanvasWidget(node, widget, app) {
height: "100%"
}
}, [controlPanel, canvasContainer, layersPanelContainer]);
const mainWidget = node.addDOMWidget("mainContainer", "widget", mainContainer);
node.addDOMWidget("mainContainer", "widget", mainContainer);
const openEditorBtn = controlPanel.querySelector(`#open-editor-btn-${node.id}`);
let backdrop = null;
let modalContent = null;
let originalParent = null;
let isEditorOpen = false;
const closeEditor = () => {
originalParent.appendChild(mainContainer);
document.body.removeChild(backdrop);
if (originalParent && backdrop) {
originalParent.appendChild(mainContainer);
document.body.removeChild(backdrop);
}
isEditorOpen = false;
openEditorBtn.textContent = "⛶";
openEditorBtn.title = "Open in Editor";
setTimeout(() => {
canvas.render();
if (node.onResize) {
@@ -656,30 +614,24 @@ async function createCanvasWidget(node, widget, app) {
}
}, 0);
};
openEditorBtn.onclick = () => {
if (isEditorOpen) {
closeEditor();
return;
}
originalParent = mainContainer.parentNode;
originalParent = mainContainer.parentElement;
if (!originalParent) {
log.error("Could not find original parent of the canvas container!");
return;
}
backdrop = $el("div.painter-modal-backdrop");
modalContent = $el("div.painter-modal-content");
const modalContent = $el("div.painter-modal-content");
modalContent.appendChild(mainContainer);
backdrop.appendChild(modalContent);
document.body.appendChild(backdrop);
isEditorOpen = true;
openEditorBtn.textContent = "X";
openEditorBtn.title = "Close Editor";
setTimeout(() => {
canvas.render();
if (node.onResize) {
@@ -691,175 +643,130 @@ async function createCanvasWidget(node, widget, app) {
window.canvasExecutionStates = new Map();
}
node.canvasWidget = canvas;
setTimeout(() => {
canvas.loadInitialState();
// Renderuj panel warstw po załadowaniu stanu
if (canvas.canvasLayersPanel) {
canvas.canvasLayersPanel.renderLayers();
}
}, 100);
const showPreviewWidget = node.widgets.find(w => w.name === "show_preview");
const showPreviewWidget = node.widgets.find((w) => w.name === "show_preview");
if (showPreviewWidget) {
const originalCallback = showPreviewWidget.callback;
showPreviewWidget.callback = function (value) {
if (originalCallback) {
originalCallback.call(this, value);
}
if (canvas && canvas.setPreviewVisibility) {
canvas.setPreviewVisibility(value);
}
if (node.graph && node.graph.canvas) {
node.setDirtyCanvas(true, true);
}
};
}
return {
canvas: canvas,
panel: controlPanel
};
}
const canvasNodeInstances = new Map();
app.registerExtension({
name: "Comfy.CanvasNode",
init() {
addStylesheet(getUrl('./css/canvas_view.css', import.meta.url));
addStylesheet(getUrl('./css/canvas_view.css'));
const originalQueuePrompt = app.queuePrompt;
app.queuePrompt = async function (number, prompt) {
log.info("Preparing to queue prompt...");
if (canvasNodeInstances.size > 0) {
log.info(`Found ${canvasNodeInstances.size} CanvasNode(s). Sending data via WebSocket...`);
const sendPromises = [];
for (const [nodeId, canvasWidget] of canvasNodeInstances.entries()) {
if (app.graph.getNodeById(nodeId) && canvasWidget.canvas && canvasWidget.canvas.canvasIO) {
log.debug(`Sending data for canvas node ${nodeId}`);
sendPromises.push(canvasWidget.canvas.canvasIO.sendDataViaWebSocket(nodeId));
} else {
}
else {
log.warn(`Node ${nodeId} not found in graph, removing from instances map.`);
canvasNodeInstances.delete(nodeId);
}
}
try {
await Promise.all(sendPromises);
log.info("All canvas data has been sent and acknowledged by the server.");
} catch (error) {
}
catch (error) {
log.error("Failed to send canvas data for one or more nodes. Aborting prompt.", error);
alert(`CanvasNode Error: ${error.message}`);
return; // Stop execution
return;
}
}
log.info("All pre-prompt tasks complete. Proceeding with original queuePrompt.");
return originalQueuePrompt.apply(this, arguments);
};
},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeType.comfyClass === "CanvasNode") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
log.debug("CanvasNode onNodeCreated: Base widget setup.");
const r = onNodeCreated?.apply(this, arguments);
this.size = [1150, 1000];
this.size = [1150, 1000];
return r;
};
nodeType.prototype.onAdded = async function () {
log.info(`CanvasNode onAdded, ID: ${this.id}`);
log.debug(`Available widgets in onAdded:`, this.widgets.map(w => w.name));
log.debug(`Available widgets in onAdded:`, this.widgets.map((w) => w.name));
if (this.canvasWidget) {
log.warn(`CanvasNode ${this.id} already initialized. Skipping onAdded setup.`);
return;
}
this.widgets.forEach(w => {
this.widgets.forEach((w) => {
log.debug(`Widget name: ${w.name}, type: ${w.type}, value: ${w.value}`);
});
const nodeIdWidget = this.widgets.find(w => w.name === "node_id");
const nodeIdWidget = this.widgets.find((w) => w.name === "node_id");
if (nodeIdWidget) {
nodeIdWidget.value = String(this.id);
log.debug(`Set hidden node_id widget to: ${nodeIdWidget.value}`);
} else {
}
else {
log.error("Could not find the hidden node_id widget!");
}
const canvasWidget = await createCanvasWidget(this, null, app);
canvasNodeInstances.set(this.id, canvasWidget);
log.info(`Registered CanvasNode instance for ID: ${this.id}`);
// Use a timeout to ensure the DOM has updated before we redraw.
setTimeout(() => {
this.setDirtyCanvas(true, true);
}, 100);
};
const onRemoved = nodeType.prototype.onRemoved;
nodeType.prototype.onRemoved = function () {
log.info(`Cleaning up canvas node ${this.id}`);
canvasNodeInstances.delete(this.id);
log.info(`Deregistered CanvasNode instance for ID: ${this.id}`);
if (window.canvasExecutionStates) {
window.canvasExecutionStates.delete(this.id);
}
const tooltip = document.getElementById(`painter-help-tooltip-${this.id}`);
if (tooltip) {
tooltip.remove();
}
const backdrop = document.querySelector('.painter-modal-backdrop');
if (backdrop && backdrop.contains(this.canvasWidget?.canvas)) {
if (backdrop && this.canvasWidget && backdrop.contains(this.canvasWidget.canvas.canvas)) {
document.body.removeChild(backdrop);
}
if (this.canvasWidget && this.canvasWidget.destroy) {
this.canvasWidget.destroy();
}
return onRemoved?.apply(this, arguments);
};
const originalGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function (_, options) {
originalGetExtraMenuOptions?.apply(this, arguments);
const self = this;
const maskEditorIndex = options.findIndex(option =>
option && option.content === "Open in MaskEditor"
);
const maskEditorIndex = options.findIndex((option) => option && option.content === "Open in MaskEditor");
if (maskEditorIndex !== -1) {
options.splice(maskEditorIndex, 1);
}
const newOptions = [
{
content: "Open in MaskEditor",
@@ -867,12 +774,14 @@ app.registerExtension({
try {
log.info("Opening LayerForge canvas in MaskEditor");
if (self.canvasWidget && self.canvasWidget.startMaskEditor) {
await self.canvasWidget.startMaskEditor();
} else {
await self.canvasWidget.startMaskEditor(null, true);
}
else {
log.error("Canvas widget not available");
alert("Canvas not ready. Please try again.");
}
} catch (e) {
}
catch (e) {
log.error("Error opening MaskEditor:", e);
alert(`Failed to open MaskEditor: ${e.message}`);
}
@@ -882,11 +791,16 @@ app.registerExtension({
content: "Open Image",
callback: async () => {
try {
if (!self.canvasWidget)
return;
const blob = await self.canvasWidget.getFlattenedCanvasAsBlob();
if (!blob)
return;
const url = URL.createObjectURL(blob);
window.open(url, '_blank');
setTimeout(() => URL.revokeObjectURL(url), 1000);
} catch (e) {
}
catch (e) {
log.error("Error opening image:", e);
}
},
@@ -895,11 +809,16 @@ app.registerExtension({
content: "Open Image with Mask Alpha",
callback: async () => {
try {
if (!self.canvasWidget)
return;
const blob = await self.canvasWidget.getFlattenedCanvasWithMaskAsBlob();
if (!blob)
return;
const url = URL.createObjectURL(blob);
window.open(url, '_blank');
setTimeout(() => URL.revokeObjectURL(url), 1000);
} catch (e) {
}
catch (e) {
log.error("Error opening image with mask:", e);
}
},
@@ -908,11 +827,16 @@ app.registerExtension({
content: "Copy Image",
callback: async () => {
try {
if (!self.canvasWidget)
return;
const blob = await self.canvasWidget.getFlattenedCanvasAsBlob();
const item = new ClipboardItem({'image/png': blob});
if (!blob)
return;
const item = new ClipboardItem({ 'image/png': blob });
await navigator.clipboard.write([item]);
log.info("Image copied to clipboard.");
} catch (e) {
}
catch (e) {
log.error("Error copying image:", e);
alert("Failed to copy image to clipboard.");
}
@@ -922,11 +846,16 @@ app.registerExtension({
content: "Copy Image with Mask Alpha",
callback: async () => {
try {
if (!self.canvasWidget)
return;
const blob = await self.canvasWidget.getFlattenedCanvasWithMaskAsBlob();
const item = new ClipboardItem({'image/png': blob});
if (!blob)
return;
const item = new ClipboardItem({ 'image/png': blob });
await navigator.clipboard.write([item]);
log.info("Image with mask alpha copied to clipboard.");
} catch (e) {
}
catch (e) {
log.error("Error copying image with mask:", e);
alert("Failed to copy image with mask to clipboard.");
}
@@ -936,7 +865,11 @@ app.registerExtension({
content: "Save Image",
callback: async () => {
try {
if (!self.canvasWidget)
return;
const blob = await self.canvasWidget.getFlattenedCanvasAsBlob();
if (!blob)
return;
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
@@ -945,7 +878,8 @@ app.registerExtension({
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(url), 1000);
} catch (e) {
}
catch (e) {
log.error("Error saving image:", e);
}
},
@@ -954,7 +888,11 @@ app.registerExtension({
content: "Save Image with Mask Alpha",
callback: async () => {
try {
if (!self.canvasWidget)
return;
const blob = await self.canvasWidget.getFlattenedCanvasWithMaskAsBlob();
if (!blob)
return;
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
@@ -963,24 +901,18 @@ app.registerExtension({
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(url), 1000);
} catch (e) {
}
catch (e) {
log.error("Error saving image with mask:", e);
}
},
},
];
if (options.length > 0) {
options.unshift({content: "___", disabled: true});
options.unshift({ content: "___", disabled: true });
}
options.unshift(...newOptions);
};
}
}
});
async function handleImportInput(data) {
if (data && data.image) {
const imageData = data.image;
await importImage(imageData);
}
}