Toggle nodes/containers on and off by tags

This commit is contained in:
space-nuko
2023-05-07 21:29:36 -05:00
parent e7f4638093
commit 8e363cdd51
27 changed files with 529 additions and 149 deletions

View File

@@ -47,6 +47,7 @@
"@litegraph-ts/core": "workspace:*",
"@litegraph-ts/nodes-basic": "workspace:*",
"@litegraph-ts/nodes-events": "workspace:*",
"@litegraph-ts/nodes-logic": "workspace:*",
"@litegraph-ts/nodes-math": "workspace:*",
"@litegraph-ts/nodes-strings": "workspace:*",
"@litegraph-ts/tsconfig": "workspace:*",

19
pnpm-lock.yaml generated
View File

@@ -46,6 +46,9 @@ importers:
'@litegraph-ts/nodes-events':
specifier: workspace:*
version: link:litegraph/packages/nodes-events
'@litegraph-ts/nodes-logic':
specifier: workspace:*
version: link:litegraph/packages/nodes-logic
'@litegraph-ts/nodes-math':
specifier: workspace:*
version: link:litegraph/packages/nodes-math
@@ -806,6 +809,22 @@ importers:
specifier: ^4.2.1
version: 4.3.1
litegraph/packages/nodes-logic:
dependencies:
'@litegraph-ts/core':
specifier: workspace:*
version: link:../core
devDependencies:
'@litegraph-ts/tsconfig':
specifier: workspace:*
version: link:../tsconfig
typescript:
specifier: ^5.0.3
version: 5.0.3
vite:
specifier: ^4.2.1
version: 4.3.1
litegraph/packages/nodes-math:
dependencies:
'@litegraph-ts/core':

View File

@@ -17,8 +17,7 @@
import GraphPage from './mobile/routes/graph.svelte';
import ListSubWorkflowsPage from './mobile/routes/list-subworkflows.svelte';
import SubWorkflowPage from './mobile/routes/subworkflow.svelte';
import HellPage from './mobile/routes/hell.svelte';
import type { Framework7Parameters } from "framework7/types";
import type { Framework7Parameters } from "framework7/types";
export let app: ComfyApp;
@@ -83,13 +82,6 @@
props: { app }
}
},
{
path: '/hell/',
component: HellPage,
options: {
props: { app }
}
},
],
popup: {
closeOnEscape: true,

View File

@@ -13,6 +13,7 @@
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
import { startDrag, stopDrag } from "$lib/utils"
import type { Writable } from "svelte/store";
import { isHidden } from "$lib/widgets/utils";
export let container: ContainerLayout | null = null;
export let zIndex: number = 0;
@@ -73,7 +74,7 @@
on:finalize="{handleFinalize}"
>
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)}
{@const hidden = item?.attrs?.hidden}
{@const hidden = isHidden(item)}
<div class="animation-wrapper"
class:hidden={hidden}
animate:flip={{duration:flipDurationMs}}
@@ -86,7 +87,7 @@
</div>
{/each}
</div>
{#if container.attrs.hidden && edit}
{#if isHidden(container) && edit}
<div class="handle handle-hidden" class:hidden={!edit} style="z-index: {zIndex+100}"/>
{/if}
{#if showHandles}

View File

@@ -12,6 +12,7 @@
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
import { startDrag, stopDrag } from "$lib/utils"
import type { Writable } from "svelte/store";
import { isHidden } from "$lib/widgets/utils";
export let container: ContainerLayout | null = null;
export let zIndex: number = 0;
@@ -75,7 +76,7 @@
on:finalize="{handleFinalize}"
>
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)}
{@const hidden = item?.attrs?.hidden}
{@const hidden = isHidden(item)}
<div class="animation-wrapper"
class:hidden={hidden}
animate:flip={{duration:flipDurationMs}}
@@ -88,7 +89,7 @@
</div>
{/each}
</div>
{#if container.attrs.hidden && edit}
{#if isHidden(container) && edit}
<div class="handle handle-hidden" class:hidden={!edit} style="z-index: {zIndex+100}"/>
{/if}
{#if showHandles}
@@ -239,6 +240,10 @@
flex-grow: 100;
}
.handle-hidden {
background-color: #40404080;
}
.handle-widget:hover {
background-color: #add8e680;
}

View File

@@ -201,7 +201,7 @@
})
async function doRefreshCombos() {
await app.refreshComboInNodes()
await app.refreshComboInNodes(true)
}
</script>

View File

@@ -9,6 +9,7 @@ import type TypedEmitter from "typed-emitter";
// Import nodes
import "@litegraph-ts/nodes-basic"
import "@litegraph-ts/nodes-events"
import "@litegraph-ts/nodes-logic"
import "@litegraph-ts/nodes-math"
import "@litegraph-ts/nodes-strings"
import "$lib/nodes/index"
@@ -70,6 +71,27 @@ export type Progress = {
max: number
}
function isActiveBackendNode(node: ComfyGraphNode, tag: string | null): boolean {
if (!node.isBackendNode)
return false;
if (tag && !hasTag(node, tag)) {
console.debug("Skipping tagged node", tag, node.properties.tags, node)
return false;
}
if (node.mode === NodeMode.NEVER) {
// Don't serialize muted nodes
return false;
}
return true;
}
function hasTag(node: LGraphNode, tag: string): boolean {
return "tags" in node.properties && node.properties.tags.indexOf(tag) !== -1
}
export default class ComfyApp {
api: ComfyAPI;
rootEl: HTMLDivElement | null = null;
@@ -443,23 +465,12 @@ export default class ComfyApp {
for (const node_ of this.lGraph.computeExecutionOrder<ComfyGraphNode>(false, null)) {
const n = workflow.nodes.find((n) => n.id === node_.id);
if (!node_.isBackendNode) {
// console.debug("Not serializing node: ", node_.type)
if (!isActiveBackendNode(node_, tag)) {
continue;
}
const node = node_ as ComfyBackendNode;
if (tag && node.tags.indexOf(tag) === -1) {
console.debug("Skipping tagged node", tag, node.tags)
continue;
}
if (node.mode === NodeMode.NEVER) {
// Don't serialize muted nodes
continue;
}
const inputs = {};
// Store all link values
@@ -469,7 +480,11 @@ export default class ComfyApp {
const inputLink = node.getInputLink(i)
const inputNode = node.getInputNode(i)
if (inputNode && tag && "tags" in inputNode && (inputNode.tags as string[]).indexOf(tag) === -1) {
// We don't check tags for non-backend nodes.
// Just check for node inactivity (so you can toggle groups of
// tagged frontend nodes on/off)
if (inputNode && inputNode.mode === NodeMode.NEVER) {
console.debug("Skipping inactive node", inputNode)
continue;
}
@@ -515,7 +530,7 @@ export default class ComfyApp {
const isValidParent = (parent: ComfyGraphNode) => {
if (!parent || parent.isBackendNode)
return false;
if ("tags" in parent && (parent.tags as string[]).indexOf(tag) === -1)
if (tag && !hasTag(parent, tag))
return false;
return true;
}
@@ -525,8 +540,8 @@ export default class ComfyApp {
if (link && !seen[link.id]) {
seen[link.id] = true
const inputNode = parent.getInputNode(link.origin_slot) as ComfyGraphNode;
if (inputNode && "tags" in inputNode && tag && (inputNode.tags as string[]).indexOf(tag) === -1) {
console.debug("Skipping tagged parent node", tag, node.tags)
if (inputNode && tag && !hasTag(inputNode, tag)) {
console.debug("Skipping tagged parent node", tag, node.properties.tags)
parent = null;
}
else {
@@ -538,7 +553,7 @@ export default class ComfyApp {
}
if (link && parent && parent.isBackendNode) {
if ("tags" in parent && tag && (parent.tags as string[]).indexOf(tag) === -1)
if (tag && !hasTag(parent, tag))
continue;
const input = node.inputs[i]
@@ -669,7 +684,7 @@ export default class ComfyApp {
/**
* Refresh combo list on whole nodes
*/
async refreshComboInNodes() {
async refreshComboInNodes(flashUI: boolean = false) {
const defs = await this.api.getNodeDefs();
for (let nodeNum in this.lGraph._nodes) {
@@ -687,11 +702,13 @@ export default class ComfyApp {
const inputNode = node.getInputNode(index)
if (inputNode && "doAutoConfig" in inputNode) {
const comfyInputNode = inputNode as nodes.ComfyWidgetNode;
comfyInputNode.doAutoConfig(comfyInput)
if (!comfyInput.config.values.includes(get(comfyInputNode.value))) {
comfyInputNode.setValue(comfyInput.config.defaultValue || comfyInput.config.values[0])
const comfyComboNode = inputNode as nodes.ComfyComboNode;
comfyComboNode.doAutoConfig(comfyInput)
if (!comfyInput.config.values.includes(get(comfyComboNode.value))) {
comfyComboNode.setValue(comfyInput.config.defaultValue || comfyInput.config.values[0])
}
if (flashUI)
comfyComboNode.comboRefreshed.set(true)
}
}
}

View File

@@ -142,7 +142,7 @@
value = spec.deserialize(value)
target.attrs[name] = value
target.attrsChanged.set(!get(target.attrsChanged))
target.attrsChanged.set(get(target.attrsChanged) + 1)
if (node && "propsChanged" in node) {
const comfyNode = node as ComfyWidgetNode
@@ -151,27 +151,39 @@
console.warn(spec)
if (spec.refreshPanelOnChange) {
console.error("A! refresh")
$refreshPanel += 1;
doRefreshPanel()
}
}
function getProperty(node: LGraphNode, spec: AttributesSpec) {
let value = node.properties[spec.name]
if (value == null)
value = spec.defaultValue
else if (spec.serialize)
value = spec.serialize(value)
console.debug("[ComfyProperties] getProperty", spec, value, node)
return value
}
function updateProperty(spec: AttributesSpec, value: any) {
if (node == null || !spec.editable)
return
const name = spec.name
console.warn("updateProperty", name, value)
console.warn("[ComfyProperties] updateProperty", name, value)
if (spec.deserialize)
value = spec.deserialize(value)
node.properties[name] = value;
if ("propsChanged" in node) {
const comfyNode = node as ComfyWidgetNode
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
comfyNode.notifyPropsChanged();
}
if (spec.refreshPanelOnChange)
$refreshPanel += 1;
doRefreshPanel()
}
function getVar(node: LGraphNode, spec: AttributesSpec) {
@@ -201,8 +213,14 @@
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
}
if (spec.refreshPanelOnChange)
$refreshPanel += 1;
if (spec.refreshPanelOnChange) {
doRefreshPanel()
}
}
function doRefreshPanel() {
console.warn("[ComfyProperties] doRefreshPanel")
$refreshPanel += 1;
}
function updateWorkflowAttribute(spec: AttributesSpec, value: any) {
@@ -214,6 +232,9 @@
$layoutState.attrs[name] = value
$layoutState = $layoutState
if (spec.refreshPanelOnChange)
doRefreshPanel()
}
</script>
@@ -281,7 +302,7 @@
<div class="props-entry">
{#if spec.type === "string"}
<TextBox
value={node.properties[spec.name] || spec.defaultValue}
value={getProperty(node, spec)}
on:change={(e) => updateProperty(spec, e.detail)}
on:input={(e) => updateProperty(spec, e.detail)}
label={spec.name}
@@ -290,7 +311,7 @@
/>
{:else if spec.type === "boolean"}
<Checkbox
value={node.properties[spec.name] || spec.defaultValue}
value={getProperty(node, spec)}
label={spec.name}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateProperty(spec, e.detail)}
@@ -298,7 +319,7 @@
{:else if spec.type === "number"}
<ComfyNumberProperty
name={spec.name}
value={node.properties[spec.name] || spec.defaultValue}
value={getProperty(node, spec)}
step={spec.step || 1}
min={spec.min || -1024}
max={spec.max || 1024}
@@ -308,7 +329,7 @@
{:else if spec.type === "enum"}
<ComfyComboProperty
name={spec.name}
value={node.properties[spec.name] || spec.defaultValue}
value={getProperty(node, spec)}
values={spec.values}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateProperty(spec, e.detail)}

View File

@@ -81,7 +81,7 @@
<span>Node: {getNodeInfo($queueState.runningNodeId)}</span>
</div>
<div>
<ProgressBar value={$queueState.progress?.value} max={$queueState.progress?.max} />
<ProgressBar value={$queueState.progress?.value} max={$queueState.progress?.max} styles="height: 30px;" />
</div>
{/if}
{#if typeof $queueState.queueRemaining === "number" && $queueState.queueRemaining > 0}

View File

@@ -14,6 +14,8 @@
import { flip } from 'svelte/animate';
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
import { startDrag, stopDrag } from "$lib/utils"
import type { Writable } from "svelte/store";
import { isHidden } from "$lib/widgets/utils";
export let container: ContainerLayout | null = null;
export let zIndex: number = 0;
@@ -34,12 +36,14 @@
{@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets" && zIndex > 1}
{@const dragDisabled = zIndex === 0 || $layoutState.currentSelection.length > 2 || !$uiState.uiUnlocked}
{#key $attrsChanged}
{#if container.attrs.variant === "tabs"}
<TabsContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
{:else if container.attrs.variant === "accordion"}
<AccordionContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
{:else}
<BlockContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
{#if edit || !isHidden(container)}
{#if container.attrs.variant === "tabs"}
<TabsContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
{:else if container.attrs.variant === "accordion"}
<AccordionContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
{:else}
<BlockContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
{/if}
{/if}
{/key}
{/if}

View File

@@ -13,6 +13,7 @@
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
import { startDrag, stopDrag } from "$lib/utils"
import type { Writable } from "svelte/store";
import { isHidden } from "$lib/widgets/utils";
export let container: ContainerLayout | null = null;
export let zIndex: number = 0;
@@ -86,7 +87,7 @@
on:finalize="{handleFinalize}"
>
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item, i(item.id)}
{@const hidden = item?.attrs?.hidden}
{@const hidden = isHidden(item)}
{@const tabName = getTabName(container, i)}
<div class="animation-wrapper"
class:hidden={hidden}
@@ -104,7 +105,7 @@
</div>
{/each}
</div>
{#if container.attrs.hidden && edit}
{#if isHidden(container) && edit}
<div class="handle handle-hidden" class:hidden={!edit} style="z-index: {zIndex+100}"/>
{/if}
{#if showHandles}

View File

@@ -7,6 +7,8 @@
import Container from "./Container.svelte"
import { type Writable } from "svelte/store"
import type { ComfyWidgetNode } from "$lib/nodes";
import { NodeMode } from "@litegraph-ts/core";
import { isHidden } from "$lib/widgets/utils";
export let dragItem: IDragItem | null = null;
export let zIndex: number = 0;
@@ -64,17 +66,18 @@
{/key}
{:else if widget && widget.node}
{@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets" && zIndex > 1}
{@const hidden = isHidden(widget)}
{#key $attrsChanged}
{#key $propsChanged}
<div class="widget {widget.attrs.classes} {getWidgetClass()}"
class:edit={edit}
class:selected={$uiState.uiUnlocked && $layoutState.currentSelection.includes(widget.id)}
class:is-executing={$queueState.runningNodeId && $queueState.runningNodeId == widget.node.id}
class:hidden={widget.attrs.hidden}
class:hidden={hidden}
>
<svelte:component this={widget.node.svelteComponentType} {widget} {isMobile} />
</div>
{#if widget.attrs.hidden && edit}
{#if hidden && edit}
<div class="handle handle-hidden" class:hidden={!edit} style="z-index: {zIndex+100}"/>
{/if}
{#if showHandles}

View File

@@ -1,4 +1,4 @@
import { LiteGraph, type ContextMenuItem, type LGraphNode, type Vector2, LConnectionKind, LLink, LGraphCanvas, type SlotType, TitleMode, type SlotLayout, BuiltInSlotType, type ITextWidget, type SerializedLGraphNode } from "@litegraph-ts/core";
import { LiteGraph, type ContextMenuItem, type LGraphNode, type Vector2, LConnectionKind, LLink, LGraphCanvas, type SlotType, TitleMode, type SlotLayout, BuiltInSlotType, type ITextWidget, type SerializedLGraphNode, NodeMode, type IToggleWidget } from "@litegraph-ts/core";
import ComfyGraphNode from "./ComfyGraphNode";
import { Watch } from "@litegraph-ts/nodes-basic";
import type { SerializedPrompt } from "$lib/components/ComfyApp";
@@ -7,6 +7,7 @@ import type { GalleryOutput } from "./ComfyWidgetNodes";
import { get } from "svelte/store";
import queueState from "$lib/stores/queueState";
import notify from "$lib/notify";
import layoutState from "$lib/stores/layoutState";
export interface ComfyQueueEventsProperties extends Record<any, any> {
}
@@ -251,3 +252,86 @@ LiteGraph.registerNodeType({
desc: "Runs a part of the graph based on a tag",
type: "actions/execute_subgraph"
})
export interface ComfySetNodeModeActionProperties extends Record<any, any> {
targetTags: string,
enable: boolean,
}
export class ComfySetNodeModeAction extends ComfyGraphNode {
override properties: ComfySetNodeModeActionProperties = {
targetTags: "",
enable: false
}
static slotLayout: SlotLayout = {
inputs: [
{ name: "enabled", type: "boolean" },
{ name: "set", type: BuiltInSlotType.ACTION },
],
}
displayWidget: ITextWidget;
enableWidget: IToggleWidget;
constructor(title?: string) {
super(title)
this.displayWidget = this.addWidget("text", "Tags", this.properties.targetTags, "targetTags")
this.enableWidget = this.addWidget("toggle", "Enable", this.properties.enable, "enable")
}
override onPropertyChanged(property: any, value: any) {
if (property === "enabled") {
this.enableWidget.value = value
}
}
override onExecute() {
const enabled = this.getInputData(0)
if (typeof enabled === "boolean")
this.setProperty("enabled", enabled)
}
override onAction(action: any, param: any) {
let enabled = this.properties.enabled
if (typeof param === "object" && "enabled" in param)
enabled = param["enabled"]
const tags = this.properties.targetTags.split(",").map(s => s.trim());
for (const node of this.graph._nodes) {
if ("tags" in node.properties) {
const comfyNode = node as ComfyGraphNode;
const hasTag = tags.some(t => comfyNode.properties.tags.indexOf(t) != -1);
if (hasTag) {
let newMode: NodeMode;
if (enabled) {
newMode = NodeMode.ALWAYS;
} else {
newMode = NodeMode.NEVER;
}
node.changeMode(newMode);
}
}
}
for (const entry of Object.values(get(layoutState).allItems)) {
if (entry.dragItem.type === "container") {
const container = entry.dragItem;
const hasTag = tags.some(t => container.attrs.tags.indexOf(t) != -1);
if (hasTag) {
container.attrs.hidden = !enabled;
console.warn("Cont", container.attrs.tags, tags, hasTag, container.attrs, enabled)
}
container.attrsChanged.set(get(container.attrsChanged) + 1)
}
}
}
}
LiteGraph.registerNodeType({
class: ComfySetNodeModeAction,
title: "Comfy.SetNodeModeAction",
desc: "Sets a group of nodes/UI containers as enabled/disabled based on their tags (comma-separated)",
type: "actions/set_node_mode"
})

View File

@@ -32,12 +32,6 @@ export class ComfyBackendNode extends ComfyGraphNode {
}
}
/*
* Tags this node belongs to
* Allows you to run subsections of the graph
*/
tags: string[] = []
private static defaultInputConfigs: Record<string, Record<string, ComfyInputConfig>> = {}
private setup(nodeData: any) {
@@ -88,7 +82,6 @@ export class ComfyBackendNode extends ComfyGraphNode {
override onSerialize(o: SerializedLGraphNode) {
super.onSerialize(o);
(o as any).tags = this.tags
for (const input of o.inputs) {
// strip user-identifying data, it will be reinstantiated later
if ((input as any).config != null) {
@@ -100,8 +93,6 @@ export class ComfyBackendNode extends ComfyGraphNode {
override onConfigure(o: SerializedLGraphNode) {
super.onConfigure(o);
this.tags = (o as any).tags || []
const configs = ComfyBackendNode.defaultInputConfigs[o.type]
for (let index = 0; index < this.inputs.length; index++) {
const input = this.inputs[index] as IComfyInputSlot

View File

@@ -1,7 +1,7 @@
import type { ComfyInputConfig } from "$lib/IComfyInputSlot";
import type { SerializedPrompt } from "$lib/components/ComfyApp";
import type ComfyWidget from "$lib/components/widgets/ComfyWidget";
import { LGraph, LGraphNode, LiteGraph, type SerializedLGraphNode, type Vector2 } from "@litegraph-ts/core";
import { LGraph, LGraphNode, LiteGraph, NodeMode, type SerializedLGraphNode, type Vector2 } from "@litegraph-ts/core";
import type { SvelteComponentDev } from "svelte/internal";
import type { ComfyWidgetNode } from "./ComfyWidgetNodes";
import type IComfyInputSlot from "$lib/IComfyInputSlot";

View File

@@ -0,0 +1,124 @@
import { BuiltInSlotType, LiteGraph, NodeMode, type INodeInputSlot, type SlotLayout, type INodeOutputSlot, LLink, LConnectionKind, type ITextWidget } from "@litegraph-ts/core";
import ComfyGraphNode from "./ComfyGraphNode";
import { Watch } from "@litegraph-ts/nodes-basic";
export interface ComfyPickFirstNodeProperties extends Record<any, any> {
acceptNullLinkData: boolean
}
function nextLetter(s: string): string {
return s.replace(/([a-zA-Z])[^a-zA-Z]*$/, function(a) {
var c = a.charCodeAt(0);
switch (c) {
case 90: return 'A';
case 122: return 'a';
default: return String.fromCharCode(++c);
}
});
}
export default class ComfyPickFirstNode extends ComfyGraphNode {
override properties: ComfyPickFirstNodeProperties = {
acceptNullLinkData: false
}
static slotLayout: SlotLayout = {
inputs: [
{ name: "A", type: "*" },
{ name: "B", type: "*" },
],
outputs: [
{ name: "out", type: "*" }
],
}
private selected: number = -1;
displayWidget: ITextWidget;
constructor(title?: string) {
super(title);
this.displayWidget = this.addWidget("text", "Value", "")
this.displayWidget.disabled = true;
}
override onDrawBackground(ctx: CanvasRenderingContext2D) {
if (this.flags.collapsed || this.selected === -1) {
return;
}
ctx.fillStyle = "#AFB";
var y = (this.selected) * LiteGraph.NODE_SLOT_HEIGHT + 6;
ctx.beginPath();
ctx.moveTo(50, y);
ctx.lineTo(50, y + LiteGraph.NODE_SLOT_HEIGHT);
ctx.lineTo(34, y + LiteGraph.NODE_SLOT_HEIGHT * 0.5);
ctx.fill();
};
override onConnectionsChange(
type: LConnectionKind,
slotIndex: number,
isConnected: boolean,
link: LLink,
ioSlot: (INodeInputSlot | INodeOutputSlot)
) {
if (type !== LConnectionKind.INPUT)
return;
if (isConnected) {
if (link != null && slotIndex === this.inputs.length - 1) {
// Add a new input
const lastInputName = this.inputs[this.inputs.length - 1].name
const inputName = nextLetter(lastInputName);
this.addInput(inputName, "*")
}
}
else {
if (this.getInputLink(this.inputs.length - 1) != null)
return;
// Remove empty inputs
for (let i = this.inputs.length - 2; i > 0; i--) {
if (i <= 0)
break;
if (this.getInputLink(i) == null)
this.removeInput(i)
else
break;
}
let name = "A"
for (let i = 0; i < this.inputs.length; i++) {
this.inputs[i].name = name;
name = nextLetter(name);
}
}
}
override onExecute() {
for (let index = 0; index < this.inputs.length; index++) {
const link = this.getInputLink(index);
if (link != null && (link.data != null || this.properties.acceptNullLinkData)) {
const node = this.getInputNode(index);
if (node != null && node.mode === NodeMode.ALWAYS) {
this.selected = index;
this.displayWidget.value = Watch.toString(link.data)
this.setOutputData(0, link.data)
return
}
}
}
this.selected = -1;
this.setOutputData(0, null)
}
}
LiteGraph.registerNodeType({
class: ComfyPickFirstNode,
title: "Comfy.PickFirst",
desc: "Picks the first active input connected to this node (top to bottom)",
type: "utils/pick_first"
})

View File

@@ -1,4 +1,4 @@
import { LiteGraph, type ContextMenuItem, type LGraphNode, type Vector2, LConnectionKind, LLink, LGraphCanvas, type SlotType, TitleMode, type SlotLayout, LGraph, type INodeInputSlot, type ITextWidget, type INodeOutputSlot, type SerializedLGraphNode, BuiltInSlotType, type PropertyLayout, type IComboWidget } from "@litegraph-ts/core";
import { LiteGraph, type ContextMenuItem, type LGraphNode, type Vector2, LConnectionKind, LLink, LGraphCanvas, type SlotType, TitleMode, type SlotLayout, LGraph, type INodeInputSlot, type ITextWidget, type INodeOutputSlot, type SerializedLGraphNode, BuiltInSlotType, type PropertyLayout, type IComboWidget, NodeMode } from "@litegraph-ts/core";
import ComfyGraphNode from "./ComfyGraphNode";
import ComboWidget from "$lib/widgets/ComboWidget.svelte";
import RangeWidget from "$lib/widgets/RangeWidget.svelte";
@@ -93,6 +93,17 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
return Watch.toString(value)
}
override changeMode(modeTo: NodeMode): boolean {
const result = super.changeMode(modeTo);
this.notifyPropsChanged();
// Also need to notify the parent container since it's what controls the
// hidden state of the widget
const layoutEntry = layoutState.findLayoutEntryForNode(this.id)
if (layoutEntry && layoutEntry.parent)
layoutEntry.parent.attrsChanged.set(get(layoutEntry.parent.attrsChanged) + 1)
return result;
}
private onValueUpdated(value: any) {
console.debug("[Widget] valueUpdated", this, value)
this.displayWidget.value = this.formatValue(value)
@@ -171,6 +182,10 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
console.debug("Property copy", input, this.properties)
this.setValue(get(this.value))
this.notifyPropsChanged();
}
notifyPropsChanged() {
this.propsChanged.set(get(this.propsChanged) + 1)
}
@@ -200,7 +215,7 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
}
// Force reactivity change so the frontend can be updated with the new props
this.propsChanged.set(get(this.propsChanged) + 1)
this.notifyPropsChanged();
}
clampOneConfig(input: IComfyInputSlot) { }
@@ -314,8 +329,11 @@ export class ComfyComboNode extends ComfyWidgetNode<string> {
override svelteComponentType = ComboWidget
override defaultValue = "A";
comboRefreshed: Writable<boolean>;
constructor(name?: string) {
super(name, "A")
this.comboRefreshed = writable(false)
}
onConnectOutput(
@@ -613,7 +631,7 @@ export class ComfyCheckboxNode extends ComfyWidgetNode<boolean> {
const changed = value != get(this.value);
super.setValue(Boolean(value))
if (changed)
this.triggerSlot(1)
this.triggerSlot(1, value)
}
constructor(name?: string) {

View File

@@ -1,6 +1,7 @@
export { default as ComfyReroute } from "./ComfyReroute"
export { ComfyWidgetNode, ComfySliderNode, ComfyComboNode, ComfyTextNode } from "./ComfyWidgetNodes"
export { ComfyQueueEvents, ComfyCopyAction, ComfySwapAction, ComfyNotifyAction, ComfyStoreImagesAction, ComfyExecuteSubgraphAction } from "./ComfyActionNodes"
export { default as ComfyPickFirstNode } from "./ComfyPickFirstNode"
export { default as ComfyValueControl } from "./ComfyValueControl"
export { default as ComfySelector } from "./ComfySelector"
export { default as ComfyImageCacheNode } from "./ComfyImageCacheNode"

View File

@@ -1,7 +1,7 @@
import { get, writable } from 'svelte/store';
import type { Readable, Writable } from 'svelte/store';
import type ComfyApp from "$lib/components/ComfyApp"
import type { LGraphNode, IWidget, LGraph } from "@litegraph-ts/core"
import { type LGraphNode, type IWidget, type LGraph, NodeMode } from "@litegraph-ts/core"
import { dndzone, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
import type { ComfyWidgetNode } from '$lib/nodes';
@@ -116,6 +116,12 @@ export type Attributes = {
*/
containerVariant?: "block" | "hidden",
/*
* Tags for hiding containers with
* For WidgetLayouts this will be ignored, it will use node.properties.tags instead
*/
tags: string[],
/*
* If true, don't show this component in the UI
*/
@@ -142,6 +148,12 @@ export type Attributes = {
*/
variant?: string,
/*
* What state to set this widget to in the frontend if its corresponding
* node is disabled in the graph.
*/
nodeDisabledState: "visible" | "disabled" | "hidden",
/*********************************************/
/* Special attributes for widgets/containers */
/*********************************************/
@@ -154,6 +166,9 @@ export type Attributes = {
buttonSize?: "large" | "small"
}
/*
* Defines something that can be edited in the properties side panel.
*/
export type AttributesSpec = {
/*
* ID necessary for svelte's keyed each, autoset at the top level in this source file.
@@ -256,9 +271,13 @@ export type AttributesCategorySpec = {
export type AttributesSpecList = AttributesCategorySpec[]
const serializeStringArray = (arg: string[]) => arg.join(",")
const serializeStringArray = (arg: string[]) => {
if (arg == null)
arg = []
return arg.join(",")
}
const deserializeStringArray = (arg: string) => {
if (arg === "")
if (arg === "" || arg == null)
return []
return arg.split(",").map(s => s.trim())
}
@@ -315,6 +334,15 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
defaultValue: "",
editable: true,
},
{
name: "nodeDisabledState",
type: "enum",
location: "widget",
editable: true,
values: ["visible", "disabled", "hidden"],
defaultValue: "disabled",
canShow: (di: IDragItem) => di.type === "widget"
},
// Container variants
{
@@ -372,15 +400,6 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
categoryName: "behavior",
specs: [
// Node variables
{
name: "tags",
type: "string",
location: "nodeVars",
editable: true,
defaultValue: [],
serialize: serializeStringArray,
deserialize: deserializeStringArray
},
{
name: "saveUserState",
type: "boolean",
@@ -388,6 +407,39 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
editable: true,
defaultValue: true,
},
{
name: "mode",
type: "enum",
location: "nodeVars",
editable: true,
values: ["ALWAYS", "NEVER"],
defaultValue: "ALWAYS",
serialize: (s) => s === NodeMode.ALWAYS ? "ALWAYS" : "NEVER",
deserialize: (m) => m === "ALWAYS" ? NodeMode.ALWAYS : NodeMode.NEVER
},
// Node properties
{
name: "tags",
type: "string",
location: "nodeProps",
editable: true,
defaultValue: [],
serialize: serializeStringArray,
deserialize: deserializeStringArray
},
// Container tags are contained in the widget attributes
{
name: "tags",
type: "string",
location: "widget",
editable: true,
defaultValue: [],
serialize: serializeStringArray,
deserialize: deserializeStringArray,
canShow: (di: IDragItem) => di.type === "container"
},
// Range
{
@@ -457,14 +509,23 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
// This is needed so the specs can be iterated with svelte's keyed #each.
let i = 0;
for (const cat of Object.values(ALL_ATTRIBUTES)) {
for (const val of Object.values(cat.specs)) {
val.id = i;
for (const spec of Object.values(cat.specs)) {
spec.id = i;
i += 1;
}
}
export { ALL_ATTRIBUTES };
const defaultWidgetAttributes: Attributes = {} as any
for (const cat of Object.values(ALL_ATTRIBUTES)) {
for (const spec of Object.values(cat.specs)) {
if (spec.location === "widget" && spec.defaultValue != null) {
defaultWidgetAttributes[spec.name] = spec.defaultValue;
}
}
}
/*
* Something that can be dragged around in the frontend - a widget or a container.
*/
@@ -494,7 +555,7 @@ export interface IDragItem {
* Hackish thing to indicate to Svelte that an attribute changed.
* TODO Use Writeable<Attributes> instead!
*/
attrsChanged: Writable<boolean>
attrsChanged: Writable<number>
}
/*
@@ -528,7 +589,8 @@ type LayoutStateOps = {
groupItems: (dragItems: IDragItem[], attrs?: Partial<Attributes>) => ContainerLayout,
ungroup: (container: ContainerLayout) => void,
getCurrentSelection: () => IDragItem[],
findLayoutForNode: (nodeId: number) => IDragItem | null;
findLayoutEntryForNode: (nodeId: number) => DragItemEntry | null,
findLayoutForNode: (nodeId: number) => IDragItem | null,
serialize: () => SerializedLayoutState,
deserialize: (data: SerializedLayoutState, graph: LGraph) => void,
initDefaultLayout: () => void,
@@ -575,13 +637,10 @@ function addContainer(parent: ContainerLayout | null, attrs: Partial<Attributes>
const dragItem: ContainerLayout = {
type: "container",
id: `${state.currentId++}`,
attrsChanged: writable(false),
attrsChanged: writable(0),
attrs: {
...defaultWidgetAttributes,
title: "Container",
direction: "vertical",
classes: "",
containerVariant: "block",
flexGrow: 100,
...attrs
}
}
@@ -602,12 +661,11 @@ function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partia
type: "widget",
id: `${state.currentId++}`,
node: node,
attrsChanged: writable(false),
attrsChanged: writable(0),
attrs: {
...defaultWidgetAttributes,
title: widgetName,
direction: "horizontal",
classes: "",
flexGrow: 100,
nodeDisabledState: "disabled",
...attrs
}
}
@@ -778,16 +836,23 @@ function ungroup(container: ContainerLayout) {
store.set(state)
}
function findLayoutForNode(nodeId: number): WidgetLayout | null {
function findLayoutEntryForNode(nodeId: number): DragItemEntry | null {
const state = get(store)
const found = Object.entries(state.allItems).find(pair =>
pair[1].dragItem.type === "widget"
&& (pair[1].dragItem as WidgetLayout).node.id === nodeId)
if (found)
return found[1].dragItem as WidgetLayout
return found[1]
return null;
}
function findLayoutForNode(nodeId: number): WidgetLayout | null {
const found = findLayoutEntryForNode(nodeId);
if (!found)
return null;
return found.dragItem as WidgetLayout
}
function initDefaultLayout() {
store.set({
root: null,
@@ -869,8 +934,8 @@ function deserialize(data: SerializedLayoutState, graph: LGraph) {
const dragItem: IDragItem = {
type: entry.dragItem.type,
id: entry.dragItem.id,
attrs: entry.dragItem.attrs,
attrsChanged: writable(false)
attrs: { ...defaultWidgetAttributes, ...entry.dragItem.attrs },
attrsChanged: writable(0)
};
const dragEntry: DragItemEntry = {
@@ -939,6 +1004,7 @@ const layoutStateStore: WritableLayoutStateStore =
nodeRemoved,
getCurrentSelection,
groupItems,
findLayoutEntryForNode,
findLayoutForNode,
ungroup,
initDefaultLayout,

View File

@@ -4,6 +4,7 @@
import { type WidgetLayout } from "$lib/stores/layoutState";
import { Button } from "@gradio/button";
import { get, type Writable, writable } from "svelte/store";
import { isDisabled } from "./utils"
export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
let node: ComfyButtonNode | null = null;
@@ -32,9 +33,9 @@
<div class="wrapper gradio-button">
{#key $attrsChanged}
{#if node !== null}
{#if widget !== null && node !== null}
<Button
disabled={widget.attrs.disabled}
disabled={isDisabled(widget)}
on:click={onClick}
variant={widget.attrs.buttonVariant || "primary"}
size={widget.attrs.buttonSize === "small" ? "sm" : "lg"}

View File

@@ -1,9 +1,10 @@
<script lang="ts">
import type { ComfyCheckboxNode } from "$lib/nodes/ComfyWidgetNodes";
import { type WidgetLayout } from "$lib/stores/layoutState";
import { Block } from "@gradio/atoms";
import { Block } from "@gradio/atoms";
import { Checkbox } from "@gradio/form";
import { get, type Writable, writable } from "svelte/store";
import { isDisabled } from "./utils"
export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
@@ -31,7 +32,7 @@
{#key $attrsChanged}
{#if node !== null}
<Block>
<Checkbox disabled={widget.attrs.disabled} label={widget.attrs.title} bind:value={$nodeValue} on:select={onSelect} />
<Checkbox disabled={isDisabled(widget)} label={widget.attrs.title} bind:value={$nodeValue} on:select={onSelect} />
</Block>
{/if}
{/key}

View File

@@ -4,11 +4,14 @@
import type { ComfyComboNode } from "$lib/nodes/index";
import { type WidgetLayout } from "$lib/stores/layoutState";
import { get, type Writable } from "svelte/store";
import { isDisabled } from "./utils"
export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
let node: ComfyComboNode | null = null;
let nodeValue: Writable<string> | null = null;
let propsChanged: Writable<number> | null = null;
let comboRefreshed: Writable<boolean> | null = null;
let wasComboRefreshed: boolean = false;
let option: any
export let debug: boolean = false;
@@ -34,6 +37,9 @@
node = widget.node as ComfyComboNode
nodeValue = node.value;
propsChanged = node.propsChanged;
comboRefreshed = node.comboRefreshed;
if ($comboRefreshed)
flashOnRefreshed();
setOption($nodeValue) // don't react on option
}
}
@@ -46,6 +52,12 @@
$nodeValue = option.value;
}
$: $comboRefreshed && flashOnRefreshed();
function flashOnRefreshed() {
setTimeout(() => ($comboRefreshed = false), 1000);
}
function getLinkValue() {
if (!node)
return "???";
@@ -64,46 +76,39 @@
input.blur();
navigator.vibrate(20)
}
let lastPropsChanged: number = 0;
let werePropsChanged: boolean = false;
$: if ($propsChanged !== lastPropsChanged) {
werePropsChanged = true;
lastPropsChanged = $propsChanged;
setTimeout(() => (werePropsChanged = false), 2000);
}
</script>
<div class="wrapper comfy-combo" class:updated={werePropsChanged}>
<div class="wrapper comfy-combo" class:updated={$comboRefreshed}>
{#key $propsChanged}
{#if node !== null && nodeValue !== null}
<label>
{#if widget.attrs.title !== ""}
<BlockTitle show_label={true}>{widget.attrs.title}</BlockTitle>
{/if}
<Select
bind:value={option}
items={node.properties.values}
disabled={widget.attrs.disabled || node.properties.values.length === 0}
clearable={false}
showChevron={true}
inputAttributes={{ autocomplete: 'off' }}
bind:input
on:change
on:focus={onFocus}
on:select={onSelect}
on:filter
on:blur
/>
{#if debug}
<div>Value: {option?.value}</div>
<div>Items: {node.properties.values}</div>
<div>NodeValue: {$nodeValue}</div>
<div>LinkValue: {getLinkValue()}</div>
{/if}
</label>
{/if}
{#key $comboRefreshed}
{#if node !== null && nodeValue !== null}
<label>
{#if widget.attrs.title !== ""}
<BlockTitle show_label={true}>{widget.attrs.title}</BlockTitle>
{/if}
<Select
bind:value={option}
items={node.properties.values}
disabled={isDisabled(widget) || node.properties.values.length === 0}
clearable={false}
showChevron={true}
inputAttributes={{ autocomplete: 'off' }}
bind:input
on:change
on:focus={onFocus}
on:select={onSelect}
on:filter
on:blur
/>
{#if debug}
<div>Value: {option?.value}</div>
<div>Items: {node.properties.values}</div>
<div>NodeValue: {$nodeValue}</div>
<div>LinkValue: {getLinkValue()}</div>
{/if}
</label>
{/if}
{/key}
{/key}
</div>

View File

@@ -5,6 +5,7 @@
import { get, type Writable } from "svelte/store";
import { debounce } from "$lib/utils";
import interfaceState from "$lib/stores/interfaceState";
import { isDisabled } from "./utils"
export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
let node: ComfySliderNode | null = null;
@@ -104,7 +105,7 @@
{#if node !== null && option !== null}
<Range
bind:value={option}
disabled={widget.attrs.disabled}
disabled={isDisabled(widget)}
minimum={node.properties.min}
maximum={node.properties.max}
step={node.properties.step}

View File

@@ -3,6 +3,7 @@
import type { ComfyComboNode } from "$lib/nodes/index";
import { type WidgetLayout } from "$lib/stores/layoutState";
import { get, type Writable } from "svelte/store";
import { isDisabled } from "./utils"
export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
let node: ComfyComboNode | null = null;
@@ -33,7 +34,7 @@
<TextBox
bind:value={$nodeValue}
label={widget.attrs.title}
disabled={widget.attrs.disabled}
disabled={isDisabled(widget)}
lines={node.properties.multiline ? 5 : 1}
max_lines={node.properties.multiline ? 5 : 1}
show_label={widget.attrs.title !== ""}

26
src/lib/widgets/utils.ts Normal file
View File

@@ -0,0 +1,26 @@
import type { IDragItem } from "$lib/stores/layoutState";
import layoutState from "$lib/stores/layoutState";
import { NodeMode } from "@litegraph-ts/core";
import { get } from "svelte/store";
export function isDisabled(widget: IDragItem) {
if (widget.attrs.disabled)
return true;
if (widget.type === "widget") {
return widget.attrs.nodeDisabledState === "disabled" && widget.node.mode === NodeMode.NEVER
}
return false;
}
export function isHidden(widget: IDragItem) {
if (widget.attrs.hidden)
return true;
if (widget.type === "widget") {
return widget.attrs.nodeDisabledState === "hidden" && widget.node.mode === NodeMode.NEVER
}
return false;
}

View File

@@ -33,8 +33,5 @@
<ListItem link="/graph/" title="Show Node Graph">
<i class="icon icon-f7" slot="media" />
</ListItem>
<ListItem link="/hell/" title="🔥 HELL 🔥">
<i class="icon icon-f7" slot="media" />
</ListItem>
</List>
</Page>