diff --git a/js/CanvasIO.js b/js/CanvasIO.js index a4d708c..f6a3458 100644 --- a/js/CanvasIO.js +++ b/js/CanvasIO.js @@ -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; diff --git a/js/CanvasInteractions.js b/js/CanvasInteractions.js index e5da8b2..22bc7ae 100644 --- a/js/CanvasInteractions.js +++ b/js/CanvasInteractions.js @@ -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); diff --git a/js/CanvasView.js b/js/CanvasView.js index 44a8e18..32e8b46 100644 --- a/js/CanvasView.js +++ b/js/CanvasView.js @@ -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); } }), diff --git a/js/utils/CanvasInputUtils.js b/js/utils/CanvasInputUtils.js new file mode 100644 index 0000000..5b489f5 --- /dev/null +++ b/js/utils/CanvasInputUtils.js @@ -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'; +} diff --git a/src/CanvasIO.ts b/src/CanvasIO.ts index e0d1165..786fc5b 100644 --- a/src/CanvasIO.ts +++ b/src/CanvasIO.ts @@ -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) { diff --git a/src/CanvasInteractions.ts b/src/CanvasInteractions.ts index 1a2b605..f727dc3 100644 --- a/src/CanvasInteractions.ts +++ b/src/CanvasInteractions.ts @@ -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 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 => { diff --git a/src/CanvasView.ts b/src/CanvasView.ts index 6721f34..dfb9d52 100644 --- a/src/CanvasView.ts +++ b/src/CanvasView.ts @@ -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); } }), diff --git a/src/utils/CanvasInputUtils.ts b/src/utils/CanvasInputUtils.ts new file mode 100644 index 0000000..f67132c --- /dev/null +++ b/src/utils/CanvasInputUtils.ts @@ -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 { + return isFitOnAddEnabled(widgets) ? 'fit' : 'center'; +} diff --git a/tests/test_canvas_input_options_characterization.mjs b/tests/test_canvas_input_options_characterization.mjs new file mode 100644 index 0000000..99d0ce3 --- /dev/null +++ b/tests/test_canvas_input_options_characterization.mjs @@ -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'); +});