implement strict image/mask input separation

Enhanced LayerForge input handling to strictly separate image and mask loading based on connection type. Images now only load when allowImage=true and masks only when allowMask=true, preventing unintended cross-loading between input types.
This commit is contained in:
Dariusz L
2025-08-09 01:44:31 +02:00
parent 285ad035b2
commit b21d6e3502
6 changed files with 244 additions and 118 deletions

View File

@@ -376,8 +376,8 @@ export class Canvas {
const handleExecutionStart = () => {
// Check for input data when execution starts, but don't reset the flag
log.debug('Execution started, checking for input data...');
// Don't reset inputDataLoaded here - we want to remember if we already loaded this input
this.canvasIO.checkForInputData();
// On start, only allow images; mask should load on mask-connect or after execution completes
this.canvasIO.checkForInputData({ allowImage: true, allowMask: false, reason: 'execution_start' });
if (getAutoRefreshValue()) {
lastExecutionStartTime = Date.now();
// Store a snapshot of the context for the upcoming batch
@@ -402,7 +402,7 @@ export class Canvas {
const handleExecutionSuccess = async () => {
// Always check for input data after execution completes
log.debug('Execution success, checking for input data...');
await this.canvasIO.checkForInputData();
await this.canvasIO.checkForInputData({ allowImage: true, allowMask: true, reason: 'execution_success' });
if (getAutoRefreshValue()) {
log.info('Auto-refresh triggered, importing latest images.');
if (!this.pendingBatchContext) {

View File

@@ -363,16 +363,19 @@ export class CanvasIO {
return this.scheduleDataCheck();
}
}
async checkForInputData() {
async checkForInputData(options) {
try {
const nodeId = this.canvas.node.id;
log.info(`Checking for input data for node ${nodeId}...`);
const allowImage = options?.allowImage ?? true;
const allowMask = options?.allowMask ?? true;
const reason = options?.reason ?? 'unspecified';
log.info(`Checking for input data for node ${nodeId}... opts: image=${allowImage}, mask=${allowMask}, reason=${reason}`);
// Track loaded links separately for image and mask
let imageLoaded = false;
let maskLoaded = false;
let imageChanged = false;
// First, try to get data from connected node's output if available
if (this.canvas.node.inputs && this.canvas.node.inputs[0] && this.canvas.node.inputs[0].link) {
// First, try to get data from connected node's output if available (IMAGES)
if (allowImage && this.canvas.node.inputs && this.canvas.node.inputs[0] && this.canvas.node.inputs[0].link) {
const linkId = this.canvas.node.inputs[0].link;
const graph = this.canvas.node.graph;
// Always check if images have changed first
@@ -422,10 +425,11 @@ export class CanvasIO {
this.canvas.inputDataLoaded = false;
log.info("Resetting inputDataLoaded flag due to image change");
}
if (graph) {
const link = graph.links[linkId];
if (link) {
const sourceNode = graph.getNodeById(link.origin_id);
if (this.canvas.node.graph) {
const graph2 = this.canvas.node.graph;
const link2 = graph2.links[linkId];
if (link2) {
const sourceNode = graph2.getNodeById(link2.origin_id);
if (sourceNode && sourceNode.imgs && sourceNode.imgs.length > 0) {
// The connected node has images in its output - handle multiple images (batch)
log.info(`Found ${sourceNode.imgs.length} image(s) in connected node's output, loading all`);
@@ -458,8 +462,8 @@ export class CanvasIO {
}
}
}
// Check for mask input separately
if (this.canvas.node.inputs && this.canvas.node.inputs[1] && this.canvas.node.inputs[1].link) {
// Check for mask input separately (from nodeOutputs) ONLY when allowed
if (allowMask && this.canvas.node.inputs && this.canvas.node.inputs[1] && this.canvas.node.inputs[1].link) {
const maskLinkId = this.canvas.node.inputs[1].link;
// Check if we already loaded this mask link
if (this.canvas.lastLoadedMaskLinkId === maskLinkId) {
@@ -591,6 +595,7 @@ export class CanvasIO {
// Apply to MaskTool (centers internally)
if (this.canvas.maskTool) {
this.canvas.maskTool.setMask(finalMaskImg, true);
this.canvas.maskAppliedFromInput = true;
this.canvas.canvasState.saveMaskState();
this.canvas.render();
// Mark this mask link as loaded to avoid re-applying
@@ -605,15 +610,30 @@ export class CanvasIO {
}
else {
// nodeOutputs exist but don't have tensor data yet (need workflow execution)
log.info(`Mask node ${graph.links[maskLinkId]?.origin_id} found but has no tensor data yet. Mask will be applied automatically after workflow execution.`);
log.info(`Mask node ${this.canvas.node.graph?.links[maskLinkId]?.origin_id} found but has no tensor data yet. Mask will be applied automatically after workflow execution.`);
// Don't retry - data won't be available until workflow runs
}
}
}
// Even if images are already loaded from connected nodes, still check backend for fresh execution data.
// We'll dedupe by comparing backend payload hash with lastLoadedImageSrc.
const nodeInputs = this.canvas.node.inputs;
// Check backend for input data
// Only check backend if we have actual inputs connected
const hasImageInput = this.canvas.node.inputs && this.canvas.node.inputs[0] && this.canvas.node.inputs[0].link;
const hasMaskInput = this.canvas.node.inputs && this.canvas.node.inputs[1] && this.canvas.node.inputs[1].link;
// If mask input is disconnected, clear any currently applied mask to ensure full separation
if (!hasMaskInput) {
if (this.canvas.maskTool) {
this.canvas.maskTool.clear();
this.canvas.render();
}
this.canvas.maskAppliedFromInput = false;
this.canvas.lastLoadedMaskLinkId = undefined;
log.info("Mask input disconnected - cleared mask to enforce separation from input_image");
}
if (!hasImageInput && !hasMaskInput) {
log.debug("No inputs connected, skipping backend check");
this.canvas.inputDataLoaded = true;
return;
}
// Check backend for input data only if we have connected inputs
const response = await fetch(`/layerforge/get_input_data/${nodeId}`);
const result = await response.json();
if (result.success && result.has_input) {
@@ -625,11 +645,8 @@ export class CanvasIO {
else if (result.data?.input_image) {
backendBatchHash = result.data.input_image;
}
// Check mask separately - don't skip if only images are unchanged
let shouldCheckMask = false;
if (this.canvas.node.inputs && this.canvas.node.inputs[1] && this.canvas.node.inputs[1].link) {
shouldCheckMask = true;
}
// Check mask separately - don't skip if only images are unchanged AND mask is actually connected AND allowed
const shouldCheckMask = hasMaskInput && allowMask;
if (backendBatchHash && this.canvas.lastLoadedImageSrc === backendBatchHash && !shouldCheckMask) {
log.debug("Backend input data unchanged and no mask to check, skipping reload");
this.canvas.inputDataLoaded = true;
@@ -640,7 +657,7 @@ export class CanvasIO {
imageLoaded = true; // Mark images as already loaded to skip reloading them
}
// Check if we already loaded image data (by checking the current link)
if (!imageLoaded && this.canvas.node.inputs && this.canvas.node.inputs[0] && this.canvas.node.inputs[0].link) {
if (allowImage && !imageLoaded && this.canvas.node.inputs && this.canvas.node.inputs[0] && this.canvas.node.inputs[0].link) {
const currentLinkId = this.canvas.node.inputs[0].link;
if (this.canvas.lastLoadedLinkId !== currentLinkId) {
// Mark this link as loaded
@@ -648,15 +665,29 @@ export class CanvasIO {
imageLoaded = false; // Will load from backend
}
}
// Always check for mask data from backend when there's a mask input
// Don't rely on maskLoaded flag from nodeOutputs check
if (this.canvas.node.inputs && this.canvas.node.inputs[1] && this.canvas.node.inputs[1].link) {
// Check for mask data from backend ONLY when mask input is actually connected AND allowed
// Only reset if the mask link actually changed
if (allowMask && hasMaskInput && this.canvas.node.inputs && this.canvas.node.inputs[1]) {
const currentMaskLinkId = this.canvas.node.inputs[1].link;
// Always reset mask loaded flag and clear lastLoadedMaskLinkId to force fresh load
maskLoaded = false;
// Clear the stored mask link to force reload from backend
this.canvas.lastLoadedMaskLinkId = undefined;
log.debug(`Mask input detected, will force check backend for mask data`);
// Only reset if this is a different mask link than what we loaded before
if (this.canvas.lastLoadedMaskLinkId !== currentMaskLinkId) {
maskLoaded = false;
log.debug(`New mask input detected (${currentMaskLinkId}), will check backend for mask data`);
}
else {
log.debug(`Same mask input (${currentMaskLinkId}), mask already loaded`);
maskLoaded = true;
}
}
else {
// No mask input connected, or mask loading not allowed right now
maskLoaded = true; // Mark as loaded to skip mask processing
if (!allowMask) {
log.debug("Mask loading is currently disabled by caller, skipping mask check");
}
else {
log.debug("No mask input connected, skipping mask check");
}
}
log.info("Input data found from backend, adding to canvas");
const inputData = result.data;
@@ -679,8 +710,8 @@ export class CanvasIO {
const widgets = this.canvas.node.widgets;
const fitOnAddWidget = widgets ? widgets.find((w) => w.name === "fit_on_add") : null;
const addMode = (fitOnAddWidget && fitOnAddWidget.value) ? 'fit' : 'center';
// Load input image(s) if provided and not already loaded
if (!imageLoaded) {
// Load input image(s) only if image input is actually connected, not already loaded, and allowed
if (allowImage && !imageLoaded && hasImageInput) {
if (inputData.input_images_batch) {
// Handle batch of images
const batch = inputData.input_images_batch;
@@ -719,8 +750,11 @@ export class CanvasIO {
log.debug("No input image data from backend");
}
}
// Handle mask separately (not tied to layer) if not already loaded
if (!maskLoaded && inputData.input_mask) {
else if (!hasImageInput && (inputData.input_images_batch || inputData.input_image)) {
log.debug("Backend has image data but no image input connected, skipping image load");
}
// Handle mask separately only if mask input is actually connected, allowed, and not already loaded
if (allowMask && !maskLoaded && hasMaskInput && inputData.input_mask) {
log.info("Processing input mask");
// Load mask image
const maskImg = new Image();
@@ -730,8 +764,8 @@ export class CanvasIO {
maskImg.src = inputData.input_mask;
});
// Determine if we should fit the mask or use it at original size
const fitOnAddWidget = this.canvas.node.widgets.find((w) => w.name === "fit_on_add");
const shouldFit = fitOnAddWidget && fitOnAddWidget.value;
const fitOnAddWidget2 = this.canvas.node.widgets.find((w) => w.name === "fit_on_add");
const shouldFit = fitOnAddWidget2 && fitOnAddWidget2.value;
if (shouldFit && this.canvas.maskTool) {
// Scale mask to fit output area if fit_on_add is enabled
const bounds = this.canvas.outputAreaBounds;
@@ -758,10 +792,17 @@ export class CanvasIO {
// Apply mask at original size
this.canvas.maskTool.setMask(maskImg, true);
}
this.canvas.maskAppliedFromInput = true;
// Save the mask state
this.canvas.canvasState.saveMaskState();
log.info("Applied input mask to mask tool" + (shouldFit ? " (fitted to output area)" : " (original size)"));
}
else if (!hasMaskInput && inputData.input_mask) {
log.debug("Backend has mask data but no mask input connected, skipping mask load");
}
else if (!allowMask && inputData.input_mask) {
log.debug("Mask input data present in backend but mask loading is disabled by caller; skipping");
}
}
else {
log.debug("No input data from backend");

View File

@@ -1112,7 +1112,8 @@ app.registerExtension({
log.info("Input image already connected on node creation, checking for data...");
if (canvasWidget.canvas && canvasWidget.canvas.canvasIO) {
canvasWidget.canvas.inputDataLoaded = false;
canvasWidget.canvas.canvasIO.checkForInputData();
// Only allow images on init; mask should load only on mask connect or execution
canvasWidget.canvas.canvasIO.checkForInputData({ allowImage: true, allowMask: false, reason: "init_image_connected" });
}
}
}
@@ -1140,7 +1141,11 @@ app.registerExtension({
log.info("Canvas now ready, checking for input data...");
if (connected) {
retryCanvas.inputDataLoaded = false;
retryCanvas.canvasIO.checkForInputData();
// Respect which input triggered the connection:
const opts = (index === 1)
? { allowImage: false, allowMask: true, reason: "mask_connect" }
: { allowImage: true, allowMask: false, reason: "image_connect" };
retryCanvas.canvasIO.checkForInputData(opts);
}
}
else if (retryCount < retryDelays.length) {
@@ -1165,10 +1170,21 @@ app.registerExtension({
canvas.lastLoadedLinkId = undefined;
// Mark that we have a pending input connection
canvas.hasPendingInputConnection = true;
// If mask input is not connected and a mask was auto-applied from input_mask before, clear it now
if (!(this.inputs && this.inputs[1] && this.inputs[1].link)) {
if (canvas.maskAppliedFromInput && canvas.maskTool) {
canvas.maskTool.clear();
canvas.render();
canvas.maskAppliedFromInput = false;
canvas.lastLoadedMaskLinkId = undefined;
log.info("Cleared auto-applied mask because input_image connected without input_mask");
}
}
// Check for data immediately when connected
setTimeout(() => {
log.info("Checking for input data after connection...");
canvas.canvasIO.checkForInputData();
// Only load images here; masks should not auto-load on image connect
canvas.canvasIO.checkForInputData({ allowImage: true, allowMask: false, reason: "image_connect" });
}, 500);
}
else {
@@ -1189,12 +1205,19 @@ app.registerExtension({
// Check for data immediately when connected
setTimeout(() => {
log.info("Checking for input data after mask connection...");
canvas.canvasIO.checkForInputData();
// Only load mask here; images are handled by image connect or execution
canvas.canvasIO.checkForInputData({ allowImage: false, allowMask: true, reason: "mask_connect" });
}, 500);
}
else {
log.info("Input mask disconnected");
canvas.hasPendingMaskConnection = false;
// If the current mask came from input_mask, clear it to avoid affecting images when mask is not connected
if (canvas.maskAppliedFromInput && canvas.maskTool) {
canvas.maskAppliedFromInput = false;
canvas.lastLoadedMaskLinkId = undefined;
log.info("Cleared auto-applied mask due to mask input disconnection");
}
}
}
}
@@ -1206,8 +1229,8 @@ app.registerExtension({
const canvas = this.canvasWidget?.canvas || this.canvasWidget;
if (canvas && canvas.canvasIO) {
// Don't reset inputDataLoaded - just check for new data
// The checkForInputData method will handle checking if we already loaded this image
canvas.canvasIO.checkForInputData();
// On execution we allow both image and mask to load
canvas.canvasIO.checkForInputData({ allowImage: true, allowMask: true, reason: "execution" });
}
// Call original if it exists
if (originalOnExecuted) {

View File

@@ -492,8 +492,8 @@ export class Canvas {
const handleExecutionStart = () => {
// Check for input data when execution starts, but don't reset the flag
log.debug('Execution started, checking for input data...');
// Don't reset inputDataLoaded here - we want to remember if we already loaded this input
this.canvasIO.checkForInputData();
// On start, only allow images; mask should load on mask-connect or after execution completes
this.canvasIO.checkForInputData({ allowImage: true, allowMask: false, reason: 'execution_start' });
if (getAutoRefreshValue()) {
lastExecutionStartTime = Date.now();
@@ -520,7 +520,7 @@ export class Canvas {
const handleExecutionSuccess = async () => {
// Always check for input data after execution completes
log.debug('Execution success, checking for input data...');
await this.canvasIO.checkForInputData();
await this.canvasIO.checkForInputData({ allowImage: true, allowMask: true, reason: 'execution_success' });
if (getAutoRefreshValue()) {
log.info('Auto-refresh triggered, importing latest images.');

View File

@@ -428,18 +428,21 @@ export class CanvasIO {
}
}
async checkForInputData(): Promise<void> {
async checkForInputData(options?: { allowImage?: boolean; allowMask?: boolean; reason?: string }): Promise<void> {
try {
const nodeId = this.canvas.node.id;
log.info(`Checking for input data for node ${nodeId}...`);
const allowImage = options?.allowImage ?? true;
const allowMask = options?.allowMask ?? true;
const reason = options?.reason ?? 'unspecified';
log.info(`Checking for input data for node ${nodeId}... opts: image=${allowImage}, mask=${allowMask}, reason=${reason}`);
// Track loaded links separately for image and mask
let imageLoaded = false;
let maskLoaded = false;
let imageChanged = false;
// First, try to get data from connected node's output if available
if (this.canvas.node.inputs && this.canvas.node.inputs[0] && this.canvas.node.inputs[0].link) {
// First, try to get data from connected node's output if available (IMAGES)
if (allowImage && this.canvas.node.inputs && this.canvas.node.inputs[0] && this.canvas.node.inputs[0].link) {
const linkId = this.canvas.node.inputs[0].link;
const graph = (this.canvas.node as any).graph;
@@ -491,10 +494,11 @@ export class CanvasIO {
log.info("Resetting inputDataLoaded flag due to image change");
}
if (graph) {
const link = graph.links[linkId];
if (link) {
const sourceNode = graph.getNodeById(link.origin_id);
if ((this.canvas.node as any).graph) {
const graph2 = (this.canvas.node as any).graph;
const link2 = graph2.links[linkId];
if (link2) {
const sourceNode = graph2.getNodeById(link2.origin_id);
if (sourceNode && sourceNode.imgs && sourceNode.imgs.length > 0) {
// The connected node has images in its output - handle multiple images (batch)
log.info(`Found ${sourceNode.imgs.length} image(s) in connected node's output, loading all`);
@@ -506,26 +510,26 @@ export class CanvasIO {
this.canvas.lastLoadedLinkId = linkId;
this.canvas.lastLoadedImageSrc = batchImageSrcs;
// Don't clear layers - just add new ones
if (imageChanged) {
log.info("Image change detected, will add new layers");
}
// Don't clear layers - just add new ones
if (imageChanged) {
log.info("Image change detected, will add new layers");
}
// Determine add mode
const fitOnAddWidget = this.canvas.node.widgets.find((w) => w.name === "fit_on_add");
const addMode = (fitOnAddWidget && fitOnAddWidget.value) ? 'fit' : 'center';
// Determine add mode
const fitOnAddWidget = this.canvas.node.widgets.find((w) => w.name === "fit_on_add");
const addMode = (fitOnAddWidget && fitOnAddWidget.value) ? 'fit' : 'center';
// Add all images from the batch as separate layers
for (let i = 0; i < sourceNode.imgs.length; i++) {
const img = sourceNode.imgs[i];
await this.canvas.canvasLayers.addLayerWithImage(
img,
{ name: `Batch Image ${i + 1}` }, // Give each layer a unique name
addMode,
this.canvas.outputAreaBounds
);
log.debug(`Added batch image ${i + 1}/${sourceNode.imgs.length} to canvas`);
}
// Add all images from the batch as separate layers
for (let i = 0; i < sourceNode.imgs.length; i++) {
const img = sourceNode.imgs[i];
await this.canvas.canvasLayers.addLayerWithImage(
img,
{ name: `Batch Image ${i + 1}` }, // Give each layer a unique name
addMode,
this.canvas.outputAreaBounds
);
log.debug(`Added batch image ${i + 1}/${sourceNode.imgs.length} to canvas`);
}
this.canvas.inputDataLoaded = true;
imageLoaded = true;
@@ -538,8 +542,8 @@ export class CanvasIO {
}
}
// Check for mask input separately
if (this.canvas.node.inputs && this.canvas.node.inputs[1] && this.canvas.node.inputs[1].link) {
// Check for mask input separately (from nodeOutputs) ONLY when allowed
if (allowMask && this.canvas.node.inputs && this.canvas.node.inputs[1] && this.canvas.node.inputs[1].link) {
const maskLinkId = this.canvas.node.inputs[1].link;
// Check if we already loaded this mask link
@@ -678,7 +682,8 @@ export class CanvasIO {
// Apply to MaskTool (centers internally)
if (this.canvas.maskTool) {
this.canvas.maskTool.setMask(finalMaskImg, true);
this.canvas.maskTool.setMask(finalMaskImg, true);
(this.canvas as any).maskAppliedFromInput = true;
this.canvas.canvasState.saveMaskState();
this.canvas.render();
// Mark this mask link as loaded to avoid re-applying
@@ -691,17 +696,34 @@ export class CanvasIO {
}
} else {
// nodeOutputs exist but don't have tensor data yet (need workflow execution)
log.info(`Mask node ${graph.links[maskLinkId]?.origin_id} found but has no tensor data yet. Mask will be applied automatically after workflow execution.`);
log.info(`Mask node ${(this.canvas.node as any).graph?.links[maskLinkId]?.origin_id} found but has no tensor data yet. Mask will be applied automatically after workflow execution.`);
// Don't retry - data won't be available until workflow runs
}
}
}
// Even if images are already loaded from connected nodes, still check backend for fresh execution data.
// We'll dedupe by comparing backend payload hash with lastLoadedImageSrc.
const nodeInputs = this.canvas.node.inputs;
// Only check backend if we have actual inputs connected
const hasImageInput = this.canvas.node.inputs && this.canvas.node.inputs[0] && this.canvas.node.inputs[0].link;
const hasMaskInput = this.canvas.node.inputs && this.canvas.node.inputs[1] && this.canvas.node.inputs[1].link;
// Check backend for input data
// If mask input is disconnected, clear any currently applied mask to ensure full separation
if (!hasMaskInput) {
if (this.canvas.maskTool) {
this.canvas.maskTool.clear();
this.canvas.render();
}
(this.canvas as any).maskAppliedFromInput = false;
this.canvas.lastLoadedMaskLinkId = undefined;
log.info("Mask input disconnected - cleared mask to enforce separation from input_image");
}
if (!hasImageInput && !hasMaskInput) {
log.debug("No inputs connected, skipping backend check");
this.canvas.inputDataLoaded = true;
return;
}
// Check backend for input data only if we have connected inputs
const response = await fetch(`/layerforge/get_input_data/${nodeId}`);
const result = await response.json();
@@ -713,11 +735,8 @@ export class CanvasIO {
} else if (result.data?.input_image) {
backendBatchHash = result.data.input_image;
}
// Check mask separately - don't skip if only images are unchanged
let shouldCheckMask = false;
if (this.canvas.node.inputs && this.canvas.node.inputs[1] && this.canvas.node.inputs[1].link) {
shouldCheckMask = true;
}
// Check mask separately - don't skip if only images are unchanged AND mask is actually connected AND allowed
const shouldCheckMask = hasMaskInput && allowMask;
if (backendBatchHash && this.canvas.lastLoadedImageSrc === backendBatchHash && !shouldCheckMask) {
log.debug("Backend input data unchanged and no mask to check, skipping reload");
@@ -729,7 +748,7 @@ export class CanvasIO {
}
// Check if we already loaded image data (by checking the current link)
if (!imageLoaded && this.canvas.node.inputs && this.canvas.node.inputs[0] && this.canvas.node.inputs[0].link) {
if (allowImage && !imageLoaded && this.canvas.node.inputs && this.canvas.node.inputs[0] && this.canvas.node.inputs[0].link) {
const currentLinkId = this.canvas.node.inputs[0].link;
if (this.canvas.lastLoadedLinkId !== currentLinkId) {
// Mark this link as loaded
@@ -738,15 +757,26 @@ export class CanvasIO {
}
}
// Always check for mask data from backend when there's a mask input
// Don't rely on maskLoaded flag from nodeOutputs check
if (this.canvas.node.inputs && this.canvas.node.inputs[1] && this.canvas.node.inputs[1].link) {
// Check for mask data from backend ONLY when mask input is actually connected AND allowed
// Only reset if the mask link actually changed
if (allowMask && hasMaskInput && this.canvas.node.inputs && this.canvas.node.inputs[1]) {
const currentMaskLinkId = this.canvas.node.inputs[1].link;
// Always reset mask loaded flag and clear lastLoadedMaskLinkId to force fresh load
maskLoaded = false;
// Clear the stored mask link to force reload from backend
this.canvas.lastLoadedMaskLinkId = undefined;
log.debug(`Mask input detected, will force check backend for mask data`);
// Only reset if this is a different mask link than what we loaded before
if (this.canvas.lastLoadedMaskLinkId !== currentMaskLinkId) {
maskLoaded = false;
log.debug(`New mask input detected (${currentMaskLinkId}), will check backend for mask data`);
} else {
log.debug(`Same mask input (${currentMaskLinkId}), mask already loaded`);
maskLoaded = true;
}
} else {
// No mask input connected, or mask loading not allowed right now
maskLoaded = true; // Mark as loaded to skip mask processing
if (!allowMask) {
log.debug("Mask loading is currently disabled by caller, skipping mask check");
} else {
log.debug("No mask input connected, skipping mask check");
}
}
log.info("Input data found from backend, adding to canvas");
@@ -774,8 +804,8 @@ export class CanvasIO {
const fitOnAddWidget = widgets ? widgets.find((w: any) => w.name === "fit_on_add") : null;
const addMode = (fitOnAddWidget && fitOnAddWidget.value) ? 'fit' : 'center';
// Load input image(s) if provided and not already loaded
if (!imageLoaded) {
// Load input image(s) only if image input is actually connected, not already loaded, and allowed
if (allowImage && !imageLoaded && hasImageInput) {
if (inputData.input_images_batch) {
// Handle batch of images
const batch = inputData.input_images_batch;
@@ -828,10 +858,12 @@ export class CanvasIO {
} else {
log.debug("No input image data from backend");
}
} else if (!hasImageInput && (inputData.input_images_batch || inputData.input_image)) {
log.debug("Backend has image data but no image input connected, skipping image load");
}
// Handle mask separately (not tied to layer) if not already loaded
if (!maskLoaded && inputData.input_mask) {
// Handle mask separately only if mask input is actually connected, allowed, and not already loaded
if (allowMask && !maskLoaded && hasMaskInput && inputData.input_mask) {
log.info("Processing input mask");
// Load mask image
@@ -843,8 +875,8 @@ export class CanvasIO {
});
// Determine if we should fit the mask or use it at original size
const fitOnAddWidget = this.canvas.node.widgets.find((w) => w.name === "fit_on_add");
const shouldFit = fitOnAddWidget && fitOnAddWidget.value;
const fitOnAddWidget2 = this.canvas.node.widgets.find((w) => w.name === "fit_on_add");
const shouldFit = fitOnAddWidget2 && fitOnAddWidget2.value;
if (shouldFit && this.canvas.maskTool) {
// Scale mask to fit output area if fit_on_add is enabled
@@ -883,10 +915,15 @@ export class CanvasIO {
this.canvas.maskTool.setMask(maskImg, true);
}
(this.canvas as any).maskAppliedFromInput = true;
// Save the mask state
this.canvas.canvasState.saveMaskState();
this.canvas.canvasState.saveMaskState()
log.info("Applied input mask to mask tool" + (shouldFit ? " (fitted to output area)" : " (original size)"));
} else if (!hasMaskInput && inputData.input_mask) {
log.debug("Backend has mask data but no mask input connected, skipping mask load");
} else if (!allowMask && inputData.input_mask) {
log.debug("Mask input data present in backend but mask loading is disabled by caller; skipping");
}
} else {
log.debug("No input data from backend");

View File

@@ -1268,16 +1268,17 @@ app.registerExtension({
// Check if there are already connected inputs
setTimeout(() => {
if (this.inputs && this.inputs.length > 0) {
// Check if input_image (index 0) is connected
if (this.inputs[0] && this.inputs[0].link) {
log.info("Input image already connected on node creation, checking for data...");
if (canvasWidget.canvas && canvasWidget.canvas.canvasIO) {
canvasWidget.canvas.inputDataLoaded = false;
canvasWidget.canvas.canvasIO.checkForInputData();
if (this.inputs && this.inputs.length > 0) {
// Check if input_image (index 0) is connected
if (this.inputs[0] && this.inputs[0].link) {
log.info("Input image already connected on node creation, checking for data...");
if (canvasWidget.canvas && canvasWidget.canvas.canvasIO) {
canvasWidget.canvas.inputDataLoaded = false;
// Only allow images on init; mask should load only on mask connect or execution
canvasWidget.canvas.canvasIO.checkForInputData({ allowImage: true, allowMask: false, reason: "init_image_connected" });
}
}
}
}
if (this.setDirtyCanvas) {
this.setDirtyCanvas(true, true);
}
@@ -1306,7 +1307,11 @@ app.registerExtension({
log.info("Canvas now ready, checking for input data...");
if (connected) {
retryCanvas.inputDataLoaded = false;
retryCanvas.canvasIO.checkForInputData();
// Respect which input triggered the connection:
const opts = (index === 1)
? { allowImage: false, allowMask: true, reason: "mask_connect" }
: { allowImage: true, allowMask: false, reason: "image_connect" };
retryCanvas.canvasIO.checkForInputData(opts);
}
} else if (retryCount < retryDelays.length) {
log.warn(`Canvas still not ready, retry ${retryCount + 1}/${retryDelays.length}...`);
@@ -1331,10 +1336,23 @@ app.registerExtension({
canvas.lastLoadedLinkId = undefined;
// Mark that we have a pending input connection
canvas.hasPendingInputConnection = true;
// If mask input is not connected and a mask was auto-applied from input_mask before, clear it now
if (!(this.inputs && this.inputs[1] && this.inputs[1].link)) {
if ((canvas as any).maskAppliedFromInput && canvas.maskTool) {
canvas.maskTool.clear();
canvas.render();
(canvas as any).maskAppliedFromInput = false;
canvas.lastLoadedMaskLinkId = undefined;
log.info("Cleared auto-applied mask because input_image connected without input_mask");
}
}
// Check for data immediately when connected
setTimeout(() => {
log.info("Checking for input data after connection...");
canvas.canvasIO.checkForInputData();
// Only load images here; masks should not auto-load on image connect
canvas.canvasIO.checkForInputData({ allowImage: true, allowMask: false, reason: "image_connect" });
}, 500);
} else {
log.info("Input image disconnected");
@@ -1355,11 +1373,18 @@ app.registerExtension({
// Check for data immediately when connected
setTimeout(() => {
log.info("Checking for input data after mask connection...");
canvas.canvasIO.checkForInputData();
// Only load mask here; images are handled by image connect or execution
canvas.canvasIO.checkForInputData({ allowImage: false, allowMask: true, reason: "mask_connect" });
}, 500);
} else {
log.info("Input mask disconnected");
canvas.hasPendingMaskConnection = false;
// If the current mask came from input_mask, clear it to avoid affecting images when mask is not connected
if ((canvas as any).maskAppliedFromInput && canvas.maskTool) {
(canvas as any).maskAppliedFromInput = false;
canvas.lastLoadedMaskLinkId = undefined;
log.info("Cleared auto-applied mask due to mask input disconnection");
}
}
}
}
@@ -1373,8 +1398,8 @@ app.registerExtension({
const canvas = (this as any).canvasWidget?.canvas || (this as any).canvasWidget;
if (canvas && canvas.canvasIO) {
// Don't reset inputDataLoaded - just check for new data
// The checkForInputData method will handle checking if we already loaded this image
canvas.canvasIO.checkForInputData();
// On execution we allow both image and mask to load
canvas.canvasIO.checkForInputData({ allowImage: true, allowMask: true, reason: "execution" });
}
// Call original if it exists