Region widget fixes
This commit is contained in:
Submodule litegraph updated: 1a77c461ad...34b0f0c220
@@ -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 []
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user