This commit is contained in:
space-nuko
2023-05-02 14:58:02 -07:00
parent 5b4254c054
commit 890c839b4d
8 changed files with 202 additions and 114 deletions

View File

@@ -1,5 +1,6 @@
import type { INodeInputSlot } from "@litegraph-ts/core"; import type { INodeInputSlot } from "@litegraph-ts/core";
// TODO generalize
export type ComfyInputConfig = { export type ComfyInputConfig = {
min?: number, min?: number,
max?: number, max?: number,

View File

@@ -110,10 +110,10 @@
app.eventBus.on("configured", nodeState.configureFinished); app.eventBus.on("configured", nodeState.configureFinished);
app.eventBus.on("cleared", nodeState.clear); app.eventBus.on("cleared", nodeState.clear);
// app.eventBus.on("nodeAdded", layoutState.nodeAdded); app.eventBus.on("nodeAdded", layoutState.nodeAdded);
// app.eventBus.on("nodeRemoved", layoutState.nodeRemoved); app.eventBus.on("nodeRemoved", layoutState.nodeRemoved);
// app.eventBus.on("configured", layoutState.configureFinished); app.eventBus.on("configured", layoutState.configureFinished);
// app.eventBus.on("cleared", layoutState.clear); app.eventBus.on("cleared", layoutState.clear);
app.eventBus.on("autosave", doAutosave); app.eventBus.on("autosave", doAutosave);
app.eventBus.on("restored", doRestore); app.eventBus.on("restored", doRestore);

View File

@@ -27,7 +27,6 @@
} }
else if (dragItem.type === "widget") { else if (dragItem.type === "widget") {
widget = dragItem as WidgetLayout; widget = dragItem as WidgetLayout;
widgetState = nodeState.findWidgetByName(widget.nodeId, widget.widgetName)
container = null; container = null;
} }
@@ -47,9 +46,9 @@
{:else if widget} {:else if widget}
<div class="widget" class:widget-edit-outline={$uiState.uiEditMode === "widgets" && zIndex > 1} <div class="widget" class:widget-edit-outline={$uiState.uiEditMode === "widgets" && zIndex > 1}
class:selected={$uiState.uiEditMode !== "disabled" && $layoutState.currentSelection.includes(widget.id)} class:selected={$uiState.uiEditMode !== "disabled" && $layoutState.currentSelection.includes(widget.id)}
class:is-executing={$queueState.runningNodeId && $queueState.runningNodeId == widget.attrs.associatedNode} class:is-executing={$queueState.runningNodeId && $queueState.runningNodeId == widget.node.id}
> >
<svelte:component this={getComponentForWidgetState(widgetState)} item={widgetState} /> <svelte:component this={widget.node.svelteComponentType} node={widget.node} />
</div> </div>
{#if showHandles} {#if showHandles}
<div class="handle handle-widget" style="z-index: {zIndex+100}" data-drag-item-id={widget.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/> <div class="handle handle-widget" style="z-index: {zIndex+100}" data-drag-item-id={widget.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/>

View File

@@ -6,9 +6,10 @@ import TextWidget from "$lib/widgets/TextWidget.svelte";
import type { SvelteComponentDev } from "svelte/internal"; import type { SvelteComponentDev } from "svelte/internal";
import { ComfyWidgets } from "$lib/widgets"; import { ComfyWidgets } from "$lib/widgets";
import { Watch } from "@litegraph-ts/nodes-basic"; import { Watch } from "@litegraph-ts/nodes-basic";
import type IComfyInputSlot from "$lib/IComfyInputSlot";
import { writable, type Unsubscriber, type Writable } from "svelte/store";
export interface ComfyWidgetProperties extends Record<string, any> { export interface ComfyWidgetProperties extends Record<string, any> {
value: any,
} }
/* /*
@@ -16,14 +17,20 @@ export interface ComfyWidgetProperties extends Record<string, any> {
* widget is changed, the value of the first output in the node is updated * widget is changed, the value of the first output in the node is updated
* in the litegraph instance. * in the litegraph instance.
*/ */
export abstract class ComfyWidgetNode extends ComfyGraphNode { export abstract class ComfyWidgetNode<T> extends ComfyGraphNode {
abstract properties: ComfyWidgetProperties; abstract properties: ComfyWidgetProperties;
value: Writable<T>
unsubscribe: Unsubscriber;
/** Svelte class for the frontend logic */ /** Svelte class for the frontend logic */
abstract svelteComponentType: typeof SvelteComponentDev abstract svelteComponentType: typeof SvelteComponentDev
/** Compatible litegraph widget types that can be connected to this node */ /** Compatible litegraph widget types that can be connected to this node */
abstract inputWidgetTypes: string[] abstract inputWidgetTypes: string[]
/** If false, user manually set min/max/step, and should not be autoinherited from connected input */
autoConfig: boolean = true;
override isBackendNode = false; override isBackendNode = false;
override serialize_widgets = true; override serialize_widgets = true;
@@ -31,9 +38,10 @@ export abstract class ComfyWidgetNode extends ComfyGraphNode {
override size: Vector2 = [60, 40]; override size: Vector2 = [60, 40];
constructor(name?: string) { constructor(name?: string, value: T) {
super(name)
const color = LGraphCanvas.node_colors["blue"] const color = LGraphCanvas.node_colors["blue"]
super(name)
this.value = writable(value)
this.color ||= color.color this.color ||= color.color
this.bgColor ||= color.bgColor this.bgColor ||= color.bgColor
this.displayWidget = this.addWidget<ITextWidget>( this.displayWidget = this.addWidget<ITextWidget>(
@@ -41,16 +49,15 @@ export abstract class ComfyWidgetNode extends ComfyGraphNode {
"Value", "Value",
"" ""
); );
this.unsubscribe = this.value.subscribe(this.onValueUpdated.bind(this))
} }
override onPropertyChanged(name: string, value: any) { private onValueUpdated(value: any) {
if (name == "value") { this.displayWidget.value = Watch.toString(value)
this.displayWidget.value = Watch.toString(value)
}
} }
setValue(value: any) { setValue(value: any) {
this.setProperty("value", value) this.value.set(value)
} }
override onExecute() { override onExecute() {
@@ -60,15 +67,54 @@ export abstract class ComfyWidgetNode extends ComfyGraphNode {
// TODO send event to linked nodes // TODO send event to linked nodes
} }
onConnectOutput(
outputIndex: number,
inputType: INodeInputSlot["type"],
input: INodeInputSlot,
inputNode: LGraphNode,
inputIndex: number
): boolean {
if (this.autoConfig) {
// Copy properties from default config in input slot
if ("config" in input) {
const comfyInput = input as IComfyInputSlot;
for (const key in comfyInput.config)
this.setProperty(key, comfyInput.config[key])
}
console.debug("Property copy", input, this.properties)
}
return true;
}
clampConfig() {
for (const link of this.getOutputLinks(0)) {
const node = this.graph._nodes_by_id[link.target_id]
if (node) {
const input = node.inputs[link.target_slot]
if (input && "config" in input)
this.clampOneConfig(input as IComfyInputSlot)
}
}
}
clampOneConfig(input: IComfyInputSlot) {}
} }
export interface ComfySliderProperties extends ComfyWidgetProperties { export interface ComfySliderProperties extends ComfyWidgetProperties {
value: number min: number,
max: number,
step: number,
precision: number
} }
export class ComfySliderNode extends ComfyWidgetNode { export class ComfySliderNode extends ComfyWidgetNode<number> {
override properties: ComfySliderProperties = { override properties: ComfySliderProperties = {
value: 0 min: 0,
max: 10,
step: 1,
precision: 1
} }
override svelteComponentType = RangeWidget override svelteComponentType = RangeWidget
@@ -79,6 +125,17 @@ export class ComfySliderNode extends ComfyWidgetNode {
{ name: "value", type: "number" } { name: "value", type: "number" }
] ]
} }
constructor(name?: string) {
super(name, 0)
}
override clampOneConfig(input: IComfyInputSlot) {
this.setProperty("min", Math.max(this.properties.min, input.config.min))
this.setProperty("max", Math.max(this.properties.max, input.config.max))
this.setProperty("step", Math.max(this.properties.step, input.config.step))
this.setProperty("value", Math.max(Math.min(this.properties.value, this.properties.max), this.properties.min))
}
} }
LiteGraph.registerNodeType({ LiteGraph.registerNodeType({
@@ -89,14 +146,12 @@ LiteGraph.registerNodeType({
}) })
export interface ComfyComboProperties extends ComfyWidgetProperties { export interface ComfyComboProperties extends ComfyWidgetProperties {
options: string[], options: string[]
value: string
} }
export class ComfyComboNode extends ComfyWidgetNode { export class ComfyComboNode extends ComfyWidgetNode<string> {
override properties: ComfyComboProperties = { override properties: ComfyComboProperties = {
options: ["*"], options: ["*"]
value: "*"
} }
static slotLayout: SlotLayout = { static slotLayout: SlotLayout = {
@@ -107,6 +162,43 @@ export class ComfyComboNode extends ComfyWidgetNode {
override svelteComponentType = ComboWidget override svelteComponentType = ComboWidget
override inputWidgetTypes = ["combo", "enum"] override inputWidgetTypes = ["combo", "enum"]
constructor(name?: string) {
super(name, "*")
}
onConnectOutput(
outputIndex: number,
inputType: INodeInputSlot["type"],
input: INodeInputSlot,
inputNode: LGraphNode,
inputIndex: number
): boolean {
if (!super.onConnectOutput(outputIndex, inputType, input, inputNode, inputIndex))
return false;
const thisProps = this.properties;
const otherProps = inputNode.properties;
// Ensure combo options match
if (!(otherProps.options instanceof Array))
return false;
if (otherProps.options.length !== thisProps.options.length)
return false;
if (otherProps.find((v, i) => thisProps[i] !== v))
return false;
return true;
}
override clampOneConfig(input: IComfyInputSlot) {
if (!(this.properties.value in input.config.values)) {
if (input.config.values.length === 0)
this.setProperty("value", "")
else
this.setProperty("value", input.config.values[0])
}
}
} }
LiteGraph.registerNodeType({ LiteGraph.registerNodeType({
@@ -117,12 +209,12 @@ LiteGraph.registerNodeType({
}) })
export interface ComfyTextProperties extends ComfyWidgetProperties { export interface ComfyTextProperties extends ComfyWidgetProperties {
value: string multiline: boolean;
} }
export class ComfyTextNode extends ComfyWidgetNode { export class ComfyTextNode extends ComfyWidgetNode<string> {
override properties: ComfyTextProperties = { override properties: ComfyTextProperties = {
value: "" multiline: false
} }
static slotLayout: SlotLayout = { static slotLayout: SlotLayout = {
@@ -133,6 +225,10 @@ export class ComfyTextNode extends ComfyWidgetNode {
override svelteComponentType = TextWidget override svelteComponentType = TextWidget
override inputWidgetTypes = ["text"] override inputWidgetTypes = ["text"]
constructor(name?: string) {
super(name, "")
}
} }
LiteGraph.registerNodeType({ LiteGraph.registerNodeType({

View File

@@ -5,6 +5,7 @@ import type { LGraphNode, IWidget, LGraph } from "@litegraph-ts/core"
import nodeState from "$lib/state/nodeState"; import nodeState from "$lib/state/nodeState";
import type { NodeStateStore } from './nodeState'; import type { NodeStateStore } from './nodeState';
import { dndzone, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action'; import { dndzone, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
import type { ComfyWidgetNode } from '$lib/nodes';
type DragItemEntry = { type DragItemEntry = {
dragItem: IDragItem, dragItem: IDragItem,
@@ -59,16 +60,6 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
editable: true, editable: true,
}, },
] ]
},
{
categoryName: "behavior",
specs: [
{
name: "associatedNode",
type: "number",
editable: false,
},
]
} }
]; ];
export { ALL_ATTRIBUTES }; export { ALL_ATTRIBUTES };
@@ -77,8 +68,7 @@ export type Attributes = {
direction: string, direction: string,
title: string, title: string,
showTitle: boolean, showTitle: boolean,
classes: string, classes: string
associatedNode: number | null
} }
export interface IDragItem { export interface IDragItem {
@@ -94,8 +84,7 @@ export interface ContainerLayout extends IDragItem {
export interface WidgetLayout extends IDragItem { export interface WidgetLayout extends IDragItem {
type: "widget", type: "widget",
nodeId: number, node: ComfyWidgetNode
widgetName: string
} }
type DragItemID = string; type DragItemID = string;
@@ -121,7 +110,8 @@ const store: Writable<LayoutState> = writable({
allItems: {}, allItems: {},
currentId: 0, currentId: 0,
currentSelection: [], currentSelection: [],
isMenuOpen: false isMenuOpen: false,
isConfiguring: true
}) })
addContainer(null, { direction: "horizontal", showTitle: false }); addContainer(null, { direction: "horizontal", showTitle: false });
@@ -155,7 +145,6 @@ function addContainer(parentId: DragItemID | null, attrs: Partial<Attributes> =
showTitle: true, showTitle: true,
direction: "vertical", direction: "vertical",
classes: "", classes: "",
associatedNode: null,
...attrs ...attrs
} }
} }
@@ -173,31 +162,25 @@ function addContainer(parentId: DragItemID | null, attrs: Partial<Attributes> =
return dragItem; return dragItem;
} }
function addWidget(parentId: DragItemID, node: LGraphNode, widget: IWidget<any, any>, attrs: Partial<Attributes> = {}, index: number = -1): WidgetLayout { function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partial<Attributes> = {}, index: number = -1): WidgetLayout {
const state = get(store); const state = get(store);
const widgetName = "Widget"
const dragItem: WidgetLayout = { const dragItem: WidgetLayout = {
type: "widget", type: "widget",
id: `${state.currentId++}`, id: `${state.currentId++}`,
nodeId: node.id, node: node,
widgetName: widget.name, // TODO name and displayName
attrs: { attrs: {
title: widget.name, title: widgetName,
showTitle: true, showTitle: true,
direction: "horizontal", direction: "horizontal",
classes: "", classes: "",
associatedNode: null,
...attrs ...attrs
} }
} }
const parent = state.allItems[parentId] const parentEntry = state.allItems[parent.id]
const entry: DragItemEntry = { dragItem, children: [], parent: parent.dragItem }; const entry: DragItemEntry = { dragItem, children: [], parent: parentEntry.dragItem };
state.allItems[dragItem.id] = entry; state.allItems[dragItem.id] = entry;
parent.children ||= [] moveItem(dragItem, parent)
if (index !== -1)
parent.children.splice(index, 0, dragItem)
else
parent.children.push(dragItem)
store.set(state)
return dragItem; return dragItem;
} }
@@ -220,7 +203,17 @@ function nodeAdded(node: LGraphNode) {
return; return;
const parent = findDefaultContainerForInsertion(); const parent = findDefaultContainerForInsertion();
// Add default node panel containing all widgets
// Two cases where we want to add nodes:
// 1. User adds a new UI node, so we should instantiate its widget in the frontend.
// 2. User adds a node with inputs that can be filled by frontend widgets.
// Depending on config, this means we should instantiate default UI nodes connected to those inputs.
if ("svelteComponentType" in node) {
addWidget(parent, node as ComfyWidgetNode);
}
// Add default node panel with all widgets autoinstantiated
// if (node.widgets && node.widgets.length > 0) { // if (node.widgets && node.widgets.length > 0) {
// const container = addContainer(parent.id, { title: node.title, direction: "vertical", associatedNode: node.id }); // const container = addContainer(parent.id, { title: node.title, direction: "vertical", associatedNode: node.id });
// for (const widget of node.widgets) { // for (const widget of node.widgets) {
@@ -248,32 +241,12 @@ function nodeRemoved(node: LGraphNode) {
console.debug("[layoutState] nodeRemoved", node) console.debug("[layoutState] nodeRemoved", node)
// Remove widgets bound to the node
let del = Object.entries(state.allItems).filter(pair => let del = Object.entries(state.allItems).filter(pair =>
pair[1].dragItem.type === "widget" pair[1].dragItem.type === "widget"
&& pair[1].dragItem.attrs.associatedNode === node.id) && (pair[1].dragItem as WidgetLayout).node.id === node.id)
for (const item of del) {
const [id, dragItem] = item;
console.debug("[layoutState] Remove widget", id, state.allItems[id])
removeEntry(state, id)
}
const isAssociatedContainer = (dragItem: IDragItem) => for (const pair of del) {
dragItem.type === "container" const [id, dragItem] = pair;
&& dragItem.attrs.associatedNode === node.id;
let delContainers = []
// Remove widget from all children lists
// TODO just use parent.children
for (const entry of Object.values(state.allItems)) {
if (entry.children?.length === 0 && isAssociatedContainer(entry.dragItem))
delContainers.push(entry.dragItem.id)
}
// Remove empty containers bound to the node
for (const id of delContainers) {
console.debug("[layoutState] Remove container", id, state.allItems[id])
removeEntry(state, id) removeEntry(state, id)
} }
@@ -290,7 +263,7 @@ function configureFinished(graph: LGraph) {
const left = addContainer(state.root.id, { direction: "vertical", showTitle: false }); const left = addContainer(state.root.id, { direction: "vertical", showTitle: false });
const right = addContainer(state.root.id, { direction: "vertical", showTitle: false }); const right = addContainer(state.root.id, { direction: "vertical", showTitle: false });
for (const node of graph.computeExecutionOrder(false, null)) { for (const node of graph._nodes_in_order) {
nodeAdded(node) nodeAdded(node)
} }

View File

@@ -2,32 +2,39 @@
import type { WidgetDrawState, WidgetUIState, WidgetUIStateStore } from "$lib/stores/nodeState"; import type { WidgetDrawState, WidgetUIState, WidgetUIStateStore } from "$lib/stores/nodeState";
import { BlockTitle } from "@gradio/atoms"; import { BlockTitle } from "@gradio/atoms";
import { Dropdown } from "@gradio/form"; import { Dropdown } from "@gradio/form";
import { get } from "svelte/store";
import Select from 'svelte-select'; import Select from 'svelte-select';
export let item: WidgetUIState | null = null; import type { ComfyComboNode } from "$lib/nodes/index";
import { type WidgetLayout } from "$lib/stores/layoutState";
import { get, type Writable } from "svelte/store";
export let widget: WidgetLayout | null = null;
let node: ComfyComboNode | null = null;
let nodeValue: Writable<string> | null = null;
let itemValue: WidgetUIStateStore | null = null; let itemValue: WidgetUIStateStore | null = null;
let option: any; let option: any;
$: if(item) { $: if(widget) {
if (!itemValue) node = widget.node as ComfyComboNode
itemValue = item.value; nodeValue = node.value;
if (!option) updateOption(); // don't react on option
option = get(item.value);
}; };
function updateOption() {
option = get(nodeValue);
}
$: if (option && itemValue) { $: if (option && itemValue) {
$itemValue = option.value $itemValue = option.value
} }
</script> </script>
<div class="wrapper gr-combo"> <div class="wrapper gr-combo">
{#if item !== null && option !== undefined} {#if node !== null && option !== undefined}
<label> <label>
<BlockTitle show_label={true}>{item.widget.name}</BlockTitle> <BlockTitle show_label={true}>{widget.attrs.title}</BlockTitle>
<Select <Select
bind:value={option} bind:value={option}
bind:items={item.widget.options.values} bind:items={node.properties.values}
disabled={item.widget.options.values.length === 0} disabled={node.properties.values.length === 0}
clearable={false} clearable={false}
on:change on:change
on:select on:select

View File

@@ -1,35 +1,38 @@
<script lang="ts"> <script lang="ts">
import type { WidgetUIState, WidgetUIStateStore } from "$lib/stores/nodeState"; import type { ComfySliderNode } from "$lib/nodes/index";
import { type WidgetLayout } from "$lib/stores/layoutState";
import { Range } from "@gradio/form"; import { Range } from "@gradio/form";
import { get } from "svelte/store"; import { get, type Writable } from "svelte/store";
export let item: WidgetUIState | null = null; export let widget: WidgetLayout | null = null;
let itemValue: WidgetUIStateStore | null = null; let node: ComfySliderNode | null = null;
let nodeValue: Writable<number> | null = null;
let option: number | null = null; let option: number | null = null;
$: if (item) { $: if(widget) {
itemValue = item.value; node = widget.node
nodeValue = node.value;
updateOption(); // don't react on option updateOption(); // don't react on option
} };
function updateOption() { function updateOption() {
option = get(itemValue); option = get(nodeValue);
} }
function onRelease(e: Event) { function onRelease(e: Event) {
if (itemValue && option) { if (nodeValue && option) {
$itemValue = option $nodeValue = option
} }
} }
</script> </script>
<div class="wrapper gr-range"> <div class="wrapper gr-range">
{#if item !== null && option !== null} {#if node !== null && option !== null}
<Range <Range
bind:value={option} bind:value={option}
minimum={item.widget.options.min} minimum={node.properties.min}
maximum={item.widget.options.max} maximum={node.properties.max}
step={item.widget.options.step} step={node.properties.step}
label={item.widget.name} label={widget.attrs.title}
show_label={true} show_label={true}
on:release={onRelease} on:release={onRelease}
on:change on:change

View File

@@ -1,18 +1,27 @@
<script lang="ts"> <script lang="ts">
import type { WidgetUIState, WidgetUIStateStore } from "$lib/stores/nodeState"; import type { WidgetUIStateStore } from "$lib/stores/nodeState";
import { TextBox } from "@gradio/form"; import { TextBox } from "@gradio/form";
export let item: WidgetUIState | null = null; import type { ComfyComboNode } from "$lib/nodes/index";
import { type WidgetLayout } from "$lib/stores/layoutState";
import { get, type Writable } from "svelte/store";
export let widget: WidgetLayout | null = null;
let node: ComfyComboNode | null = null;
let nodeValue: Writable<string> | null = null;
let itemValue: WidgetUIStateStore | null = null; let itemValue: WidgetUIStateStore | null = null;
$: if (item) { itemValue = item.value; }
$: if(widget) {
node = widget.node as ComfyComboNode
nodeValue = node.value;
};
</script> </script>
<div class="wrapper gr-textbox"> <div class="wrapper gr-textbox">
{#if item !== null && itemValue !== null} {#if node !== null && nodeValue !== null}
<TextBox <TextBox
bind:value={$itemValue} bind:value={$itemValue}
label={item.widget.name} label={widget.attrs.title}
lines={item.widget.options.multiline ? 5 : 1} lines={node.properties.multiline ? 5 : 1}
max_lines={item.widget.options.multiline ? 5 : 1} max_lines={node.properties.multiline ? 5 : 1}
show_label={true} show_label={true}
on:change on:change
on:submit on:submit