refactor: centralize fit-on-add policy

This commit is contained in:
Dariusz L
2026-08-13 15:52:47 +02:00
parent 23ad04b2de
commit 03187ce8ed
9 changed files with 80 additions and 32 deletions
+5 -10
View File
@@ -4,6 +4,7 @@ import { showErrorNotification } from "./utils/NotificationUtils.js";
import { webSocketManager } from "./utils/WebSocketManager.js";
import { scaleImageToFit, loadImage, blobToDataUrl, tensorToImageData, createImageFromImageData } from "./utils/ImageUtils.js";
import { postImageBlob } from "./utils/ImageUploadUtils.js";
import { getImageAddMode, isFitOnAddEnabled } from "./utils/CanvasInputUtils.js";
const log = createModuleLogger('CanvasIO');
function imageBatchIdentity(sources) {
return sources.join('|');
@@ -432,8 +433,7 @@ export class CanvasIO {
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';
const addMode = getImageAddMode(this.canvas.node.widgets);
// Add all images from the batch as separate layers
await this.addBatchImages(sourceNode.imgs, addMode, this.canvas.outputAreaBounds, 'to canvas');
this.canvas.inputDataLoaded = true;
@@ -522,9 +522,7 @@ export class CanvasIO {
// Convert to HTMLImageElement
const maskImg = await loadImage(maskCanvas.toDataURL());
// Respect fit_on_add (scale to output area)
const widgets = this.canvas.node.widgets;
const fitOnAddWidget = widgets ? widgets.find((w) => w.name === "fit_on_add") : null;
const shouldFit = fitOnAddWidget && fitOnAddWidget.value;
const shouldFit = isFitOnAddEnabled(this.canvas.node.widgets);
let finalMaskImg = maskImg;
if (shouldFit) {
const bounds = this.canvas.outputAreaBounds;
@@ -634,9 +632,7 @@ export class CanvasIO {
// Mark that we've loaded input data for this execution
this.canvas.inputDataLoaded = true;
// Determine add mode based on fit_on_add setting
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';
const addMode = getImageAddMode(this.canvas.node.widgets);
// 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) {
@@ -670,8 +666,7 @@ export class CanvasIO {
// Load mask image
const maskImg = await loadImage(inputData.input_mask);
// Determine if we should fit the mask or use it at original size
const fitOnAddWidget2 = this.canvas.node.widgets.find((w) => w.name === "fit_on_add");
const shouldFit = fitOnAddWidget2 && fitOnAddWidget2.value;
const shouldFit = isFitOnAddEnabled(this.canvas.node.widgets);
let finalMaskImg = maskImg;
if (shouldFit && this.canvas.maskTool) {
const bounds = this.canvas.outputAreaBounds;
+2 -2
View File
@@ -1,6 +1,7 @@
import { createModuleLogger } from "./log_system/log_funcs.js";
import { snapToGrid, getSnapAdjustment, isPointInRotatedLayer } from "./utils/CommonUtils.js";
import { loadImageFromBlob } from "./utils/ImageUtils.js";
import { getImageAddMode } from "./utils/CanvasInputUtils.js";
const log = createModuleLogger('CanvasInteractions');
export class CanvasInteractions {
constructor(canvas) {
@@ -1108,8 +1109,7 @@ export class CanvasInteractions {
}
async loadDroppedImageFile(file, worldCoords) {
void loadImageFromBlob(file).then(async (img) => {
const fitOnAddWidget = this.canvas.node.widgets.find((w) => w.name === "fit_on_add");
const addMode = fitOnAddWidget && fitOnAddWidget.value ? 'fit' : 'center';
const addMode = getImageAddMode(this.canvas.node.widgets);
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
}).catch(error => {
log.error(`Failed to load dropped image: ${file.name}`, error);
+3 -4
View File
@@ -15,6 +15,7 @@ import { iconLoader, LAYERFORGE_TOOLS } from "./utils/IconLoader.js";
import { exportCanvasImage } from "./utils/CanvasExportUtils.js";
import { getFlattenedCanvasBlob } from "./utils/CanvasBlobUtils.js";
import { loadPreviewImage } from "./utils/PreviewUtils.js";
import { getImageAddMode } from "./utils/CanvasInputUtils.js";
import { fetchMattingModelStatus } from "./utils/MattingUtils.js";
import { setupSAMDetectorHook } from "./SAMDetectorIntegration.js";
const log = createModuleLogger('Canvas_view');
@@ -418,8 +419,7 @@ 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 addMode = fitOnAddWidget && fitOnAddWidget.value ? 'fit' : 'center';
const addMode = getImageAddMode(node.widgets);
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
@@ -447,8 +447,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 addMode = fitOnAddWidget && fitOnAddWidget.value ? 'fit' : 'center';
const addMode = getImageAddMode(node.widgets);
canvas.canvasLayers.handlePaste(addMode);
}
}),
+7
View File
@@ -0,0 +1,7 @@
export function isFitOnAddEnabled(widgets) {
const fitOnAddWidget = widgets?.find(widget => widget.name === 'fit_on_add');
return Boolean(fitOnAddWidget?.value);
}
export function getImageAddMode(widgets) {
return isFitOnAddEnabled(widgets) ? 'fit' : 'center';
}
+5 -10
View File
@@ -4,6 +4,7 @@ import { showErrorNotification } from "./utils/NotificationUtils.js";
import { webSocketManager } from "./utils/WebSocketManager.js";
import { scaleImageToFit, loadImage, blobToDataUrl, tensorToImageData, createImageFromImageData } from "./utils/ImageUtils.js";
import { postImageBlob } from "./utils/ImageUploadUtils.js";
import { getImageAddMode, isFitOnAddEnabled } from "./utils/CanvasInputUtils.js";
import type { Canvas } from './Canvas';
import type { Layer, Shape, AddMode } from './types';
@@ -537,8 +538,7 @@ export class CanvasIO {
}
// Determine add mode
const fitOnAddWidget = this.canvas.node.widgets.find((w) => w.name === "fit_on_add");
const addMode = (fitOnAddWidget && fitOnAddWidget.value) ? 'fit' : 'center';
const addMode = getImageAddMode(this.canvas.node.widgets);
// Add all images from the batch as separate layers
await this.addBatchImages(
@@ -639,9 +639,7 @@ export class CanvasIO {
const maskImg = await loadImage(maskCanvas.toDataURL());
// Respect fit_on_add (scale to output area)
const widgets = this.canvas.node.widgets;
const fitOnAddWidget = widgets ? widgets.find((w: any) => w.name === "fit_on_add") : null;
const shouldFit = fitOnAddWidget && fitOnAddWidget.value;
const shouldFit = isFitOnAddEnabled(this.canvas.node.widgets);
let finalMaskImg: HTMLImageElement = maskImg;
if (shouldFit) {
@@ -761,9 +759,7 @@ export class CanvasIO {
this.canvas.inputDataLoaded = true;
// Determine add mode based on fit_on_add setting
const widgets = this.canvas.node.widgets;
const fitOnAddWidget = widgets ? widgets.find((w: any) => w.name === "fit_on_add") : null;
const addMode = (fitOnAddWidget && fitOnAddWidget.value) ? 'fit' : 'center';
const addMode = getImageAddMode(this.canvas.node.widgets);
// Load input image(s) only if image input is actually connected, not already loaded, and allowed
if (allowImage && !imageLoaded && hasImageInput) {
@@ -813,8 +809,7 @@ export class CanvasIO {
const maskImg = await loadImage(inputData.input_mask);
// Determine if we should fit the mask or use it at original size
const fitOnAddWidget2 = this.canvas.node.widgets.find((w) => w.name === "fit_on_add");
const shouldFit = fitOnAddWidget2 && fitOnAddWidget2.value;
const shouldFit = isFitOnAddEnabled(this.canvas.node.widgets);
let finalMaskImg: HTMLImageElement = maskImg;
if (shouldFit && this.canvas.maskTool) {
+2 -2
View File
@@ -1,6 +1,7 @@
import { createModuleLogger } from "./log_system/log_funcs.js";
import { snapToGrid, getSnapAdjustment, isPointInRotatedLayer } from "./utils/CommonUtils.js";
import { loadImageFromBlob } from "./utils/ImageUtils.js";
import { getImageAddMode } from "./utils/CanvasInputUtils.js";
import type { Canvas } from './Canvas';
import type { Layer, Point } from './types';
@@ -1285,8 +1286,7 @@ export class CanvasInteractions {
async loadDroppedImageFile(file: File, worldCoords: Point): Promise<void> {
void loadImageFromBlob(file).then(async img => {
const fitOnAddWidget = this.canvas.node.widgets.find((w: any) => w.name === "fit_on_add");
const addMode = fitOnAddWidget && fitOnAddWidget.value ? 'fit' : 'center';
const addMode = getImageAddMode(this.canvas.node.widgets);
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
}).catch(error => {
+3 -4
View File
@@ -21,6 +21,7 @@ import { iconLoader, LAYERFORGE_TOOLS } from "./utils/IconLoader.js";
import { exportCanvasImage, type CanvasExportAction } from "./utils/CanvasExportUtils.js";
import { getFlattenedCanvasBlob, type CanvasBlobVariant } from "./utils/CanvasBlobUtils.js";
import { loadPreviewImage } from "./utils/PreviewUtils.js";
import { getImageAddMode } from "./utils/CanvasInputUtils.js";
import { fetchMattingModelStatus } from "./utils/MattingUtils.js";
import { registerImageInClipspace, startSAMDetectorMonitoring, setupSAMDetectorHook } from "./SAMDetectorIntegration.js";
import type { ComfyNode, Layer, AddMode } from './types';
@@ -535,8 +536,7 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
textContent: "Add Image",
title: "Add image from file",
onclick: () => {
const fitOnAddWidget = node.widgets.find((w) => w.name === "fit_on_add");
const addMode: AddMode = fitOnAddWidget && fitOnAddWidget.value ? 'fit' : 'center';
const addMode: AddMode = getImageAddMode(node.widgets);
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
@@ -563,8 +563,7 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
textContent: "Paste Image",
title: "Paste image from clipboard",
onclick: () => {
const fitOnAddWidget = node.widgets.find((w) => w.name === "fit_on_add");
const addMode: AddMode = fitOnAddWidget && fitOnAddWidget.value ? 'fit' : 'center';
const addMode: AddMode = getImageAddMode(node.widgets);
canvas.canvasLayers.handlePaste(addMode);
}
}),
+14
View File
@@ -0,0 +1,14 @@
import type { AddMode } from '../types';
export function isFitOnAddEnabled(
widgets?: readonly { name: string; value?: unknown }[]
): boolean {
const fitOnAddWidget = widgets?.find(widget => widget.name === 'fit_on_add');
return Boolean(fitOnAddWidget?.value);
}
export function getImageAddMode(
widgets?: readonly { name: string; value?: unknown }[]
): Extract<AddMode, 'fit' | 'center'> {
return isFitOnAddEnabled(widgets) ? 'fit' : 'center';
}
@@ -0,0 +1,39 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';
import {
getImageAddMode,
isFitOnAddEnabled,
} from '../js/utils/CanvasInputUtils.js';
const [canvasIOSource, canvasViewSource, interactionsSource, inputUtilsSource] = await Promise.all([
readFile(new URL('../src/CanvasIO.ts', import.meta.url), 'utf8'),
readFile(new URL('../src/CanvasView.ts', import.meta.url), 'utf8'),
readFile(new URL('../src/CanvasInteractions.ts', import.meta.url), 'utf8'),
readFile(new URL('../src/utils/CanvasInputUtils.ts', import.meta.url), 'utf8'),
]);
test('image entry points use the shared fit_on_add policy', () => {
assert.match(inputUtilsSource, /export function isFitOnAddEnabled\(/);
assert.match(inputUtilsSource, /export function getImageAddMode\(/);
assert.equal((canvasIOSource.match(/getImageAddMode\(this/g) ?? []).length, 2);
assert.equal((canvasIOSource.match(/isFitOnAddEnabled\(this/g) ?? []).length, 2);
assert.equal((canvasViewSource.match(/getImageAddMode\(node/g) ?? []).length, 2);
assert.equal((interactionsSource.match(/getImageAddMode\(this/g) ?? []).length, 1);
assert.doesNotMatch(canvasIOSource, /find\(.*fit_on_add/);
assert.doesNotMatch(canvasViewSource, /find\(.*fit_on_add/);
assert.doesNotMatch(interactionsSource, /find\(.*fit_on_add/);
});
test('shared fit_on_add helper preserves fit and center behavior', () => {
assert.equal(isFitOnAddEnabled([{ name: 'fit_on_add', value: true }]), true);
assert.equal(isFitOnAddEnabled([{ name: 'fit_on_add', value: false }]), false);
assert.equal(isFitOnAddEnabled([{ name: 'fit_on_add', value: 1 }]), true);
assert.equal(isFitOnAddEnabled([{ name: 'other', value: true }]), false);
assert.equal(isFitOnAddEnabled(undefined), false);
assert.equal(getImageAddMode([{ name: 'fit_on_add', value: true }]), 'fit');
assert.equal(getImageAddMode([{ name: 'fit_on_add', value: false }]), 'center');
assert.equal(getImageAddMode(undefined), 'center');
});