Sliders and region breakout node
This commit is contained in:
@@ -592,6 +592,7 @@ export default class ComfyApp {
|
||||
|
||||
setColor("COMFYBOX_IMAGES", "rebeccapurple")
|
||||
setColor("COMFYBOX_IMAGE", "fuchsia")
|
||||
setColor("COMFYBOX_REGION", "salmon")
|
||||
setColor(BuiltInSlotType.EVENT, "lightseagreen")
|
||||
setColor(BuiltInSlotType.ACTION, "lightseagreen")
|
||||
}
|
||||
|
||||
@@ -151,5 +151,7 @@
|
||||
|
||||
.edit {
|
||||
border: 2px dashed var(--color-blue-400);
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
34
src/lib/nodes/ComfyRegionToCoordsNode.ts
Normal file
34
src/lib/nodes/ComfyRegionToCoordsNode.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { LiteGraph, type SlotLayout } from "@litegraph-ts/core";
|
||||
import ComfyGraphNode from "./ComfyGraphNode";
|
||||
|
||||
export default class ComfyRegionToCoordsNode extends ComfyGraphNode {
|
||||
static slotLayout: SlotLayout = {
|
||||
inputs: [
|
||||
{ name: "in", type: "COMFYBOX_REGION" },
|
||||
],
|
||||
outputs: [
|
||||
{ name: "x", type: "number" },
|
||||
{ name: "y", type: "number" },
|
||||
{ name: "width", type: "number" },
|
||||
{ name: "height", type: "number" },
|
||||
],
|
||||
}
|
||||
|
||||
override onExecute() {
|
||||
const value = this.getInputData(0);
|
||||
if (!Array.isArray(value))
|
||||
return;
|
||||
|
||||
this.setOutputData(0, value[0])
|
||||
this.setOutputData(1, value[1])
|
||||
this.setOutputData(2, value[2])
|
||||
this.setOutputData(3, value[3])
|
||||
}
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType({
|
||||
class: ComfyRegionToCoordsNode,
|
||||
title: "Comfy.RegionToCoords",
|
||||
desc: "Converts a COMFYBOX_REGION to four outputs of [x, y, width, height]",
|
||||
type: "utils/region_to_coords"
|
||||
})
|
||||
@@ -1,5 +1,3 @@
|
||||
import "$lib/nodes/ComfyGraphNode";
|
||||
|
||||
export { default as ComfyReroute } from "./ComfyReroute"
|
||||
export { default as ComfyPickFirstNode } from "./ComfyPickFirstNode"
|
||||
export { default as ComfyValueControl } from "./ComfyValueControl"
|
||||
@@ -8,3 +6,4 @@ export { default as ComfyTriggerNewEventNode } from "./ComfyTriggerNewEventNode"
|
||||
export { default as ComfyConfigureQueuePromptButton } from "./ComfyConfigureQueuePromptButton"
|
||||
export { default as ComfyPickImageNode } from "./ComfyPickImageNode"
|
||||
export { default as ComfyNoChangeEvent } from "./ComfyNoChangeEvent"
|
||||
export { default as ComfyRegionToCoordsNode } from "./ComfyRegionToCoordsNode"
|
||||
|
||||
@@ -43,10 +43,7 @@ export default class ComfyMultiRegionNode extends ComfyWidgetNode<BoundingBox[]>
|
||||
{ name: "changed", type: BuiltInSlotType.EVENT },
|
||||
|
||||
// dynamic outputs, may be removed later
|
||||
{ name: "x1", type: "number" },
|
||||
{ name: "y1", type: "number" },
|
||||
{ name: "w1", type: "number" },
|
||||
{ name: "h1", type: "number" },
|
||||
{ name: "region1", type: "COMFYBOX_REGION" },
|
||||
]
|
||||
}
|
||||
|
||||
@@ -77,24 +74,24 @@ export default class ComfyMultiRegionNode extends ComfyWidgetNode<BoundingBox[]>
|
||||
let height = this.getInputData(2) || 0
|
||||
|
||||
if (width != this.properties.canvasWidth || height != this.properties.canvasHeight) {
|
||||
console.warn("SIZCHANGE", width, height, this.properties.canvasWidth, this.properties.canvasHeight)
|
||||
this.properties.canvasWidth = width;
|
||||
this.properties.canvasHeight = height;
|
||||
this.sizeChanged.set(true);
|
||||
this.updateSize();
|
||||
}
|
||||
|
||||
const value = this.getValue();
|
||||
|
||||
for (let index = 0; index < this.properties.regionCount * 2; index += 2) {
|
||||
for (let index = 0; index < this.properties.regionCount; index++) {
|
||||
const bbox = value[index]
|
||||
if (bbox != null) {
|
||||
const xOutput = this.outputs[index + 1]
|
||||
if (xOutput != null) {
|
||||
this.setOutputData(index + 1, bbox[0] * this.properties.canvasWidth)
|
||||
this.setOutputData(index + 2, bbox[1] * this.properties.canvasHeight)
|
||||
this.setOutputData(index + 3, bbox[2] * this.properties.canvasWidth)
|
||||
this.setOutputData(index + 4, bbox[3] * this.properties.canvasHeight)
|
||||
const output = this.outputs[index + 1] // + changed slot
|
||||
if (output != null) {
|
||||
let data = this.getOutputData(index) || [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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,10 +107,7 @@ export default class ComfyMultiRegionNode extends ComfyWidgetNode<BoundingBox[]>
|
||||
}
|
||||
|
||||
for (let index = 0; index < this.properties.regionCount; index++) {
|
||||
this.addOutput(`x${index + 1}`, "number")
|
||||
this.addOutput(`y${index + 1}`, "number")
|
||||
this.addOutput(`w${index + 1}`, "number")
|
||||
this.addOutput(`h${index + 1}`, "number")
|
||||
this.addOutput(`region${index + 1}`, "COMFYBOX_REGION")
|
||||
}
|
||||
|
||||
this.regionsChanged.set(true);
|
||||
|
||||
@@ -9,10 +9,13 @@
|
||||
import type { BoundingBox } from "$lib/nodes/widgets/ComfyMultiRegionNode";
|
||||
import type { WidgetLayout } from "$lib/stores/layoutStates";
|
||||
import { Block, BlockLabel } from "@gradio/atoms";
|
||||
import { Chart as SquareIcon } from "@gradio/icons";
|
||||
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 { clamp } from "$lib/utils";
|
||||
import Row from "$lib/components/gradio/app/Row.svelte";
|
||||
import Column from "$lib/components/gradio/app/Column.svelte";
|
||||
|
||||
// ref: https://html-color.codes/
|
||||
const COLOR_MAP: [string, string][] = [
|
||||
@@ -50,7 +53,6 @@ const COLOR_MAP: [string, string][] = [
|
||||
$: widget && setNodeValue(widget);
|
||||
|
||||
function setNodeValue(widget: WidgetLayout) {
|
||||
console.error("SETNODEVALUE")
|
||||
if (widget) {
|
||||
node = widget.node as ComfyMultiRegionNode
|
||||
nodeValue = node.value;
|
||||
@@ -95,7 +97,6 @@ const COLOR_MAP: [string, string][] = [
|
||||
}
|
||||
|
||||
$: if (node != null && $sizeChanged) {
|
||||
console.warn("SIZCHANGEd")
|
||||
updateImage(node.properties.canvasWidth, node.properties.canvasHeight)
|
||||
.then(() => {
|
||||
return recreateDisplayBoxes()
|
||||
@@ -106,7 +107,15 @@ const COLOR_MAP: [string, string][] = [
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
displayBoxes = await recreateDisplayBoxes(node, $nodeValue);
|
||||
if (node) {
|
||||
updateImage(node.properties.canvasWidth, node.properties.canvasHeight)
|
||||
.then(() => {
|
||||
return recreateDisplayBoxes()
|
||||
})
|
||||
.then(dbs => {
|
||||
displayBoxes = dbs;
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
$: if ($regionsChanged) {
|
||||
@@ -116,7 +125,6 @@ const COLOR_MAP: [string, string][] = [
|
||||
|
||||
async function updateImage(width: number, height: number) {
|
||||
showWidget = width > 0 && height > 0;
|
||||
console.error("SHOW", showWidget, width, height)
|
||||
const blank = generateBlankCanvas(width, height, "transparent");
|
||||
const url = blank.toDataURL();
|
||||
const newImg = await loadImage(url);
|
||||
@@ -246,7 +254,6 @@ const COLOR_MAP: [string, string][] = [
|
||||
// Calculate viewport scale based on the current canvas size and the natural image size
|
||||
let vpScale = Math.min(imageElem.clientWidth / imageElem.naturalWidth, imageElem.clientHeight / imageElem.naturalHeight);
|
||||
let vpOffset = imageElem.getBoundingClientRect();
|
||||
console.warn(vpScale, vpOffset)
|
||||
|
||||
// Calculate scaled dimensions of the canvas
|
||||
let scaledX = imageElem.naturalWidth * vpScale;
|
||||
@@ -382,6 +389,62 @@ const COLOR_MAP: [string, string][] = [
|
||||
async function onResize() {
|
||||
displayBoxes = await recreateDisplayBoxes();
|
||||
}
|
||||
|
||||
function updateX(newX: number) {
|
||||
const bbox = $nodeValue[selectedIndex]
|
||||
const dbox = displayBoxes[selectedIndex]
|
||||
if (!bbox || !dbox)
|
||||
return
|
||||
|
||||
bbox[0] = newX
|
||||
displayBoxes[selectedIndex] = displayBoundingBox(bbox, selectedIndex, imageElem, dbox);
|
||||
}
|
||||
|
||||
function updateY(newY: number) {
|
||||
const bbox = $nodeValue[selectedIndex]
|
||||
const dbox = displayBoxes[selectedIndex]
|
||||
if (!bbox || !dbox)
|
||||
return
|
||||
|
||||
bbox[1] = newY
|
||||
displayBoxes[selectedIndex] = displayBoundingBox(bbox, selectedIndex, imageElem, dbox);
|
||||
}
|
||||
|
||||
function updateWidth(newWidth: number) {
|
||||
const bbox = $nodeValue[selectedIndex]
|
||||
const dbox = displayBoxes[selectedIndex]
|
||||
if (!bbox || !dbox)
|
||||
return
|
||||
|
||||
bbox[2] = newWidth
|
||||
displayBoxes[selectedIndex] = displayBoundingBox(bbox, selectedIndex, imageElem, dbox);
|
||||
}
|
||||
|
||||
function updateHeight(newHeight: number) {
|
||||
const bbox = $nodeValue[selectedIndex]
|
||||
const dbox = displayBoxes[selectedIndex]
|
||||
if (!bbox || !dbox)
|
||||
return
|
||||
|
||||
bbox[3] = newHeight
|
||||
displayBoxes[selectedIndex] = displayBoundingBox(bbox, selectedIndex, imageElem, dbox);
|
||||
}
|
||||
|
||||
function updateValue() {
|
||||
// Clamp regions
|
||||
const bbox = $nodeValue[selectedIndex]
|
||||
const dbox = displayBoxes[selectedIndex]
|
||||
if (bbox && dbox) {
|
||||
bbox[2] = clamp(bbox[2], 0, 1)
|
||||
bbox[3] = clamp(bbox[3], 0, 1)
|
||||
bbox[0] = clamp(bbox[0], 0, 1 - bbox[2])
|
||||
bbox[1] = clamp(bbox[1], 0, 1 - bbox[3])
|
||||
displayBoxes[selectedIndex] = displayBoundingBox(bbox, selectedIndex, imageElem, dbox);
|
||||
}
|
||||
|
||||
// Force reactivity after changing a bbox's internal values
|
||||
$nodeValue = $nodeValue
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:resize={onResize}/>
|
||||
@@ -393,11 +456,13 @@ const COLOR_MAP: [string, string][] = [
|
||||
<BlockLabel
|
||||
label={label}
|
||||
show_label={label != ""}
|
||||
Icon={SquareIcon}
|
||||
Icon={ChartIcon}
|
||||
float={label != ""}
|
||||
/>
|
||||
{/if}
|
||||
{#if showWidget}
|
||||
{@const selectedBBox = $nodeValue[selectedIndex]}
|
||||
{@const selectedDBox = displayBoxes[selectedIndex]}
|
||||
<div class="regions-container">
|
||||
<div bind:this={imageContainer} class="regions-image-container">
|
||||
<img bind:this={imageElem} class="regions-image"/>
|
||||
@@ -427,9 +492,37 @@ const COLOR_MAP: [string, string][] = [
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#if selectedBBox}
|
||||
<Block>
|
||||
<Row>
|
||||
<Range label="X" value={selectedBBox[0]}
|
||||
show_label={true} minimum={0.0} maximum={1.0} step={0.01}
|
||||
on:change={(e) => updateX(e.detail)}
|
||||
on:release={updateValue}
|
||||
/>
|
||||
<Range label="Width" value={selectedBBox[2]}
|
||||
show_label={true} minimum={0.0} maximum={1.0} step={0.01}
|
||||
on:change={(e) => updateWidth(e.detail)}
|
||||
on:release={updateValue}
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<Range label="Y" value={selectedBBox[1]}
|
||||
show_label={true} minimum={0.0} maximum={1.0} step={0.01}
|
||||
on:change={(e) => updateY(e.detail)}
|
||||
on:release={updateValue}
|
||||
/>
|
||||
<Range label="Height" value={selectedBBox[3]}
|
||||
show_label={true} minimum={0.0} maximum={1.0} step={0.01}
|
||||
on:change={(e) => updateHeight(e.detail)}
|
||||
on:release={updateValue}
|
||||
/>
|
||||
</Row>
|
||||
</Block>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="regions-empty">
|
||||
<span>(Empty canvas)</span>
|
||||
<span>(No regions)</span>
|
||||
</div>
|
||||
{/if}
|
||||
</Block>
|
||||
@@ -441,7 +534,10 @@ const COLOR_MAP: [string, string][] = [
|
||||
padding: 0;
|
||||
|
||||
.regions-image-container {
|
||||
display: flex;
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
border: 3px solid var(--input-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user