Files
Gin3601-LLS-node/web/js/lls_mask_draw.js
T

919 lines
27 KiB
JavaScript

import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
const EXTENSION_NAME = "LLS.MaskDraw";
const TARGET_NODE_CLASS = "LLSSimpleMaskDraw";
const TARGET_NODE_DISPLAY_NAME = "LLS Simple Mask Draw";
const HISTORY_LIMIT = 20;
const MAX_PREVIEW_EDGE = 420;
const MIN_PREVIEW_EDGE = 220;
function clamp(value, min, max) {
return Math.min(max, Math.max(min, value));
}
function asNumber(value, fallback) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : fallback;
}
function asBoolean(value, fallback = false) {
return typeof value === "boolean" ? value : fallback;
}
function findWidget(node, name) {
return node.widgets?.find((widget) => widget.name === name) ?? null;
}
function getNodeClassName(node) {
return String(node?.comfyClass || node?.constructor?.comfyClass || node?.type || node?.title || "");
}
function hideWidget(widget) {
if (!widget) {
return;
}
widget.type = "hidden";
widget.computeSize = () => [0, -4];
}
function chainWidgetCallback(widget, handler) {
if (!widget || widget.__llsMaskDrawChained) {
return;
}
const previous = widget.callback;
widget.callback = function chainedMaskDrawCallback() {
previous?.apply(this, arguments);
handler?.apply(this, arguments);
};
widget.__llsMaskDrawChained = true;
}
function parseState(rawValue) {
try {
const parsed = JSON.parse(rawValue || "{}");
if (!parsed || typeof parsed !== "object") {
throw new Error("Invalid state payload");
}
return {
version: asNumber(parsed.version, 1),
mask_png_base64: String(parsed.mask_png_base64 || ""),
touched: asBoolean(parsed.touched, false),
editor: parsed.editor && typeof parsed.editor === "object" ? parsed.editor : {},
};
} catch {
return {
version: 1,
mask_png_base64: "",
touched: false,
editor: {},
};
}
}
function dataUrlToBase64(dataUrl) {
const commaIndex = String(dataUrl || "").indexOf(",");
return commaIndex >= 0 ? dataUrl.slice(commaIndex + 1) : "";
}
function getInputLink(node, inputName) {
const input = node.inputs?.find((item) => item.name === inputName);
if (!input || input.link == null) {
return null;
}
return node.graph?.links?.[input.link] ?? null;
}
function getUpstreamNode(node, inputName) {
const link = getInputLink(node, inputName);
if (!link) {
return null;
}
return node.graph?.getNodeById?.(link.origin_id) ?? null;
}
function normalizePath(rawValue) {
return String(rawValue || "").trim().replace(/\\/g, "/");
}
function splitAnnotatedPath(rawValue) {
const normalized = normalizePath(rawValue);
const match = normalized.match(/^(.*?)(?:\s+\[([^\]]+)\])?$/);
const path = match?.[1] ?? normalized;
const annotatedType = match?.[2] ?? "";
const slashIndex = path.lastIndexOf("/");
return {
path,
annotatedType,
subfolder: slashIndex >= 0 ? path.slice(0, slashIndex) : "",
filename: slashIndex >= 0 ? path.slice(slashIndex + 1) : path,
};
}
function buildViewUrl(rawValue, fallbackType, extraParams = {}) {
const normalized = normalizePath(rawValue);
if (!normalized) {
return null;
}
const params = new URLSearchParams();
if (normalized.startsWith("blake3:")) {
params.set("filename", normalized);
} else {
const parsed = splitAnnotatedPath(normalized);
if (!parsed.filename) {
return null;
}
params.set("filename", parsed.filename);
if (parsed.subfolder) {
params.set("subfolder", parsed.subfolder);
}
params.set("type", parsed.annotatedType || fallbackType || "input");
}
for (const [key, value] of Object.entries(extraParams)) {
if (value != null && value !== "") {
params.set(key, String(value));
}
}
const relativePath = `/view?${params.toString()}`;
return typeof api?.apiURL === "function" ? api.apiURL(relativePath) : relativePath;
}
function resolveImageSource(node) {
const upstream = getUpstreamNode(node, "image");
const className = getNodeClassName(upstream);
const imageWidget = findWidget(upstream, "image");
if (!imageWidget?.value) {
return null;
}
if (className === "LoadImage") {
return {
url: buildViewUrl(imageWidget.value, "input"),
label: "LoadImage",
};
}
if (className === "LoadImageOutput") {
return {
url: buildViewUrl(imageWidget.value, "output"),
label: "LoadImageOutput",
};
}
return null;
}
function resolveMaskSource(node) {
const upstream = getUpstreamNode(node, "input_mask");
const className = getNodeClassName(upstream);
const imageWidget = findWidget(upstream, "image");
if (!imageWidget?.value) {
return null;
}
if (className === "LoadImageMask") {
const channelName = String(findWidget(upstream, "channel")?.value || "alpha").toLowerCase();
if (channelName === "alpha") {
return {
url: buildViewUrl(imageWidget.value, "input", { channel: "a" }),
channel: "a",
invert: true,
label: "LoadImageMask.alpha",
};
}
const channelMap = {
red: "r",
green: "g",
blue: "b",
};
const mappedChannel = channelMap[channelName];
if (!mappedChannel) {
return null;
}
return {
url: buildViewUrl(imageWidget.value, "input"),
channel: mappedChannel,
invert: false,
label: `LoadImageMask.${channelName}`,
};
}
if (className === "LoadImage") {
return {
url: buildViewUrl(imageWidget.value, "input", { channel: "a" }),
channel: "a",
invert: true,
label: "LoadImage.alpha",
};
}
return null;
}
function loadImageElement(url) {
return new Promise((resolve, reject) => {
if (!url) {
resolve(null);
return;
}
const image = new Image();
image.onload = () => resolve(image);
image.onerror = () => reject(new Error(`Failed to load image: ${url}`));
image.src = url;
});
}
function computeDisplaySize(width, height) {
const safeWidth = Math.max(1, Math.round(width));
const safeHeight = Math.max(1, Math.round(height));
const maxEdge = Math.max(safeWidth, safeHeight);
let scale = 1;
if (maxEdge > MAX_PREVIEW_EDGE) {
scale = MAX_PREVIEW_EDGE / maxEdge;
} else if (maxEdge < MIN_PREVIEW_EDGE) {
scale = MIN_PREVIEW_EDGE / maxEdge;
}
return {
width: Math.max(1, Math.round(safeWidth * scale)),
height: Math.max(1, Math.round(safeHeight * scale)),
};
}
function drawHintText(context, lines) {
context.fillStyle = "#171717";
context.fillRect(0, 0, context.canvas.width, context.canvas.height);
context.fillStyle = "#9a9a9a";
context.font = "12px sans-serif";
lines.forEach((line, index) => {
context.fillText(line, 14, 24 + index * 18);
});
}
function drawMaskOverlay(stageContext, overlayContext, overlayCanvas, maskCanvas, overlayAlpha, invertMask) {
if (!maskCanvas.width || !maskCanvas.height) {
return;
}
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
overlayContext.drawImage(maskCanvas, 0, 0, overlayCanvas.width, overlayCanvas.height);
const imageData = overlayContext.getImageData(0, 0, overlayCanvas.width, overlayCanvas.height);
const pixels = imageData.data;
for (let index = 0; index < pixels.length; index += 4) {
let value = pixels[index];
if (invertMask) {
value = 255 - value;
}
pixels[index] = 255;
pixels[index + 1] = 0;
pixels[index + 2] = 0;
pixels[index + 3] = Math.round((value / 255) * overlayAlpha * 255);
}
overlayContext.putImageData(imageData, 0, 0);
stageContext.drawImage(overlayCanvas, 0, 0);
}
function attachMaskEditor(node) {
if (node.__llsMaskDrawEditor || typeof node.addDOMWidget !== "function") {
return node.__llsMaskDrawEditor ?? null;
}
const stateWidget = findWidget(node, "mask_state_json");
const drawModeWidget = findWidget(node, "draw_mode");
const brushSizeWidget = findWidget(node, "brush_size");
const brushSoftnessWidget = findWidget(node, "brush_softness");
const overlayAlphaWidget = findWidget(node, "overlay_alpha");
const invertWidget = findWidget(node, "invert_mask");
hideWidget(stateWidget);
const savedState = parseState(stateWidget?.value);
const container = document.createElement("div");
container.className = "lls-mask-draw-editor";
container.style.display = "flex";
container.style.flexDirection = "column";
container.style.alignItems = "flex-start";
container.style.gap = "8px";
container.style.width = "100%";
container.style.boxSizing = "border-box";
const toolbar = document.createElement("div");
toolbar.style.display = "flex";
toolbar.style.flexWrap = "wrap";
toolbar.style.gap = "6px";
toolbar.style.width = "100%";
const frame = document.createElement("div");
frame.style.display = "inline-flex";
frame.style.border = "1px solid #4e4e4e";
frame.style.borderRadius = "8px";
frame.style.overflow = "hidden";
frame.style.background = "#111";
const stage = document.createElement("canvas");
stage.style.display = "block";
stage.style.cursor = "crosshair";
stage.style.touchAction = "none";
stage.style.userSelect = "none";
frame.appendChild(stage);
const status = document.createElement("div");
status.style.fontSize = "12px";
status.style.lineHeight = "1.45";
status.style.color = "#b8b8b8";
status.style.maxWidth = "420px";
status.style.whiteSpace = "normal";
container.append(toolbar, frame, status);
const domWidget = node.addDOMWidget("lls_mask_editor", "lls_mask_editor", container);
domWidget.serialize = false;
if (domWidget.options) {
domWidget.options.hideOnZoom = false;
}
const stageContext = stage.getContext("2d");
const overlayCanvas = document.createElement("canvas");
const overlayContext = overlayCanvas.getContext("2d", { willReadFrequently: true });
const maskCanvas = document.createElement("canvas");
let maskContext = maskCanvas.getContext("2d", { willReadFrequently: true });
const history = {
undo: [],
redo: [],
};
const state = {
baseImage: null,
localMaskActive: Boolean(savedState.touched && savedState.mask_png_base64),
drawMode: String(savedState.editor.draw_mode || "brush"),
brushSize: clamp(asNumber(savedState.editor.brush_size, 32), 1, 512),
brushSoftness: clamp(asNumber(savedState.editor.brush_softness, 0.5), 0, 1),
overlayAlpha: clamp(asNumber(savedState.editor.overlay_alpha, 0.4), 0, 1),
drawing: false,
dirtyStroke: false,
lastPoint: null,
loadToken: 0,
};
let canPersist = false;
function updateStatus(message) {
status.textContent = message;
}
function resetHistory() {
history.undo.length = 0;
history.redo.length = 0;
}
function fillMaskBlack() {
if (!maskCanvas.width || !maskCanvas.height || !maskContext) {
return;
}
maskContext.save();
maskContext.globalCompositeOperation = "source-over";
maskContext.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
maskContext.fillStyle = "rgb(0, 0, 0)";
maskContext.fillRect(0, 0, maskCanvas.width, maskCanvas.height);
maskContext.restore();
}
function ensureMaskCanvasSize(width, height, options = {}) {
const preserve = options.preserve !== false;
const nextWidth = Math.max(1, Math.round(width));
const nextHeight = Math.max(1, Math.round(height));
if (maskCanvas.width === nextWidth && maskCanvas.height === nextHeight) {
return;
}
let previousCanvas = null;
if (preserve && maskCanvas.width > 0 && maskCanvas.height > 0) {
previousCanvas = document.createElement("canvas");
previousCanvas.width = maskCanvas.width;
previousCanvas.height = maskCanvas.height;
previousCanvas.getContext("2d").drawImage(maskCanvas, 0, 0);
}
maskCanvas.width = nextWidth;
maskCanvas.height = nextHeight;
maskContext = maskCanvas.getContext("2d", { willReadFrequently: true });
fillMaskBlack();
if (previousCanvas) {
maskContext.drawImage(previousCanvas, 0, 0, nextWidth, nextHeight);
}
}
function snapshotMask() {
if (!maskCanvas.width || !maskCanvas.height) {
return "";
}
return maskCanvas.toDataURL("image/png");
}
function persistState() {
if (!canPersist || !stateWidget) {
return;
}
stateWidget.value = JSON.stringify({
version: 1,
mask_png_base64: state.localMaskActive ? dataUrlToBase64(snapshotMask()) : "",
touched: state.localMaskActive,
editor: {
draw_mode: state.drawMode,
brush_size: state.brushSize,
brush_softness: state.brushSoftness,
overlay_alpha: state.overlayAlpha,
},
});
app.graph?.setDirtyCanvas(true, true);
}
function readWidgetValues(options = {}) {
const persist = options.persist !== false;
state.drawMode = String(drawModeWidget?.value ?? state.drawMode || "brush");
state.brushSize = clamp(asNumber(brushSizeWidget?.value, state.brushSize || 32), 1, 512);
state.brushSoftness = clamp(asNumber(brushSoftnessWidget?.value, state.brushSoftness ?? 0.5), 0, 1);
state.overlayAlpha = clamp(asNumber(overlayAlphaWidget?.value, state.overlayAlpha ?? 0.4), 0, 1);
if (persist) {
persistState();
}
}
function updateNodeSize() {
const sourceWidth = state.baseImage?.naturalWidth || maskCanvas.width || 320;
const sourceHeight = state.baseImage?.naturalHeight || maskCanvas.height || 240;
const displaySize = computeDisplaySize(sourceWidth, sourceHeight);
stage.width = displaySize.width;
stage.height = displaySize.height;
stage.style.width = `${displaySize.width}px`;
stage.style.height = `${displaySize.height}px`;
overlayCanvas.width = displaySize.width;
overlayCanvas.height = displaySize.height;
frame.style.width = `${displaySize.width}px`;
container.style.width = `${displaySize.width}px`;
if (typeof node.setSize === "function") {
requestAnimationFrame(() => {
node.setSize([
Math.max(360, displaySize.width + 28),
Math.max(250, displaySize.height + 150),
]);
app.graph?.setDirtyCanvas(true, true);
});
}
}
function renderStage() {
updateNodeSize();
stageContext.clearRect(0, 0, stage.width, stage.height);
if (state.baseImage) {
stageContext.drawImage(state.baseImage, 0, 0, stage.width, stage.height);
} else {
drawHintText(stageContext, state.localMaskActive
? [
"Source preview unavailable.",
"Connect Load Image / Load Image Output",
"to show the edited mask over the image.",
]
: [
"Connect Load Image / Load Image Output",
"to start drawing before execution.",
]);
}
drawMaskOverlay(
stageContext,
overlayContext,
overlayCanvas,
maskCanvas,
state.overlayAlpha,
asBoolean(invertWidget?.value, false),
);
app.graph?.setDirtyCanvas(true, true);
}
function renderSourceMask(image, channel, invert) {
if (!maskCanvas.width || !maskCanvas.height) {
return;
}
const scratchCanvas = document.createElement("canvas");
scratchCanvas.width = maskCanvas.width;
scratchCanvas.height = maskCanvas.height;
const scratchContext = scratchCanvas.getContext("2d", { willReadFrequently: true });
scratchContext.drawImage(image, 0, 0, scratchCanvas.width, scratchCanvas.height);
const imageData = scratchContext.getImageData(0, 0, scratchCanvas.width, scratchCanvas.height);
const pixels = imageData.data;
for (let index = 0; index < pixels.length; index += 4) {
let value = pixels[index];
if (channel === "g") {
value = pixels[index + 1];
} else if (channel === "b") {
value = pixels[index + 2];
} else if (channel === "a") {
value = pixels[index + 3];
}
if (invert) {
value = 255 - value;
}
pixels[index] = value;
pixels[index + 1] = value;
pixels[index + 2] = value;
pixels[index + 3] = 255;
}
scratchContext.putImageData(imageData, 0, 0);
fillMaskBlack();
maskContext.drawImage(scratchCanvas, 0, 0);
}
async function restoreMaskFromDataUrl(dataUrl) {
const image = await loadImageElement(dataUrl);
if (!image) {
return false;
}
ensureMaskCanvasSize(image.naturalWidth || image.width, image.naturalHeight || image.height, { preserve: false });
fillMaskBlack();
maskContext.drawImage(image, 0, 0, maskCanvas.width, maskCanvas.height);
return true;
}
async function restoreSerializedLocalMask(maskBase64) {
if (!maskBase64) {
return false;
}
try {
const restored = await restoreMaskFromDataUrl(`data:image/png;base64,${maskBase64}`);
if (restored) {
resetHistory();
}
return restored;
} catch {
return false;
}
}
function pushUndoSnapshot() {
const snapshot = snapshotMask();
if (!snapshot) {
return;
}
history.undo.push(snapshot);
if (history.undo.length > HISTORY_LIMIT) {
history.undo.shift();
}
history.redo.length = 0;
}
async function restoreHistorySnapshot(snapshot) {
if (!snapshot) {
return false;
}
const restored = await restoreMaskFromDataUrl(snapshot);
renderStage();
return restored;
}
function getMaskPoint(event) {
if (!maskCanvas.width || !maskCanvas.height) {
return null;
}
const bounds = stage.getBoundingClientRect();
if (!bounds.width || !bounds.height) {
return null;
}
return {
x: clamp((event.clientX - bounds.left) * (maskCanvas.width / bounds.width), 0, maskCanvas.width),
y: clamp((event.clientY - bounds.top) * (maskCanvas.height / bounds.height), 0, maskCanvas.height),
};
}
function applyBrushStamp(x, y) {
const radius = Math.max(1, state.brushSize) / 2;
const edge = clamp(1 - state.brushSoftness, 0, 1);
const channelValue = state.drawMode === "erase" ? 0 : 255;
const gradient = maskContext.createRadialGradient(x, y, 0, x, y, radius);
gradient.addColorStop(0, `rgba(${channelValue}, ${channelValue}, ${channelValue}, 1)`);
gradient.addColorStop(edge, `rgba(${channelValue}, ${channelValue}, ${channelValue}, 1)`);
gradient.addColorStop(1, `rgba(${channelValue}, ${channelValue}, ${channelValue}, 0)`);
maskContext.save();
maskContext.globalCompositeOperation = "source-over";
maskContext.fillStyle = gradient;
maskContext.beginPath();
maskContext.arc(x, y, radius, 0, Math.PI * 2);
maskContext.fill();
maskContext.restore();
}
function applyStroke(fromPoint, toPoint) {
if (!toPoint) {
return;
}
if (!fromPoint) {
applyBrushStamp(toPoint.x, toPoint.y);
return;
}
const deltaX = toPoint.x - fromPoint.x;
const deltaY = toPoint.y - fromPoint.y;
const distance = Math.hypot(deltaX, deltaY);
const spacing = Math.max(1, state.brushSize * 0.2);
const steps = Math.max(1, Math.ceil(distance / spacing));
for (let step = 1; step <= steps; step += 1) {
const progress = step / steps;
applyBrushStamp(
fromPoint.x + deltaX * progress,
fromPoint.y + deltaY * progress,
);
}
}
async function reloadFromInputs() {
const requestToken = ++state.loadToken;
const imageSource = resolveImageSource(node);
const maskSource = state.localMaskActive ? null : resolveMaskSource(node);
const baseImagePromise = imageSource?.url
? loadImageElement(imageSource.url).catch(() => null)
: Promise.resolve(null);
const maskImagePromise = maskSource?.url
? loadImageElement(maskSource.url).catch(() => null)
: Promise.resolve(null);
const [baseImage, maskImage] = await Promise.all([baseImagePromise, maskImagePromise]);
if (requestToken !== state.loadToken) {
return;
}
state.baseImage = baseImage;
const targetWidth = baseImage?.naturalWidth || maskCanvas.width || maskImage?.naturalWidth || maskImage?.width || 0;
const targetHeight = baseImage?.naturalHeight || maskCanvas.height || maskImage?.naturalHeight || maskImage?.height || 0;
if (targetWidth && targetHeight) {
if (state.localMaskActive) {
ensureMaskCanvasSize(targetWidth, targetHeight, { preserve: true });
} else {
resetHistory();
ensureMaskCanvasSize(targetWidth, targetHeight, { preserve: false });
if (maskImage) {
renderSourceMask(maskImage, maskSource.channel, maskSource.invert);
}
}
}
if (!imageSource) {
updateStatus(state.localMaskActive
? "Local mask restored. Connect Load Image or Load Image Output for source preview."
: "Connect Load Image or Load Image Output to paint before execution.");
} else if (!baseImage) {
updateStatus("Source preview could not be loaded from the upstream file-backed node.");
} else if (state.localMaskActive) {
updateStatus("Editing the local mask raster. input_mask is ignored until this local mask is replaced.");
} else if (maskSource && maskImage) {
updateStatus("input_mask loaded as the editable base layer.");
} else if (maskSource && !maskImage) {
updateStatus("Source image loaded. input_mask preview is unavailable for this upstream node.");
} else {
updateStatus("Source image loaded. Drawing starts from an empty mask.");
}
renderStage();
}
function createButton(label, handler) {
const button = document.createElement("button");
button.type = "button";
button.textContent = label;
button.style.border = "1px solid #5d5d5d";
button.style.borderRadius = "6px";
button.style.background = "#202020";
button.style.color = "#f0f0f0";
button.style.padding = "4px 10px";
button.style.cursor = "pointer";
button.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
handler(event);
});
return button;
}
toolbar.append(
createButton("Clear", () => {
if (!maskCanvas.width || !maskCanvas.height) {
return;
}
pushUndoSnapshot();
fillMaskBlack();
state.localMaskActive = true;
persistState();
updateStatus("Local mask cleared to black.");
renderStage();
}),
createButton("Undo", async () => {
if (!history.undo.length) {
return;
}
const previous = history.undo.pop();
const current = snapshotMask();
if (current) {
history.redo.push(current);
}
if (await restoreHistorySnapshot(previous)) {
state.localMaskActive = true;
persistState();
updateStatus("Restored the previous local mask state.");
}
}),
createButton("Redo", async () => {
if (!history.redo.length) {
return;
}
const next = history.redo.pop();
const current = snapshotMask();
if (current) {
history.undo.push(current);
}
if (await restoreHistorySnapshot(next)) {
state.localMaskActive = true;
persistState();
updateStatus("Reapplied the next local mask state.");
}
}),
createButton("Invert", () => {
if (!invertWidget) {
return;
}
invertWidget.value = !Boolean(invertWidget.value);
invertWidget.callback?.(invertWidget.value);
renderStage();
}),
);
stage.addEventListener("pointerdown", (event) => {
event.preventDefault();
event.stopPropagation();
const point = getMaskPoint(event);
if (!point) {
updateStatus("Source image is not ready. Connect Load Image or Load Image Output first.");
renderStage();
return;
}
stage.setPointerCapture?.(event.pointerId);
pushUndoSnapshot();
state.drawing = true;
state.dirtyStroke = true;
state.localMaskActive = true;
applyStroke(null, point);
state.lastPoint = point;
renderStage();
});
stage.addEventListener("pointermove", (event) => {
if (!state.drawing) {
return;
}
event.preventDefault();
event.stopPropagation();
const point = getMaskPoint(event);
if (!point) {
return;
}
applyStroke(state.lastPoint, point);
state.lastPoint = point;
renderStage();
});
function endStroke(event) {
if (!state.drawing) {
return;
}
event?.preventDefault?.();
event?.stopPropagation?.();
state.drawing = false;
state.lastPoint = null;
if (state.dirtyStroke) {
persistState();
state.dirtyStroke = false;
updateStatus("Local mask updated.");
}
renderStage();
}
stage.addEventListener("pointerup", endStroke);
stage.addEventListener("pointercancel", endStroke);
stage.addEventListener("lostpointercapture", endStroke);
chainWidgetCallback(drawModeWidget, () => {
readWidgetValues({ persist: true });
renderStage();
});
chainWidgetCallback(brushSizeWidget, () => {
readWidgetValues({ persist: true });
renderStage();
});
chainWidgetCallback(brushSoftnessWidget, () => {
readWidgetValues({ persist: true });
renderStage();
});
chainWidgetCallback(overlayAlphaWidget, () => {
readWidgetValues({ persist: true });
renderStage();
});
chainWidgetCallback(invertWidget, () => {
renderStage();
});
async function initializeFromState(syncSerializedState) {
const parsedState = syncSerializedState ? parseState(stateWidget?.value) : savedState;
readWidgetValues({ persist: false });
if (parsedState.touched && parsedState.mask_png_base64) {
state.localMaskActive = await restoreSerializedLocalMask(parsedState.mask_png_base64);
} else if (!state.localMaskActive) {
state.localMaskActive = false;
}
await reloadFromInputs();
canPersist = true;
renderStage();
}
const editorApi = {
handleGraphConfigured() {
void initializeFromState(true);
},
handleConnectionsChange() {
void reloadFromInputs();
},
};
node.__llsMaskDrawEditor = editorApi;
updateStatus("Connect Load Image or Load Image Output to start drawing before execution.");
renderStage();
void initializeFromState(false);
return editorApi;
}
app.registerExtension({
name: EXTENSION_NAME,
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== TARGET_NODE_CLASS && nodeData.display_name !== TARGET_NODE_DISPLAY_NAME) {
return;
}
const previousOnNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function onMaskDrawNodeCreated() {
const result = previousOnNodeCreated?.apply(this, arguments);
attachMaskEditor(this);
return result;
};
const previousOnGraphConfigured = nodeType.prototype.onGraphConfigured;
nodeType.prototype.onGraphConfigured = function onMaskDrawGraphConfigured() {
const result = previousOnGraphConfigured?.apply(this, arguments);
attachMaskEditor(this)?.handleGraphConfigured?.();
return result;
};
const previousOnConnectionsChange = nodeType.prototype.onConnectionsChange;
nodeType.prototype.onConnectionsChange = function onMaskDrawConnectionsChange() {
const result = previousOnConnectionsChange?.apply(this, arguments);
attachMaskEditor(this)?.handleConnectionsChange?.();
return result;
};
},
});