refactor: share mask result application

This commit is contained in:
Dariusz L
2026-08-13 08:20:35 +02:00
parent 5b90135533
commit c1ec26644d
7 changed files with 132 additions and 82 deletions
+6 -7
View File
@@ -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;
+31 -30
View File
@@ -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();
+5
View File
@@ -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
+6 -8
View File
@@ -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);
+32 -33
View File
@@ -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();
+14
View File
@@ -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<HTMLImageElement> {
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
+38 -4
View File
@@ -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\(\)/);
});