mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-24 22:12:17 -03:00
Improve batch images and mask handling
Fixed batch image processing to prevent duplicates and layer deletion while ensuring proper mask loading from input_mask. Images are now added as new layers without removing existing ones, and masks are always checked from backend regardless of image state.
This commit is contained in:
@@ -263,24 +263,48 @@ class LayerForgeNode:
|
|||||||
input_data = {}
|
input_data = {}
|
||||||
|
|
||||||
if input_image is not None:
|
if input_image is not None:
|
||||||
# Convert image tensor to base64
|
# Convert image tensor(s) to base64 - handle batch
|
||||||
if isinstance(input_image, torch.Tensor):
|
if isinstance(input_image, torch.Tensor):
|
||||||
# Ensure correct shape [B, H, W, C]
|
# Ensure correct shape [B, H, W, C]
|
||||||
if input_image.dim() == 3:
|
if input_image.dim() == 3:
|
||||||
input_image = input_image.unsqueeze(0)
|
input_image = input_image.unsqueeze(0)
|
||||||
|
|
||||||
# Convert to numpy and then to PIL
|
batch_size = input_image.shape[0]
|
||||||
img_np = (input_image.squeeze(0).cpu().numpy() * 255).astype(np.uint8)
|
log_info(f"Processing batch of {batch_size} image(s)")
|
||||||
pil_img = Image.fromarray(img_np, 'RGB')
|
|
||||||
|
|
||||||
# Convert to base64
|
if batch_size == 1:
|
||||||
buffered = io.BytesIO()
|
# Single image - keep backward compatibility
|
||||||
pil_img.save(buffered, format="PNG")
|
img_np = (input_image.squeeze(0).cpu().numpy() * 255).astype(np.uint8)
|
||||||
img_str = base64.b64encode(buffered.getvalue()).decode()
|
pil_img = Image.fromarray(img_np, 'RGB')
|
||||||
input_data['input_image'] = f"data:image/png;base64,{img_str}"
|
|
||||||
input_data['input_image_width'] = pil_img.width
|
# Convert to base64
|
||||||
input_data['input_image_height'] = pil_img.height
|
buffered = io.BytesIO()
|
||||||
log_debug(f"Stored input image: {pil_img.width}x{pil_img.height}")
|
pil_img.save(buffered, format="PNG")
|
||||||
|
img_str = base64.b64encode(buffered.getvalue()).decode()
|
||||||
|
input_data['input_image'] = f"data:image/png;base64,{img_str}"
|
||||||
|
input_data['input_image_width'] = pil_img.width
|
||||||
|
input_data['input_image_height'] = pil_img.height
|
||||||
|
log_debug(f"Stored single input image: {pil_img.width}x{pil_img.height}")
|
||||||
|
else:
|
||||||
|
# Multiple images - store as array
|
||||||
|
images_array = []
|
||||||
|
for i in range(batch_size):
|
||||||
|
img_np = (input_image[i].cpu().numpy() * 255).astype(np.uint8)
|
||||||
|
pil_img = Image.fromarray(img_np, 'RGB')
|
||||||
|
|
||||||
|
# Convert to base64
|
||||||
|
buffered = io.BytesIO()
|
||||||
|
pil_img.save(buffered, format="PNG")
|
||||||
|
img_str = base64.b64encode(buffered.getvalue()).decode()
|
||||||
|
images_array.append({
|
||||||
|
'data': f"data:image/png;base64,{img_str}",
|
||||||
|
'width': pil_img.width,
|
||||||
|
'height': pil_img.height
|
||||||
|
})
|
||||||
|
log_debug(f"Stored batch image {i+1}/{batch_size}: {pil_img.width}x{pil_img.height}")
|
||||||
|
|
||||||
|
input_data['input_images_batch'] = images_array
|
||||||
|
log_info(f"Stored batch of {batch_size} images")
|
||||||
|
|
||||||
if input_mask is not None:
|
if input_mask is not None:
|
||||||
# Convert mask tensor to base64
|
# Convert mask tensor to base64
|
||||||
@@ -498,7 +522,7 @@ class LayerForgeNode:
|
|||||||
|
|
||||||
with cls._storage_lock:
|
with cls._storage_lock:
|
||||||
input_key = f"{node_id}_input"
|
input_key = f"{node_id}_input"
|
||||||
input_data = cls._canvas_data_storage.pop(input_key, None)
|
input_data = cls._canvas_data_storage.get(input_key, None)
|
||||||
|
|
||||||
if input_data:
|
if input_data:
|
||||||
log_info(f"Input data found for node {node_id}, sending to frontend")
|
log_info(f"Input data found for node {node_id}, sending to frontend")
|
||||||
@@ -521,6 +545,32 @@ class LayerForgeNode:
|
|||||||
'error': str(e)
|
'error': str(e)
|
||||||
}, status=500)
|
}, status=500)
|
||||||
|
|
||||||
|
@PromptServer.instance.routes.post("/layerforge/clear_input_data/{node_id}")
|
||||||
|
async def clear_input_data(request):
|
||||||
|
try:
|
||||||
|
node_id = request.match_info["node_id"]
|
||||||
|
log_info(f"Clearing input data for node: {node_id}")
|
||||||
|
|
||||||
|
with cls._storage_lock:
|
||||||
|
input_key = f"{node_id}_input"
|
||||||
|
if input_key in cls._canvas_data_storage:
|
||||||
|
del cls._canvas_data_storage[input_key]
|
||||||
|
log_info(f"Input data cleared for node {node_id}")
|
||||||
|
else:
|
||||||
|
log_debug(f"No input data to clear for node {node_id}")
|
||||||
|
|
||||||
|
return web.json_response({
|
||||||
|
'success': True,
|
||||||
|
'message': f'Input data cleared for node {node_id}'
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log_error(f"Error in clear_input_data: {str(e)}")
|
||||||
|
return web.json_response({
|
||||||
|
'success': False,
|
||||||
|
'error': str(e)
|
||||||
|
}, status=500)
|
||||||
|
|
||||||
@PromptServer.instance.routes.get("/ycnode/get_canvas_data/{node_id}")
|
@PromptServer.instance.routes.get("/ycnode/get_canvas_data/{node_id}")
|
||||||
async def get_canvas_data(request):
|
async def get_canvas_data(request):
|
||||||
try:
|
try:
|
||||||
|
|||||||
197
js/CanvasIO.js
197
js/CanvasIO.js
@@ -370,35 +370,87 @@ export class CanvasIO {
|
|||||||
// Track loaded links separately for image and mask
|
// Track loaded links separately for image and mask
|
||||||
let imageLoaded = false;
|
let imageLoaded = false;
|
||||||
let maskLoaded = false;
|
let maskLoaded = false;
|
||||||
|
let imageChanged = false;
|
||||||
// First, try to get data from connected node's output if available
|
// 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) {
|
if (this.canvas.node.inputs && this.canvas.node.inputs[0] && this.canvas.node.inputs[0].link) {
|
||||||
const linkId = this.canvas.node.inputs[0].link;
|
const linkId = this.canvas.node.inputs[0].link;
|
||||||
const graph = this.canvas.node.graph;
|
const graph = this.canvas.node.graph;
|
||||||
// Check if we already loaded this link
|
// Always check if images have changed first
|
||||||
if (this.canvas.lastLoadedLinkId === linkId) {
|
if (graph) {
|
||||||
log.debug(`Image link ${linkId} already loaded`);
|
const link = graph.links[linkId];
|
||||||
imageLoaded = true;
|
if (link) {
|
||||||
|
const sourceNode = graph.getNodeById(link.origin_id);
|
||||||
|
if (sourceNode && sourceNode.imgs && sourceNode.imgs.length > 0) {
|
||||||
|
// Create current batch identifier (all image sources combined)
|
||||||
|
const currentBatchImageSrcs = sourceNode.imgs.map((img) => img.src).join('|');
|
||||||
|
// Check if this is the same link we loaded before
|
||||||
|
if (this.canvas.lastLoadedLinkId === linkId) {
|
||||||
|
// Same link, check if images actually changed
|
||||||
|
if (this.canvas.lastLoadedImageSrc !== currentBatchImageSrcs) {
|
||||||
|
log.info(`Batch images changed for link ${linkId} (${sourceNode.imgs.length} images), will reload...`);
|
||||||
|
log.debug(`Previous batch hash: ${this.canvas.lastLoadedImageSrc?.substring(0, 100)}...`);
|
||||||
|
log.debug(`Current batch hash: ${currentBatchImageSrcs.substring(0, 100)}...`);
|
||||||
|
imageChanged = true;
|
||||||
|
// Clear the inputDataLoaded flag to force reload from backend
|
||||||
|
this.canvas.inputDataLoaded = false;
|
||||||
|
// Clear the lastLoadedImageSrc to force reload
|
||||||
|
this.canvas.lastLoadedImageSrc = undefined;
|
||||||
|
// Clear backend data to force fresh load
|
||||||
|
fetch(`/layerforge/clear_input_data/${nodeId}`, { method: 'POST' })
|
||||||
|
.then(() => log.debug("Backend input data cleared due to image change"))
|
||||||
|
.catch(err => log.error("Failed to clear backend data:", err));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.debug(`Batch images for link ${linkId} unchanged (${sourceNode.imgs.length} images)`);
|
||||||
|
imageLoaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Different link or first load
|
||||||
|
log.info(`New link ${linkId} detected, will load ${sourceNode.imgs.length} images`);
|
||||||
|
imageChanged = false; // It's not a change, it's a new link
|
||||||
|
imageLoaded = false; // Need to load
|
||||||
|
// Reset the inputDataLoaded flag for new link
|
||||||
|
this.canvas.inputDataLoaded = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
if (!imageLoaded || imageChanged) {
|
||||||
|
// Reset the inputDataLoaded flag when images change
|
||||||
|
if (imageChanged) {
|
||||||
|
this.canvas.inputDataLoaded = false;
|
||||||
|
log.info("Resetting inputDataLoaded flag due to image change");
|
||||||
|
}
|
||||||
if (graph) {
|
if (graph) {
|
||||||
const link = graph.links[linkId];
|
const link = graph.links[linkId];
|
||||||
if (link) {
|
if (link) {
|
||||||
const sourceNode = graph.getNodeById(link.origin_id);
|
const sourceNode = graph.getNodeById(link.origin_id);
|
||||||
if (sourceNode && sourceNode.imgs && sourceNode.imgs.length > 0) {
|
if (sourceNode && sourceNode.imgs && sourceNode.imgs.length > 0) {
|
||||||
// The connected node has images in its output
|
// The connected node has images in its output - handle multiple images (batch)
|
||||||
log.info("Found image in connected node's output, loading directly");
|
log.info(`Found ${sourceNode.imgs.length} image(s) in connected node's output, loading all`);
|
||||||
const img = sourceNode.imgs[0];
|
// Create a combined source identifier for batch detection
|
||||||
// Mark this link as loaded
|
const batchImageSrcs = sourceNode.imgs.map((img) => img.src).join('|');
|
||||||
|
// Mark this link and batch sources as loaded
|
||||||
this.canvas.lastLoadedLinkId = linkId;
|
this.canvas.lastLoadedLinkId = linkId;
|
||||||
// DON'T clear existing layers - just add a new one
|
this.canvas.lastLoadedImageSrc = batchImageSrcs;
|
||||||
|
// Don't clear layers - just add new ones
|
||||||
|
if (imageChanged) {
|
||||||
|
log.info("Image change detected, will add new layers");
|
||||||
|
}
|
||||||
// Determine add mode
|
// Determine add mode
|
||||||
const fitOnAddWidget = this.canvas.node.widgets.find((w) => w.name === "fit_on_add");
|
const fitOnAddWidget = this.canvas.node.widgets.find((w) => w.name === "fit_on_add");
|
||||||
const addMode = (fitOnAddWidget && fitOnAddWidget.value) ? 'fit' : 'center';
|
const addMode = (fitOnAddWidget && fitOnAddWidget.value) ? 'fit' : 'center';
|
||||||
// Add the image to canvas as a new layer
|
// Add all images from the batch as separate layers
|
||||||
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode, this.canvas.outputAreaBounds);
|
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;
|
this.canvas.inputDataLoaded = true;
|
||||||
imageLoaded = true;
|
imageLoaded = true;
|
||||||
log.info("Input image added as new layer from connected node");
|
log.info(`All ${sourceNode.imgs.length} input images from batch added as separate layers`);
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
this.canvas.saveState();
|
this.canvas.saveState();
|
||||||
}
|
}
|
||||||
@@ -558,15 +610,35 @@ export class CanvasIO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If both are already loaded from connected nodes, we're done
|
// 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;
|
const nodeInputs = this.canvas.node.inputs;
|
||||||
if (imageLoaded && (!nodeInputs || !nodeInputs[1] || maskLoaded)) {
|
// Check backend for input data
|
||||||
return;
|
|
||||||
}
|
|
||||||
// If no data from connected node, check backend
|
|
||||||
const response = await fetch(`/layerforge/get_input_data/${nodeId}`);
|
const response = await fetch(`/layerforge/get_input_data/${nodeId}`);
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success && result.has_input) {
|
if (result.success && result.has_input) {
|
||||||
|
// Dedupe: skip only if backend payload matches last loaded batch hash
|
||||||
|
let backendBatchHash;
|
||||||
|
if (result.data?.input_images_batch && Array.isArray(result.data.input_images_batch)) {
|
||||||
|
backendBatchHash = result.data.input_images_batch.map((i) => i.data).join('|');
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
if (backendBatchHash && this.canvas.lastLoadedImageSrc === backendBatchHash && !shouldCheckMask) {
|
||||||
|
log.debug("Backend input data unchanged and no mask to check, skipping reload");
|
||||||
|
this.canvas.inputDataLoaded = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (backendBatchHash && this.canvas.lastLoadedImageSrc === backendBatchHash && shouldCheckMask) {
|
||||||
|
log.debug("Images unchanged but need to check mask, continuing...");
|
||||||
|
imageLoaded = true; // Mark images as already loaded to skip reloading them
|
||||||
|
}
|
||||||
// Check if we already loaded image data (by checking the current link)
|
// 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 (!imageLoaded && this.canvas.node.inputs && this.canvas.node.inputs[0] && this.canvas.node.inputs[0].link) {
|
||||||
const currentLinkId = this.canvas.node.inputs[0].link;
|
const currentLinkId = this.canvas.node.inputs[0].link;
|
||||||
@@ -576,42 +648,76 @@ export class CanvasIO {
|
|||||||
imageLoaded = false; // Will load from backend
|
imageLoaded = false; // Will load from backend
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Check if we already loaded mask data
|
// Always check for mask data from backend when there's a mask input
|
||||||
if (!maskLoaded && this.canvas.node.inputs && this.canvas.node.inputs[1] && this.canvas.node.inputs[1].link) {
|
// 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) {
|
||||||
const currentMaskLinkId = this.canvas.node.inputs[1].link;
|
const currentMaskLinkId = this.canvas.node.inputs[1].link;
|
||||||
if (this.canvas.lastLoadedMaskLinkId !== currentMaskLinkId) {
|
// Always reset mask loaded flag and clear lastLoadedMaskLinkId to force fresh load
|
||||||
// Mark this mask link as loaded
|
maskLoaded = false;
|
||||||
this.canvas.lastLoadedMaskLinkId = currentMaskLinkId;
|
// Clear the stored mask link to force reload from backend
|
||||||
maskLoaded = false; // Will load from backend
|
this.canvas.lastLoadedMaskLinkId = undefined;
|
||||||
}
|
log.debug(`Mask input detected, will force check backend for mask data`);
|
||||||
}
|
}
|
||||||
log.info("Input data found from backend, adding to canvas");
|
log.info("Input data found from backend, adding to canvas");
|
||||||
const inputData = result.data;
|
const inputData = result.data;
|
||||||
// DON'T clear existing layers - just add new ones
|
// Compute backend batch hash for dedupe and state
|
||||||
// Mark that we've loaded input data to avoid reloading
|
let backendHashNow;
|
||||||
|
if (inputData?.input_images_batch && Array.isArray(inputData.input_images_batch)) {
|
||||||
|
backendHashNow = inputData.input_images_batch.map((i) => i.data).join('|');
|
||||||
|
}
|
||||||
|
else if (inputData?.input_image) {
|
||||||
|
backendHashNow = inputData.input_image;
|
||||||
|
}
|
||||||
|
// Just update the hash without removing any layers
|
||||||
|
if (backendHashNow) {
|
||||||
|
log.info("New backend input data detected, adding new layers");
|
||||||
|
this.canvas.lastLoadedImageSrc = backendHashNow;
|
||||||
|
}
|
||||||
|
// Mark that we've loaded input data for this execution
|
||||||
this.canvas.inputDataLoaded = true;
|
this.canvas.inputDataLoaded = true;
|
||||||
// Determine add mode based on fit_on_add setting
|
// Determine add mode based on fit_on_add setting
|
||||||
const widgets = this.canvas.node.widgets;
|
const widgets = this.canvas.node.widgets;
|
||||||
const fitOnAddWidget = widgets ? widgets.find((w) => w.name === "fit_on_add") : null;
|
const fitOnAddWidget = widgets ? widgets.find((w) => w.name === "fit_on_add") : null;
|
||||||
const addMode = (fitOnAddWidget && fitOnAddWidget.value) ? 'fit' : 'center';
|
const addMode = (fitOnAddWidget && fitOnAddWidget.value) ? 'fit' : 'center';
|
||||||
// Load input image if provided and not already loaded
|
// Load input image(s) if provided and not already loaded
|
||||||
if (!imageLoaded && inputData.input_image) {
|
if (!imageLoaded) {
|
||||||
const img = new Image();
|
if (inputData.input_images_batch) {
|
||||||
await new Promise((resolve, reject) => {
|
// Handle batch of images
|
||||||
img.onload = resolve;
|
const batch = inputData.input_images_batch;
|
||||||
img.onerror = reject;
|
log.info(`Processing batch of ${batch.length} images from backend`);
|
||||||
img.src = inputData.input_image;
|
for (let i = 0; i < batch.length; i++) {
|
||||||
});
|
const imgData = batch[i];
|
||||||
// Add image to canvas at output area position
|
const img = new Image();
|
||||||
const layer = await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode, this.canvas.outputAreaBounds // Place at output area
|
await new Promise((resolve, reject) => {
|
||||||
);
|
img.onload = resolve;
|
||||||
// Don't apply mask to the layer anymore - we'll handle it separately
|
img.onerror = reject;
|
||||||
log.info("Input image added as new layer to canvas");
|
img.src = imgData.data;
|
||||||
this.canvas.render();
|
});
|
||||||
this.canvas.saveState();
|
// Add image to canvas with unique name
|
||||||
}
|
await this.canvas.canvasLayers.addLayerWithImage(img, { name: `Batch Image ${i + 1}` }, addMode, this.canvas.outputAreaBounds);
|
||||||
else {
|
log.debug(`Added batch image ${i + 1}/${batch.length} from backend`);
|
||||||
log.debug("No input image in data");
|
}
|
||||||
|
log.info(`All ${batch.length} batch images added from backend`);
|
||||||
|
this.canvas.render();
|
||||||
|
this.canvas.saveState();
|
||||||
|
}
|
||||||
|
else if (inputData.input_image) {
|
||||||
|
// Handle single image (backward compatibility)
|
||||||
|
const img = new Image();
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
img.onload = resolve;
|
||||||
|
img.onerror = reject;
|
||||||
|
img.src = inputData.input_image;
|
||||||
|
});
|
||||||
|
// Add image to canvas at output area position
|
||||||
|
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode, this.canvas.outputAreaBounds);
|
||||||
|
log.info("Single input image added as new layer to canvas");
|
||||||
|
this.canvas.render();
|
||||||
|
this.canvas.saveState();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.debug("No input image data from backend");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Handle mask separately (not tied to layer) if not already loaded
|
// Handle mask separately (not tied to layer) if not already loaded
|
||||||
if (!maskLoaded && inputData.input_mask) {
|
if (!maskLoaded && inputData.input_mask) {
|
||||||
@@ -675,7 +781,6 @@ export class CanvasIO {
|
|||||||
this.canvas.pendingInputDataCheck = window.setTimeout(() => {
|
this.canvas.pendingInputDataCheck = window.setTimeout(() => {
|
||||||
this.canvas.pendingInputDataCheck = null;
|
this.canvas.pendingInputDataCheck = null;
|
||||||
log.debug("Retrying input data check for mask...");
|
log.debug("Retrying input data check for mask...");
|
||||||
this.checkForInputData();
|
|
||||||
}, 500); // Shorter delay for mask data retry
|
}, 500); // Shorter delay for mask data retry
|
||||||
}
|
}
|
||||||
scheduleDataCheck() {
|
scheduleDataCheck() {
|
||||||
|
|||||||
230
src/CanvasIO.ts
230
src/CanvasIO.ts
@@ -436,47 +436,100 @@ export class CanvasIO {
|
|||||||
// Track loaded links separately for image and mask
|
// Track loaded links separately for image and mask
|
||||||
let imageLoaded = false;
|
let imageLoaded = false;
|
||||||
let maskLoaded = false;
|
let maskLoaded = false;
|
||||||
|
let imageChanged = false;
|
||||||
|
|
||||||
// First, try to get data from connected node's output if available
|
// 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) {
|
if (this.canvas.node.inputs && this.canvas.node.inputs[0] && this.canvas.node.inputs[0].link) {
|
||||||
const linkId = this.canvas.node.inputs[0].link;
|
const linkId = this.canvas.node.inputs[0].link;
|
||||||
const graph = (this.canvas.node as any).graph;
|
const graph = (this.canvas.node as any).graph;
|
||||||
|
|
||||||
// Check if we already loaded this link
|
// Always check if images have changed first
|
||||||
if (this.canvas.lastLoadedLinkId === linkId) {
|
if (graph) {
|
||||||
log.debug(`Image link ${linkId} already loaded`);
|
const link = graph.links[linkId];
|
||||||
imageLoaded = true;
|
if (link) {
|
||||||
} else {
|
const sourceNode = graph.getNodeById(link.origin_id);
|
||||||
|
if (sourceNode && sourceNode.imgs && sourceNode.imgs.length > 0) {
|
||||||
|
// Create current batch identifier (all image sources combined)
|
||||||
|
const currentBatchImageSrcs = sourceNode.imgs.map((img: HTMLImageElement) => img.src).join('|');
|
||||||
|
|
||||||
|
// Check if this is the same link we loaded before
|
||||||
|
if (this.canvas.lastLoadedLinkId === linkId) {
|
||||||
|
// Same link, check if images actually changed
|
||||||
|
if (this.canvas.lastLoadedImageSrc !== currentBatchImageSrcs) {
|
||||||
|
log.info(`Batch images changed for link ${linkId} (${sourceNode.imgs.length} images), will reload...`);
|
||||||
|
log.debug(`Previous batch hash: ${this.canvas.lastLoadedImageSrc?.substring(0, 100)}...`);
|
||||||
|
log.debug(`Current batch hash: ${currentBatchImageSrcs.substring(0, 100)}...`);
|
||||||
|
imageChanged = true;
|
||||||
|
// Clear the inputDataLoaded flag to force reload from backend
|
||||||
|
this.canvas.inputDataLoaded = false;
|
||||||
|
// Clear the lastLoadedImageSrc to force reload
|
||||||
|
this.canvas.lastLoadedImageSrc = undefined;
|
||||||
|
// Clear backend data to force fresh load
|
||||||
|
fetch(`/layerforge/clear_input_data/${nodeId}`, { method: 'POST' })
|
||||||
|
.then(() => log.debug("Backend input data cleared due to image change"))
|
||||||
|
.catch(err => log.error("Failed to clear backend data:", err));
|
||||||
|
} else {
|
||||||
|
log.debug(`Batch images for link ${linkId} unchanged (${sourceNode.imgs.length} images)`);
|
||||||
|
imageLoaded = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Different link or first load
|
||||||
|
log.info(`New link ${linkId} detected, will load ${sourceNode.imgs.length} images`);
|
||||||
|
imageChanged = false; // It's not a change, it's a new link
|
||||||
|
imageLoaded = false; // Need to load
|
||||||
|
// Reset the inputDataLoaded flag for new link
|
||||||
|
this.canvas.inputDataLoaded = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!imageLoaded || imageChanged) {
|
||||||
|
// Reset the inputDataLoaded flag when images change
|
||||||
|
if (imageChanged) {
|
||||||
|
this.canvas.inputDataLoaded = false;
|
||||||
|
log.info("Resetting inputDataLoaded flag due to image change");
|
||||||
|
}
|
||||||
|
|
||||||
if (graph) {
|
if (graph) {
|
||||||
const link = graph.links[linkId];
|
const link = graph.links[linkId];
|
||||||
if (link) {
|
if (link) {
|
||||||
const sourceNode = graph.getNodeById(link.origin_id);
|
const sourceNode = graph.getNodeById(link.origin_id);
|
||||||
if (sourceNode && sourceNode.imgs && sourceNode.imgs.length > 0) {
|
if (sourceNode && sourceNode.imgs && sourceNode.imgs.length > 0) {
|
||||||
// The connected node has images in its output
|
// The connected node has images in its output - handle multiple images (batch)
|
||||||
log.info("Found image in connected node's output, loading directly");
|
log.info(`Found ${sourceNode.imgs.length} image(s) in connected node's output, loading all`);
|
||||||
const img = sourceNode.imgs[0];
|
|
||||||
|
|
||||||
// Mark this link as loaded
|
// Create a combined source identifier for batch detection
|
||||||
|
const batchImageSrcs = sourceNode.imgs.map((img: HTMLImageElement) => img.src).join('|');
|
||||||
|
|
||||||
|
// Mark this link and batch sources as loaded
|
||||||
this.canvas.lastLoadedLinkId = linkId;
|
this.canvas.lastLoadedLinkId = linkId;
|
||||||
|
this.canvas.lastLoadedImageSrc = batchImageSrcs;
|
||||||
|
|
||||||
// DON'T clear existing layers - just add a new one
|
// Don't clear layers - just add new ones
|
||||||
|
if (imageChanged) {
|
||||||
|
log.info("Image change detected, will add new layers");
|
||||||
|
}
|
||||||
|
|
||||||
// Determine add mode
|
// Determine add mode
|
||||||
const fitOnAddWidget = this.canvas.node.widgets.find((w) => w.name === "fit_on_add");
|
const fitOnAddWidget = this.canvas.node.widgets.find((w) => w.name === "fit_on_add");
|
||||||
const addMode = (fitOnAddWidget && fitOnAddWidget.value) ? 'fit' : 'center';
|
const addMode = (fitOnAddWidget && fitOnAddWidget.value) ? 'fit' : 'center';
|
||||||
|
|
||||||
// Add the image to canvas as a new layer
|
// Add all images from the batch as separate layers
|
||||||
await this.canvas.canvasLayers.addLayerWithImage(
|
for (let i = 0; i < sourceNode.imgs.length; i++) {
|
||||||
img,
|
const img = sourceNode.imgs[i];
|
||||||
{},
|
await this.canvas.canvasLayers.addLayerWithImage(
|
||||||
addMode,
|
img,
|
||||||
this.canvas.outputAreaBounds
|
{ 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;
|
this.canvas.inputDataLoaded = true;
|
||||||
imageLoaded = true;
|
imageLoaded = true;
|
||||||
log.info("Input image added as new layer from connected node");
|
log.info(`All ${sourceNode.imgs.length} input images from batch added as separate layers`);
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
this.canvas.saveState();
|
this.canvas.saveState();
|
||||||
}
|
}
|
||||||
@@ -644,17 +697,37 @@ export class CanvasIO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If both are already loaded from connected nodes, we're done
|
// 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;
|
const nodeInputs = this.canvas.node.inputs;
|
||||||
if (imageLoaded && (!nodeInputs || !nodeInputs[1] || maskLoaded)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no data from connected node, check backend
|
// Check backend for input data
|
||||||
const response = await fetch(`/layerforge/get_input_data/${nodeId}`);
|
const response = await fetch(`/layerforge/get_input_data/${nodeId}`);
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success && result.has_input) {
|
if (result.success && result.has_input) {
|
||||||
|
// Dedupe: skip only if backend payload matches last loaded batch hash
|
||||||
|
let backendBatchHash: string | undefined;
|
||||||
|
if (result.data?.input_images_batch && Array.isArray(result.data.input_images_batch)) {
|
||||||
|
backendBatchHash = result.data.input_images_batch.map((i: any) => i.data).join('|');
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backendBatchHash && this.canvas.lastLoadedImageSrc === backendBatchHash && !shouldCheckMask) {
|
||||||
|
log.debug("Backend input data unchanged and no mask to check, skipping reload");
|
||||||
|
this.canvas.inputDataLoaded = true;
|
||||||
|
return;
|
||||||
|
} else if (backendBatchHash && this.canvas.lastLoadedImageSrc === backendBatchHash && shouldCheckMask) {
|
||||||
|
log.debug("Images unchanged but need to check mask, continuing...");
|
||||||
|
imageLoaded = true; // Mark images as already loaded to skip reloading them
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we already loaded image data (by checking the current link)
|
// 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 (!imageLoaded && this.canvas.node.inputs && this.canvas.node.inputs[0] && this.canvas.node.inputs[0].link) {
|
||||||
const currentLinkId = this.canvas.node.inputs[0].link;
|
const currentLinkId = this.canvas.node.inputs[0].link;
|
||||||
@@ -665,22 +738,35 @@ export class CanvasIO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we already loaded mask data
|
// Always check for mask data from backend when there's a mask input
|
||||||
if (!maskLoaded && this.canvas.node.inputs && this.canvas.node.inputs[1] && this.canvas.node.inputs[1].link) {
|
// 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) {
|
||||||
const currentMaskLinkId = this.canvas.node.inputs[1].link;
|
const currentMaskLinkId = this.canvas.node.inputs[1].link;
|
||||||
if (this.canvas.lastLoadedMaskLinkId !== currentMaskLinkId) {
|
// Always reset mask loaded flag and clear lastLoadedMaskLinkId to force fresh load
|
||||||
// Mark this mask link as loaded
|
maskLoaded = false;
|
||||||
this.canvas.lastLoadedMaskLinkId = currentMaskLinkId;
|
// Clear the stored mask link to force reload from backend
|
||||||
maskLoaded = false; // Will load from backend
|
this.canvas.lastLoadedMaskLinkId = undefined;
|
||||||
}
|
log.debug(`Mask input detected, will force check backend for mask data`);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("Input data found from backend, adding to canvas");
|
log.info("Input data found from backend, adding to canvas");
|
||||||
const inputData = result.data;
|
const inputData = result.data;
|
||||||
|
|
||||||
// DON'T clear existing layers - just add new ones
|
// Compute backend batch hash for dedupe and state
|
||||||
|
let backendHashNow: string | undefined;
|
||||||
|
if (inputData?.input_images_batch && Array.isArray(inputData.input_images_batch)) {
|
||||||
|
backendHashNow = inputData.input_images_batch.map((i: any) => i.data).join('|');
|
||||||
|
} else if (inputData?.input_image) {
|
||||||
|
backendHashNow = inputData.input_image;
|
||||||
|
}
|
||||||
|
|
||||||
// Mark that we've loaded input data to avoid reloading
|
// Just update the hash without removing any layers
|
||||||
|
if (backendHashNow) {
|
||||||
|
log.info("New backend input data detected, adding new layers");
|
||||||
|
this.canvas.lastLoadedImageSrc = backendHashNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark that we've loaded input data for this execution
|
||||||
this.canvas.inputDataLoaded = true;
|
this.canvas.inputDataLoaded = true;
|
||||||
|
|
||||||
// Determine add mode based on fit_on_add setting
|
// Determine add mode based on fit_on_add setting
|
||||||
@@ -688,30 +774,60 @@ export class CanvasIO {
|
|||||||
const fitOnAddWidget = widgets ? widgets.find((w: any) => w.name === "fit_on_add") : null;
|
const fitOnAddWidget = widgets ? widgets.find((w: any) => w.name === "fit_on_add") : null;
|
||||||
const addMode = (fitOnAddWidget && fitOnAddWidget.value) ? 'fit' : 'center';
|
const addMode = (fitOnAddWidget && fitOnAddWidget.value) ? 'fit' : 'center';
|
||||||
|
|
||||||
// Load input image if provided and not already loaded
|
// Load input image(s) if provided and not already loaded
|
||||||
if (!imageLoaded && inputData.input_image) {
|
if (!imageLoaded) {
|
||||||
const img = new Image();
|
if (inputData.input_images_batch) {
|
||||||
await new Promise((resolve, reject) => {
|
// Handle batch of images
|
||||||
img.onload = resolve;
|
const batch = inputData.input_images_batch;
|
||||||
img.onerror = reject;
|
log.info(`Processing batch of ${batch.length} images from backend`);
|
||||||
img.src = inputData.input_image;
|
|
||||||
});
|
for (let i = 0; i < batch.length; i++) {
|
||||||
|
const imgData = batch[i];
|
||||||
// Add image to canvas at output area position
|
const img = new Image();
|
||||||
const layer = await this.canvas.canvasLayers.addLayerWithImage(
|
await new Promise((resolve, reject) => {
|
||||||
img,
|
img.onload = resolve;
|
||||||
{},
|
img.onerror = reject;
|
||||||
addMode,
|
img.src = imgData.data;
|
||||||
this.canvas.outputAreaBounds // Place at output area
|
});
|
||||||
);
|
|
||||||
|
// Add image to canvas with unique name
|
||||||
// Don't apply mask to the layer anymore - we'll handle it separately
|
await this.canvas.canvasLayers.addLayerWithImage(
|
||||||
|
img,
|
||||||
log.info("Input image added as new layer to canvas");
|
{ name: `Batch Image ${i + 1}` },
|
||||||
this.canvas.render();
|
addMode,
|
||||||
this.canvas.saveState();
|
this.canvas.outputAreaBounds
|
||||||
} else {
|
);
|
||||||
log.debug("No input image in data");
|
|
||||||
|
log.debug(`Added batch image ${i + 1}/${batch.length} from backend`);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`All ${batch.length} batch images added from backend`);
|
||||||
|
this.canvas.render();
|
||||||
|
this.canvas.saveState();
|
||||||
|
|
||||||
|
} else if (inputData.input_image) {
|
||||||
|
// Handle single image (backward compatibility)
|
||||||
|
const img = new Image();
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
img.onload = resolve;
|
||||||
|
img.onerror = reject;
|
||||||
|
img.src = inputData.input_image;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add image to canvas at output area position
|
||||||
|
await this.canvas.canvasLayers.addLayerWithImage(
|
||||||
|
img,
|
||||||
|
{},
|
||||||
|
addMode,
|
||||||
|
this.canvas.outputAreaBounds
|
||||||
|
);
|
||||||
|
|
||||||
|
log.info("Single input image added as new layer to canvas");
|
||||||
|
this.canvas.render();
|
||||||
|
this.canvas.saveState();
|
||||||
|
} else {
|
||||||
|
log.debug("No input image data from backend");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle mask separately (not tied to layer) if not already loaded
|
// Handle mask separately (not tied to layer) if not already loaded
|
||||||
@@ -791,7 +907,7 @@ export class CanvasIO {
|
|||||||
this.canvas.pendingInputDataCheck = window.setTimeout(() => {
|
this.canvas.pendingInputDataCheck = window.setTimeout(() => {
|
||||||
this.canvas.pendingInputDataCheck = null;
|
this.canvas.pendingInputDataCheck = null;
|
||||||
log.debug("Retrying input data check for mask...");
|
log.debug("Retrying input data check for mask...");
|
||||||
this.checkForInputData();
|
|
||||||
}, 500); // Shorter delay for mask data retry
|
}, 500); // Shorter delay for mask data retry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export interface Canvas {
|
|||||||
inputDataLoaded: boolean;
|
inputDataLoaded: boolean;
|
||||||
lastLoadedLinkId: any;
|
lastLoadedLinkId: any;
|
||||||
lastLoadedMaskLinkId: any;
|
lastLoadedMaskLinkId: any;
|
||||||
|
lastLoadedImageSrc?: string;
|
||||||
outputAreaBounds: OutputAreaBounds;
|
outputAreaBounds: OutputAreaBounds;
|
||||||
saveState: () => void;
|
saveState: () => void;
|
||||||
render: () => void;
|
render: () => void;
|
||||||
|
|||||||
Reference in New Issue
Block a user