Region widget fixes

This commit is contained in:
space-nuko
2023-05-23 14:28:13 -05:00
parent 631029edf7
commit e3c73ce18a
3 changed files with 135 additions and 46 deletions

View File

@@ -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 MultiRegionWidget from "$lib/widgets/MultiRegionWidget.svelte";
import ComfyWidgetNode, { type ComfyWidgetProperties } from "./ComfyWidgetNode"; 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"; import { writable, type Writable } from "svelte/store";
/* x, y, width, height, all in range 0.0 - 1.0 */ /* x, y, width, height, all in range 0.0 - 1.0 */
@@ -16,6 +16,7 @@ export interface ComfyMultiRegionProperties extends ComfyWidgetProperties {
regionCount: number, regionCount: number,
canvasWidth: number, canvasWidth: number,
canvasHeight: number, canvasHeight: number,
canvasImageURL: string | null,
inputType: "size" | "image" inputType: "size" | "image"
} }
@@ -28,16 +29,13 @@ export default class ComfyMultiRegionNode extends ComfyWidgetNode<BoundingBox[]>
regionCount: 1, regionCount: 1,
canvasWidth: 512, canvasWidth: 512,
canvasHeight: 512, canvasHeight: 512,
canvasImageURL: null,
inputType: "size" inputType: "size"
} }
static slotLayout: SlotLayout = { static slotLayout: SlotLayout = {
inputs: [ inputs: [
{ name: "store", type: BuiltInSlotType.ACTION }, { name: "store", type: BuiltInSlotType.ACTION },
// dynamic inputs, may be removed later
{ name: "width", type: "number" },
{ name: "height", type: "number" },
], ],
outputs: [ outputs: [
{ name: "changed", type: BuiltInSlotType.EVENT }, { name: "changed", type: BuiltInSlotType.EVENT },
@@ -56,6 +54,24 @@ export default class ComfyMultiRegionNode extends ComfyWidgetNode<BoundingBox[]>
sizeChanged: Writable<boolean> = writable(true); sizeChanged: Writable<boolean> = writable(true);
regionsChanged: Writable<boolean> = writable(true); regionsChanged: Writable<boolean> = 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) { override onPropertyChanged(property: any, value: any) {
if (property === "regionCount") { if (property === "regionCount") {
this.updateRegions() this.updateRegions()
@@ -63,19 +79,38 @@ export default class ComfyMultiRegionNode extends ComfyWidgetNode<BoundingBox[]>
else if (property === "width" || property === "height") { else if (property === "width" || property === "height") {
this.updateSize(); this.updateSize();
} }
else if (property === "inputType") {
this.updateInputType();
} }
constructor(name?: string) {
super(name, [[...DEFAULT_BBOX]])
} }
override onExecute() { override onExecute() {
let width = this.getInputData(1) || 0 let width = 0;
let height = this.getInputData(2) || 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.canvasWidth = width;
this.properties.canvasHeight = height; this.properties.canvasHeight = height;
this.properties.canvasImageURL = imageURL
this.updateSize(); this.updateSize();
} }
@@ -86,12 +121,12 @@ export default class ComfyMultiRegionNode extends ComfyWidgetNode<BoundingBox[]>
if (bbox != null) { if (bbox != null) {
const output = this.outputs[index + 1] // + changed slot const output = this.outputs[index + 1] // + changed slot
if (output != null) { 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[0] = bbox[0] * this.properties.canvasWidth
data[1] = bbox[1] * this.properties.canvasHeight data[1] = bbox[1] * this.properties.canvasHeight
data[2] = bbox[2] * this.properties.canvasWidth data[2] = bbox[2] * this.properties.canvasWidth
data[3] = bbox[3] * this.properties.canvasHeight 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<BoundingBox[]>
private updateRegions() { private updateRegions() {
this.properties.regionCount = Math.max(this.properties.regionCount, 0); 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) { if (this.outputs[index].type !== BuiltInSlotType.EVENT) {
this.removeOutput(index); 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.addOutput(`region${index + 1}`, "COMFYBOX_REGION")
} }
this.regionsChanged.set(true); this.regionsChanged.set(true);
this.notifyPropsChanged(); // this.notifyPropsChanged();
this.setValue(this.getValue()) this.setValue(this.getValue())
} }
private _prevWidth = null
private _prevHeight = null
private _prevImageURL = null
private updateSize(value?: BoundingBox[]): BoundingBox[] { private updateSize(value?: BoundingBox[]): BoundingBox[] {
this.properties.canvasWidth = Math.max(this.properties.canvasWidth, 0); this.properties.canvasWidth = Math.max(this.properties.canvasWidth, 0);
this.properties.canvasHeight = Math.max(this.properties.canvasHeight, 0); this.properties.canvasHeight = Math.max(this.properties.canvasHeight, 0);
@@ -129,12 +171,43 @@ export default class ComfyMultiRegionNode extends ComfyWidgetNode<BoundingBox[]>
bbox[3] = clamp(bbox[3], 0, 1 - bbox[2]) bbox[3] = clamp(bbox[3], 0, 1 - bbox[2])
} }
this.sizeChanged.set(true); const sizeChanged = this.properties.canvasWidth != this._prevWidth
this.notifyPropsChanged(); || 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 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[] { override parseValue(param: any): BoundingBox[] {
if (param == null || this.properties.regionCount <= 0) if (param == null || this.properties.regionCount <= 0)
return [] return []

View File

@@ -12,7 +12,7 @@
import { Chart as ChartIcon } from "@gradio/icons"; import { Chart as ChartIcon } from "@gradio/icons";
import { Range } from "@gradio/form"; import { Range } from "@gradio/form";
import { writable, type Writable } from "svelte/store"; import { writable, type Writable } from "svelte/store";
import { generateBlankCanvas, loadImage } from "./utils"; import { generateBlankCanvas, generateImageCanvas, loadImage } from "./utils";
import { clamp } from "$lib/utils"; import { clamp } from "$lib/utils";
import Row from "$lib/components/gradio/app/Row.svelte"; import Row from "$lib/components/gradio/app/Row.svelte";
import Column from "$lib/components/gradio/app/Column.svelte"; import Column from "$lib/components/gradio/app/Column.svelte";
@@ -49,6 +49,7 @@ const COLOR_MAP: [string, string][] = [
let regionsChanged: Writable<boolean> = writable(false); let regionsChanged: Writable<boolean> = writable(false);
let propsChanged: Writable<number> = writable(0); let propsChanged: Writable<number> = writable(0);
let selectedIndex: number = 0; let selectedIndex: number = 0;
let imageOpacity: number = 1;
$: widget && setNodeValue(widget); $: widget && setNodeValue(widget);
@@ -59,6 +60,8 @@ const COLOR_MAP: [string, string][] = [
propsChanged = node.propsChanged; propsChanged = node.propsChanged;
sizeChanged = node.sizeChanged; sizeChanged = node.sizeChanged;
regionsChanged = node.regionsChanged; regionsChanged = node.regionsChanged;
updateImageAndDBoxes();
} }
}; };
@@ -88,7 +91,7 @@ const COLOR_MAP: [string, string][] = [
if (_node != null && imageElem != null && imageContainer != null) { if (_node != null && imageElem != null && imageContainer != null) {
selectedIndex = clamp(selectedIndex, 0, bboxes.length - 1); 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)) return bboxes.map((b, i) => displayBoundingBox(b, i, imageElem))
} }
else { else {
@@ -96,8 +99,11 @@ const COLOR_MAP: [string, string][] = [
} }
} }
$: if (node != null && $sizeChanged) { async function updateImageAndDBoxes() {
updateImage(node.properties.canvasWidth, node.properties.canvasHeight) if (node == null)
return;
return updateImage(node.properties.canvasWidth, node.properties.canvasHeight, node.properties.canvasImageURL)
.then(() => { .then(() => {
return recreateDisplayBoxes() return recreateDisplayBoxes()
}) })
@@ -106,16 +112,12 @@ const COLOR_MAP: [string, string][] = [
}) })
} }
onMount(async () => { $: if (node != null && $sizeChanged) {
if (node) { updateImageAndDBoxes();
updateImage(node.properties.canvasWidth, node.properties.canvasHeight)
.then(() => {
return recreateDisplayBoxes()
})
.then(dbs => {
displayBoxes = dbs;
})
} }
onMount(async () => {
await updateImageAndDBoxes();
}) })
$: if ($regionsChanged) { $: if ($regionsChanged) {
@@ -123,17 +125,25 @@ const COLOR_MAP: [string, string][] = [
recreateDisplayBoxes(node, $nodeValue).then(dbs => displayBoxes = dbs); 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; showWidget = width > 0 && height > 0;
hasImage = imageURL != null
if (imageURL == null) {
const blank = generateBlankCanvas(width, height, "transparent"); const blank = generateBlankCanvas(width, height, "transparent");
const url = blank.toDataURL(); imageURL = blank.toDataURL();
const newImg = await loadImage(url); }
const newImg = await loadImage(imageURL);
newImg.classList.add("regions-image"); newImg.classList.add("regions-image");
if (imageContainer != null) { if (imageContainer != null) {
imageContainer.replaceChildren(newImg) imageContainer.replaceChildren(newImg)
} }
imageElem = newImg; imageElem = newImg;
imageElem.style.border = `${BORDER_SIZE_PX}px solid var(--border-color-primary)`; imageElem.style.border = `${BORDER_SIZE_PX}px solid var(--border-color-primary)`;
$sizeChanged = false; $sizeChanged = false;
} }
@@ -449,7 +459,7 @@ const COLOR_MAP: [string, string][] = [
<svelte:window on:resize={onResize}/> <svelte:window on:resize={onResize}/>
{#key $propsChanged} {#key node?.properties.canvasWidth}
<Block> <Block>
{#if widget?.attrs.title} {#if widget?.attrs.title}
{@const label = widget.attrs.title} {@const label = widget.attrs.title}
@@ -462,9 +472,9 @@ const COLOR_MAP: [string, string][] = [
{/if} {/if}
{#if showWidget} {#if showWidget}
{@const selectedBBox = $nodeValue[selectedIndex]} {@const selectedBBox = $nodeValue[selectedIndex]}
{@const selectedDBox = displayBoxes[selectedIndex]}
<div class="regions-container"> <div class="regions-container">
<div bind:this={imageContainer} class="regions-image-container"> <div bind:this={imageContainer} class="regions-image-container"
style:opacity="{hasImage ? imageOpacity * 100 : 100}%">
<img bind:this={imageElem} class="regions-image" /> <img bind:this={imageElem} class="regions-image" />
</div> </div>
<div class="regions"> <div class="regions">
@@ -518,6 +528,12 @@ const COLOR_MAP: [string, string][] = [
on:release={updateValue} on:release={updateValue}
/> />
</Row> </Row>
{#if hasImage}
<Row>
<Range label="Image Opacity" bind:value={imageOpacity}
show_label={true} minimum={0.0} maximum={1.0} step={0.01}/>
</Row>
{/if}
</Block> </Block>
{/if} {/if}
{:else} {:else}