diff --git a/litegraph b/litegraph index 1a77c46..34b0f0c 160000 --- a/litegraph +++ b/litegraph @@ -1 +1 @@ -Subproject commit 1a77c461ad33546a41bda3d8d983dabadf9d588a +Subproject commit 34b0f0c220473afc9057af88c47a5558b9ee520f diff --git a/src/lib/nodes/widgets/ComfyMultiRegionNode.ts b/src/lib/nodes/widgets/ComfyMultiRegionNode.ts index 10169e1..3881608 100644 --- a/src/lib/nodes/widgets/ComfyMultiRegionNode.ts +++ b/src/lib/nodes/widgets/ComfyMultiRegionNode.ts @@ -1,8 +1,8 @@ -import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core"; +import { BuiltInSlotType, LiteGraph, type IComboWidget, type SlotLayout, type INumberWidget } from "@litegraph-ts/core"; import MultiRegionWidget from "$lib/widgets/MultiRegionWidget.svelte"; import ComfyWidgetNode, { type ComfyWidgetProperties } from "./ComfyWidgetNode"; -import { clamp } from "$lib/utils"; +import { clamp, isComfyBoxImageMetadata, type ComfyBoxImageMetadata, comfyBoxImageToComfyURL } from "$lib/utils"; import { writable, type Writable } from "svelte/store"; /* x, y, width, height, all in range 0.0 - 1.0 */ @@ -16,6 +16,7 @@ export interface ComfyMultiRegionProperties extends ComfyWidgetProperties { regionCount: number, canvasWidth: number, canvasHeight: number, + canvasImageURL: string | null, inputType: "size" | "image" } @@ -28,16 +29,13 @@ export default class ComfyMultiRegionNode extends ComfyWidgetNode regionCount: 1, canvasWidth: 512, canvasHeight: 512, + canvasImageURL: null, inputType: "size" } static slotLayout: SlotLayout = { inputs: [ { name: "store", type: BuiltInSlotType.ACTION }, - - // dynamic inputs, may be removed later - { name: "width", type: "number" }, - { name: "height", type: "number" }, ], outputs: [ { name: "changed", type: BuiltInSlotType.EVENT }, @@ -56,6 +54,24 @@ export default class ComfyMultiRegionNode extends ComfyWidgetNode sizeChanged: Writable = writable(true); regionsChanged: Writable = writable(true); + inputTypeWidget: IComboWidget + regionCountWidget: INumberWidget + + constructor(name?: string) { + super(name, [[...DEFAULT_BBOX]]) + + this.inputTypeWidget = this.addWidget("combo", "Input Type", this.properties.inputType, "inputType", { values: ["size", "image"] }) + this.regionCountWidget = this.addWidget( + "number", + "# of Regions", + this.properties.regionCount, + (v: number) => { + this.setProperty("regionCount", clamp(v, 1, 16)); + }, + { min: 1, max: 16, step: 1, precision: 0 } + ) + } + override onPropertyChanged(property: any, value: any) { if (property === "regionCount") { this.updateRegions() @@ -63,19 +79,38 @@ export default class ComfyMultiRegionNode extends ComfyWidgetNode else if (property === "width" || property === "height") { this.updateSize(); } - } - - constructor(name?: string) { - super(name, [[...DEFAULT_BBOX]]) + else if (property === "inputType") { + this.updateInputType(); + } } override onExecute() { - let width = this.getInputData(1) || 0 - let height = this.getInputData(2) || 0 + let width = 0; + let height = 0; + let imageURL: string | null = null; - if (width != this.properties.canvasWidth || height != this.properties.canvasHeight) { + this.updateInputType(); + + if (this.properties.inputType === "image") { + let comfyBoxImage = this.getInputData(1); + if (isComfyBoxImageMetadata(comfyBoxImage)) { + imageURL = comfyBoxImageToComfyURL(comfyBoxImage) + width = comfyBoxImage.width; + height = comfyBoxImage.height; + } + else { + imageURL = null; + } + } + else if (this.properties.inputType === "size") { + width = this.getInputData(1) || 0 + height = this.getInputData(2) || 0 + } + + if (width != this.properties.canvasWidth || height != this.properties.canvasHeight || imageURL != this.properties.canvasImageURL) { this.properties.canvasWidth = width; this.properties.canvasHeight = height; + this.properties.canvasImageURL = imageURL this.updateSize(); } @@ -86,12 +121,12 @@ export default class ComfyMultiRegionNode extends ComfyWidgetNode if (bbox != null) { const output = this.outputs[index + 1] // + changed slot if (output != null) { - let data = this.getOutputData(index) || [0, 0, 0, 0] + let data = this.getOutputData(index + 1) || [0, 0, 0, 0] data[0] = bbox[0] * this.properties.canvasWidth data[1] = bbox[1] * this.properties.canvasHeight data[2] = bbox[2] * this.properties.canvasWidth data[3] = bbox[3] * this.properties.canvasHeight - this.setOutputData(index, data) + this.setOutputData(index + 1, data) } } } @@ -100,22 +135,29 @@ export default class ComfyMultiRegionNode extends ComfyWidgetNode private updateRegions() { this.properties.regionCount = Math.max(this.properties.regionCount, 0); - for (let index = this.outputs.length - 1; index >= 0; index--) { + if (this.outputs.length === this.properties.regionCount + 1) + return + + for (let index = this.outputs.length - 1; index > this.properties.regionCount; index--) { if (this.outputs[index].type !== BuiltInSlotType.EVENT) { this.removeOutput(index); } } - for (let index = 0; index < this.properties.regionCount; index++) { + for (let index = this.outputs.length - 1; index < this.properties.regionCount; index++) { this.addOutput(`region${index + 1}`, "COMFYBOX_REGION") } this.regionsChanged.set(true); - this.notifyPropsChanged(); + // this.notifyPropsChanged(); this.setValue(this.getValue()) } + private _prevWidth = null + private _prevHeight = null + private _prevImageURL = null + private updateSize(value?: BoundingBox[]): BoundingBox[] { this.properties.canvasWidth = Math.max(this.properties.canvasWidth, 0); this.properties.canvasHeight = Math.max(this.properties.canvasHeight, 0); @@ -129,12 +171,43 @@ export default class ComfyMultiRegionNode extends ComfyWidgetNode bbox[3] = clamp(bbox[3], 0, 1 - bbox[2]) } - this.sizeChanged.set(true); - this.notifyPropsChanged(); + const sizeChanged = this.properties.canvasWidth != this._prevWidth + || this.properties.canvasHeight != this._prevHeight + || this.properties.canvasImageURL != this._prevImageURL + + this.sizeChanged.set(sizeChanged); + + this._prevWidth = this.properties.canvasWidth; + this._prevHeight = this.properties.canvasHeight; + this._prevImageURL = this.properties.canvasImageURL; + + // this.notifyPropsChanged(); return value } + private updateInputType() { + const inputType = this.properties.inputType; + if (inputType === "image" && this.inputs[1]?.name === "image") + return + else if (inputType === "size" && this.inputs[1]?.name === "width") + return + + for (let index = this.inputs.length - 1; index >= 1; index--) { + if (this.inputs[index].type !== BuiltInSlotType.ACTION) { + this.removeInput(index); + } + } + + if (inputType === "image") { + this.addInput("image", "COMFYBOX_IMAGE"); + } + else if (inputType === "size") { + this.addInput("width", "number") + this.addInput("height", "number") + } + } + override parseValue(param: any): BoundingBox[] { if (param == null || this.properties.regionCount <= 0) return [] diff --git a/src/lib/widgets/MultiRegionWidget.svelte b/src/lib/widgets/MultiRegionWidget.svelte index 0fde6ea..37f23c8 100644 --- a/src/lib/widgets/MultiRegionWidget.svelte +++ b/src/lib/widgets/MultiRegionWidget.svelte @@ -12,7 +12,7 @@ import { Chart as ChartIcon } from "@gradio/icons"; import { Range } from "@gradio/form"; import { writable, type Writable } from "svelte/store"; - import { generateBlankCanvas, loadImage } from "./utils"; + import { generateBlankCanvas, generateImageCanvas, loadImage } from "./utils"; import { clamp } from "$lib/utils"; import Row from "$lib/components/gradio/app/Row.svelte"; import Column from "$lib/components/gradio/app/Column.svelte"; @@ -49,6 +49,7 @@ const COLOR_MAP: [string, string][] = [ let regionsChanged: Writable = writable(false); let propsChanged: Writable = writable(0); let selectedIndex: number = 0; + let imageOpacity: number = 1; $: widget && setNodeValue(widget); @@ -59,6 +60,8 @@ const COLOR_MAP: [string, string][] = [ propsChanged = node.propsChanged; sizeChanged = node.sizeChanged; regionsChanged = node.regionsChanged; + + updateImageAndDBoxes(); } }; @@ -88,7 +91,7 @@ const COLOR_MAP: [string, string][] = [ if (_node != null && imageElem != null && imageContainer != null) { selectedIndex = clamp(selectedIndex, 0, bboxes.length - 1); - await updateImage(_node.properties.canvasWidth, _node.properties.canvasHeight); + await updateImage(_node.properties.canvasWidth, _node.properties.canvasHeight, _node.properties.canvasImageURL); return bboxes.map((b, i) => displayBoundingBox(b, i, imageElem)) } else { @@ -96,8 +99,11 @@ const COLOR_MAP: [string, string][] = [ } } - $: if (node != null && $sizeChanged) { - updateImage(node.properties.canvasWidth, node.properties.canvasHeight) + async function updateImageAndDBoxes() { + if (node == null) + return; + + return updateImage(node.properties.canvasWidth, node.properties.canvasHeight, node.properties.canvasImageURL) .then(() => { return recreateDisplayBoxes() }) @@ -106,16 +112,12 @@ const COLOR_MAP: [string, string][] = [ }) } + $: if (node != null && $sizeChanged) { + updateImageAndDBoxes(); + } + onMount(async () => { - if (node) { - updateImage(node.properties.canvasWidth, node.properties.canvasHeight) - .then(() => { - return recreateDisplayBoxes() - }) - .then(dbs => { - displayBoxes = dbs; - }) - } + await updateImageAndDBoxes(); }) $: if ($regionsChanged) { @@ -123,17 +125,25 @@ const COLOR_MAP: [string, string][] = [ recreateDisplayBoxes(node, $nodeValue).then(dbs => displayBoxes = dbs); } - async function updateImage(width: number, height: number) { + let hasImage = false; + + async function updateImage(width: number, height: number, imageURL: string | null) { showWidget = width > 0 && height > 0; - const blank = generateBlankCanvas(width, height, "transparent"); - const url = blank.toDataURL(); - const newImg = await loadImage(url); + hasImage = imageURL != null + + if (imageURL == null) { + const blank = generateBlankCanvas(width, height, "transparent"); + imageURL = blank.toDataURL(); + } + + const newImg = await loadImage(imageURL); newImg.classList.add("regions-image"); if (imageContainer != null) { imageContainer.replaceChildren(newImg) } imageElem = newImg; imageElem.style.border = `${BORDER_SIZE_PX}px solid var(--border-color-primary)`; + $sizeChanged = false; } @@ -177,10 +187,10 @@ const COLOR_MAP: [string, string][] = [ const [borderColor, bgColor] = COLOR_MAP[index % COLOR_MAP.length] out ||= {} as DisplayBoundingBox - out.xPx= xDiv; - out.yPx= yDiv; - out.widthPx= wDiv; - out.heightPx= hDiv; + out.xPx = xDiv; + out.yPx = yDiv; + out.widthPx = wDiv; + out.heightPx = hDiv; out.warnLargeSize = warnLargeSize; out.bgColor = bgColor out.borderColor = borderColor @@ -449,7 +459,7 @@ const COLOR_MAP: [string, string][] = [ -{#key $propsChanged} +{#key node?.properties.canvasWidth} {#if widget?.attrs.title} {@const label = widget.attrs.title} @@ -462,10 +472,10 @@ const COLOR_MAP: [string, string][] = [ {/if} {#if showWidget} {@const selectedBBox = $nodeValue[selectedIndex]} - {@const selectedDBox = displayBoxes[selectedIndex]}
-
- +
+
{#each displayBoxes as dBox, i} @@ -518,6 +528,12 @@ const COLOR_MAP: [string, string][] = [ on:release={updateValue} /> + {#if hasImage} + + + + {/if} {/if} {:else}