diff --git a/js/MaskEditorIntegration.js b/js/MaskEditorIntegration.js index e8f9467..35b694e 100644 --- a/js/MaskEditorIntegration.js +++ b/js/MaskEditorIntegration.js @@ -5,7 +5,7 @@ import { ComfyApp } from "../../scripts/app.js"; import { createModuleLogger } from "./log_system/log_funcs.js"; import { showErrorNotification } from "./utils/NotificationUtils.js"; import { uploadImageBlob } from "./utils/ImageUploadUtils.js"; -import { createMaskImageFromResult, processMaskForViewport } from "./utils/MaskProcessingUtils.js"; +import { applyMaskResultToTool, processMaskForViewport } from "./utils/MaskProcessingUtils.js"; import { updateNodePreview } from "./utils/PreviewUtils.js"; import { mask_editor_showing, mask_editor_listen_for_cancel } from "./utils/mask_utils.js"; import { createCanvas } from "./utils/CommonUtils.js"; @@ -414,16 +414,15 @@ export class MaskEditorIntegration { // Process image to mask using MaskProcessingUtils log.debug("Processing image to mask using utils"); const bounds = this.canvas.outputAreaBounds; - const maskAsImage = await createMaskImageFromResult(resultImage, { - targetWidth: bounds.width, - targetHeight: bounds.height, - invertAlpha: true - }); log.debug("Applying mask using chunk system", { boundsPos: { x: bounds.x, y: bounds.y }, maskSize: { width: bounds.width, height: bounds.height } }); - this.maskTool.setMask(maskAsImage); + await applyMaskResultToTool(resultImage, { + targetWidth: bounds.width, + targetHeight: bounds.height, + invertAlpha: true + }, () => this.maskTool); // Update node preview using PreviewUtils await updateNodePreview(this.canvas, this.node, true); this.savedMaskState = null; diff --git a/js/SAMDetectorIntegration.js b/js/SAMDetectorIntegration.js index ad1060a..419e91f 100644 --- a/js/SAMDetectorIntegration.js +++ b/js/SAMDetectorIntegration.js @@ -3,7 +3,7 @@ import { ComfyApp } from "../../scripts/app.js"; import { createModuleLogger } from "./log_system/log_funcs.js"; import { showInfoNotification, showSuccessNotification, showErrorNotification } from "./utils/NotificationUtils.js"; import { uploadCanvasAsImage, uploadImageBlob } from "./utils/ImageUploadUtils.js"; -import { createMaskImageFromResult } from "./utils/MaskProcessingUtils.js"; +import { applyMaskResultToTool } from "./utils/MaskProcessingUtils.js"; import { updateNodePreview } from "./utils/PreviewUtils.js"; import { validateAndFixClipspace } from "./utils/ClipspaceUtils.js"; const log = createModuleLogger('SAMDetectorIntegration'); @@ -305,40 +305,41 @@ async function handleSAMDetectorResult(node, resultImage) { } // Process image to mask using MaskProcessingUtils log.debug("Processing image to mask using utils"); - const maskAsImage = await createMaskImageFromResult(resultImage, { + let actualCanvas; + await applyMaskResultToTool(resultImage, { targetWidth: resultImage.width, targetHeight: resultImage.height, invertAlpha: true - }); - // Apply mask to LayerForge canvas using MaskTool.setMask method - log.debug("Checking canvas and maskTool availability", { - hasCanvas: !!canvas, - hasCanvasProperty: !!canvas.canvas, - canvasCanvasKeys: canvas.canvas ? Object.keys(canvas.canvas) : [], - hasMaskTool: !!canvas.maskTool, - hasCanvasMaskTool: !!(canvas.canvas && canvas.canvas.maskTool), - maskToolType: typeof canvas.maskTool, - canvasMaskToolType: canvas.canvas ? typeof canvas.canvas.maskTool : 'undefined', - canvasKeys: Object.keys(canvas) - }); - // Get the actual Canvas object and its maskTool - const actualCanvas = canvas.canvas || canvas; - const maskTool = actualCanvas.maskTool; - if (!maskTool) { - log.error("MaskTool is not available. Canvas state:", { + }, () => { + // Apply mask to LayerForge canvas using MaskTool.setMask method + log.debug("Checking canvas and maskTool availability", { hasCanvas: !!canvas, - hasActualCanvas: !!actualCanvas, - canvasConstructor: canvas.constructor.name, - actualCanvasConstructor: actualCanvas ? actualCanvas.constructor.name : 'undefined', - canvasKeys: Object.keys(canvas), - actualCanvasKeys: actualCanvas ? Object.keys(actualCanvas) : [], - maskToolValue: maskTool + hasCanvasProperty: !!canvas.canvas, + canvasCanvasKeys: canvas.canvas ? Object.keys(canvas.canvas) : [], + hasMaskTool: !!canvas.maskTool, + hasCanvasMaskTool: !!(canvas.canvas && canvas.canvas.maskTool), + maskToolType: typeof canvas.maskTool, + canvasMaskToolType: canvas.canvas ? typeof canvas.canvas.maskTool : 'undefined', + canvasKeys: Object.keys(canvas) }); - throw new Error("Mask tool not available or not initialized"); - } - log.debug("Applying SAM mask to canvas using setMask method"); - // Use the setMask method which clears existing mask and sets new one - maskTool.setMask(maskAsImage); + // Get the actual Canvas object and its maskTool + actualCanvas = canvas.canvas || canvas; + const maskTool = actualCanvas.maskTool; + if (!maskTool) { + log.error("MaskTool is not available. Canvas state:", { + hasCanvas: !!canvas, + hasActualCanvas: !!actualCanvas, + canvasConstructor: canvas.constructor.name, + actualCanvasConstructor: actualCanvas ? actualCanvas.constructor.name : 'undefined', + canvasKeys: Object.keys(canvas), + actualCanvasKeys: actualCanvas ? Object.keys(actualCanvas) : [], + maskToolValue: maskTool + }); + throw new Error("Mask tool not available or not initialized"); + } + log.debug("Applying SAM mask to canvas using setMask method"); + return maskTool; + }); // Update canvas and save state (same as MaskEditorIntegration) actualCanvas.render(); actualCanvas.saveState(); diff --git a/js/utils/MaskProcessingUtils.js b/js/utils/MaskProcessingUtils.js index 4cc390b..b55eccb 100644 --- a/js/utils/MaskProcessingUtils.js +++ b/js/utils/MaskProcessingUtils.js @@ -55,6 +55,11 @@ export async function createMaskImageFromResult(sourceImage, options) { }); return convertToImage(processedMask); } +export async function applyMaskResultToTool(sourceImage, options, resolveTarget) { + const maskImage = await createMaskImageFromResult(sourceImage, options); + resolveTarget().setMask(maskImage); + return maskImage; +} /** * Processes image data with custom pixel transformation * @param sourceImage - Source image or canvas element diff --git a/src/MaskEditorIntegration.ts b/src/MaskEditorIntegration.ts index 3bd8ff5..2dba372 100644 --- a/src/MaskEditorIntegration.ts +++ b/src/MaskEditorIntegration.ts @@ -7,7 +7,7 @@ import {api} from "../../scripts/api.js"; import { createModuleLogger } from "./log_system/log_funcs.js"; import { showErrorNotification } from "./utils/NotificationUtils.js"; import { uploadCanvasAsImage, uploadCanvasWithMaskAsImage, uploadImageBlob } from "./utils/ImageUploadUtils.js"; -import { createMaskImageFromResult, processMaskForViewport } from "./utils/MaskProcessingUtils.js"; +import { applyMaskResultToTool, processMaskForViewport } from "./utils/MaskProcessingUtils.js"; import { updateNodePreview } from "./utils/PreviewUtils.js"; import { mask_editor_showing, mask_editor_listen_for_cancel } from "./utils/mask_utils.js"; import { createCanvas } from "./utils/CommonUtils.js"; @@ -499,18 +499,16 @@ export class MaskEditorIntegration { // Process image to mask using MaskProcessingUtils log.debug("Processing image to mask using utils"); const bounds = this.canvas.outputAreaBounds; - const maskAsImage = await createMaskImageFromResult(resultImage, { - targetWidth: bounds.width, - targetHeight: bounds.height, - invertAlpha: true - }); - log.debug("Applying mask using chunk system", { boundsPos: {x: bounds.x, y: bounds.y}, maskSize: {width: bounds.width, height: bounds.height} }); - this.maskTool.setMask(maskAsImage); + await applyMaskResultToTool(resultImage, { + targetWidth: bounds.width, + targetHeight: bounds.height, + invertAlpha: true + }, () => this.maskTool); // Update node preview using PreviewUtils await updateNodePreview(this.canvas, this.node, true); diff --git a/src/SAMDetectorIntegration.ts b/src/SAMDetectorIntegration.ts index 1f9415a..66f1f19 100644 --- a/src/SAMDetectorIntegration.ts +++ b/src/SAMDetectorIntegration.ts @@ -5,7 +5,7 @@ import { ComfyApp } from "../../scripts/app.js"; import { createModuleLogger } from "./log_system/log_funcs.js"; import { showInfoNotification, showSuccessNotification, showErrorNotification } from "./utils/NotificationUtils.js"; import { uploadCanvasAsImage, uploadImageBlob } from "./utils/ImageUploadUtils.js"; -import { createMaskImageFromResult } from "./utils/MaskProcessingUtils.js"; +import { applyMaskResultToTool } from "./utils/MaskProcessingUtils.js"; import { updateNodePreview } from "./utils/PreviewUtils.js"; import { validateAndFixClipspace } from "./utils/ClipspaceUtils.js"; import type { ComfyNode } from './types'; @@ -346,45 +346,44 @@ async function handleSAMDetectorResult(node: ComfyNode, resultImage: HTMLImageEl // Process image to mask using MaskProcessingUtils log.debug("Processing image to mask using utils"); - const maskAsImage = await createMaskImageFromResult(resultImage, { + let actualCanvas: any; + await applyMaskResultToTool(resultImage, { targetWidth: resultImage.width, targetHeight: resultImage.height, invertAlpha: true - }); - - // Apply mask to LayerForge canvas using MaskTool.setMask method - log.debug("Checking canvas and maskTool availability", { - hasCanvas: !!canvas, - hasCanvasProperty: !!canvas.canvas, - canvasCanvasKeys: canvas.canvas ? Object.keys(canvas.canvas) : [], - hasMaskTool: !!canvas.maskTool, - hasCanvasMaskTool: !!(canvas.canvas && canvas.canvas.maskTool), - maskToolType: typeof canvas.maskTool, - canvasMaskToolType: canvas.canvas ? typeof canvas.canvas.maskTool : 'undefined', - canvasKeys: Object.keys(canvas) - }); - - // Get the actual Canvas object and its maskTool - const actualCanvas = canvas.canvas || canvas; - const maskTool = actualCanvas.maskTool; - - if (!maskTool) { - log.error("MaskTool is not available. Canvas state:", { + }, () => { + // Apply mask to LayerForge canvas using MaskTool.setMask method + log.debug("Checking canvas and maskTool availability", { hasCanvas: !!canvas, - hasActualCanvas: !!actualCanvas, - canvasConstructor: canvas.constructor.name, - actualCanvasConstructor: actualCanvas ? actualCanvas.constructor.name : 'undefined', - canvasKeys: Object.keys(canvas), - actualCanvasKeys: actualCanvas ? Object.keys(actualCanvas) : [], - maskToolValue: maskTool + hasCanvasProperty: !!canvas.canvas, + canvasCanvasKeys: canvas.canvas ? Object.keys(canvas.canvas) : [], + hasMaskTool: !!canvas.maskTool, + hasCanvasMaskTool: !!(canvas.canvas && canvas.canvas.maskTool), + maskToolType: typeof canvas.maskTool, + canvasMaskToolType: canvas.canvas ? typeof canvas.canvas.maskTool : 'undefined', + canvasKeys: Object.keys(canvas) }); - throw new Error("Mask tool not available or not initialized"); - } - log.debug("Applying SAM mask to canvas using setMask method"); + // Get the actual Canvas object and its maskTool + actualCanvas = canvas.canvas || canvas; + const maskTool = actualCanvas.maskTool; - // Use the setMask method which clears existing mask and sets new one - maskTool.setMask(maskAsImage); + if (!maskTool) { + log.error("MaskTool is not available. Canvas state:", { + hasCanvas: !!canvas, + hasActualCanvas: !!actualCanvas, + canvasConstructor: canvas.constructor.name, + actualCanvasConstructor: actualCanvas ? actualCanvas.constructor.name : 'undefined', + canvasKeys: Object.keys(canvas), + actualCanvasKeys: actualCanvas ? Object.keys(actualCanvas) : [], + maskToolValue: maskTool + }); + throw new Error("Mask tool not available or not initialized"); + } + + log.debug("Applying SAM mask to canvas using setMask method"); + return maskTool; + }); // Update canvas and save state (same as MaskEditorIntegration) actualCanvas.render(); diff --git a/src/utils/MaskProcessingUtils.ts b/src/utils/MaskProcessingUtils.ts index 57ba9d8..6d0cc73 100644 --- a/src/utils/MaskProcessingUtils.ts +++ b/src/utils/MaskProcessingUtils.ts @@ -121,6 +121,20 @@ export async function createMaskImageFromResult( return convertToImage(processedMask); } +interface MaskResultTarget { + setMask(image: HTMLImageElement): void; +} + +export async function applyMaskResultToTool( + sourceImage: HTMLImageElement | HTMLCanvasElement, + options: MaskResultOptions, + resolveTarget: () => MaskResultTarget +): Promise { + const maskImage = await createMaskImageFromResult(sourceImage, options); + resolveTarget().setMask(maskImage); + return maskImage; +} + /** * Processes image data with custom pixel transformation * @param sourceImage - Source image or canvas element diff --git a/tests/test_mask_result_characterization.mjs b/tests/test_mask_result_characterization.mjs index b5fa32b..e0e8c34 100644 --- a/tests/test_mask_result_characterization.mjs +++ b/tests/test_mask_result_characterization.mjs @@ -2,7 +2,10 @@ import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import test from 'node:test'; -import { createMaskImageFromResult } from '../js/utils/MaskProcessingUtils.js'; +import { + applyMaskResultToTool, + createMaskImageFromResult, +} from '../js/utils/MaskProcessingUtils.js'; function installMaskConversionStubs(sourcePixels) { const originalDocument = Object.getOwnPropertyDescriptor(globalThis, 'document'); @@ -108,15 +111,46 @@ test('mask result conversion preserves target size, transformed pixels, and imag } }); +test('mask result application processes before resolving and updating the target tool', async () => { + const stubs = installMaskConversionStubs([ + 10, 20, 30, 0, + 40, 50, 60, 128, + ]); + const appliedMasks = []; + + try { + const result = await applyMaskResultToTool( + { width: 2, height: 1 }, + { targetWidth: 2, targetHeight: 1, invertAlpha: true }, + () => ({ + setMask(mask) { + appliedMasks.push(mask); + }, + }) + ); + + assert.deepEqual(appliedMasks, [result]); + assert.deepEqual([...stubs.output.data], [ + 255, 255, 255, 255, + 255, 255, 255, 127, + ]); + } finally { + stubs.restore(); + } +}); + test('mask integrations share conversion but retain their own side effects', async () => { const maskEditorSource = await readFile(new URL('../src/MaskEditorIntegration.ts', import.meta.url), 'utf8'); const samSource = await readFile(new URL('../src/SAMDetectorIntegration.ts', import.meta.url), 'utf8'); - assert.match(maskEditorSource, /createMaskImageFromResult\(/); - assert.match(samSource, /createMaskImageFromResult\(/); + assert.match(maskEditorSource, /applyMaskResultToTool\(/); + assert.match(samSource, /applyMaskResultToTool\(/); assert.doesNotMatch(maskEditorSource, /processImageToMask|convertToImage/); assert.doesNotMatch(samSource, /processImageToMask|convertToImage/); - assert.match(maskEditorSource, /this\.maskTool\.setMask\(maskAsImage\)/); + assert.doesNotMatch(maskEditorSource, /setMask\(maskAsImage\)/); + assert.doesNotMatch(samSource, /setMask\(maskAsImage\)/); + assert.match(maskEditorSource, /targetWidth: bounds\.width/); + assert.match(samSource, /targetWidth: resultImage\.width/); assert.match(samSource, /actualCanvas\.render\(\)/); assert.match(samSource, /actualCanvas\.saveState\(\)/); });