diff --git a/litegraph b/litegraph index fd575bf..e7df2b2 160000 --- a/litegraph +++ b/litegraph @@ -1 +1 @@ -Subproject commit fd575bf9a2aded2c3225d470fd1e89538e055dfd +Subproject commit e7df2b2b75bb24fd3682bea80229608438bfc913 diff --git a/package.json b/package.json index f17a7d0..219d603 100644 --- a/package.json +++ b/package.json @@ -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:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f7e8cb..7ce7654 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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': diff --git a/src/AppMobile.svelte b/src/AppMobile.svelte index 2ea05ae..013700d 100644 --- a/src/AppMobile.svelte +++ b/src/AppMobile.svelte @@ -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, diff --git a/src/lib/components/AccordionContainer.svelte b/src/lib/components/AccordionContainer.svelte index 0d42f1d..aa16886 100644 --- a/src/lib/components/AccordionContainer.svelte +++ b/src/lib/components/AccordionContainer.svelte @@ -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)}
{/each}
- {#if container.attrs.hidden && edit} + {#if isHidden(container) && edit}
{/if} {#if showHandles} diff --git a/src/lib/components/BlockContainer.svelte b/src/lib/components/BlockContainer.svelte index 6d846c9..10520f3 100644 --- a/src/lib/components/BlockContainer.svelte +++ b/src/lib/components/BlockContainer.svelte @@ -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)}
{/each}
- {#if container.attrs.hidden && edit} + {#if isHidden(container) && edit}
{/if} {#if showHandles} @@ -239,6 +240,10 @@ flex-grow: 100; } + .handle-hidden { + background-color: #40404080; + } + .handle-widget:hover { background-color: #add8e680; } diff --git a/src/lib/components/ComfyApp.svelte b/src/lib/components/ComfyApp.svelte index b41deaa..6c9468e 100644 --- a/src/lib/components/ComfyApp.svelte +++ b/src/lib/components/ComfyApp.svelte @@ -201,7 +201,7 @@ }) async function doRefreshCombos() { - await app.refreshComboInNodes() + await app.refreshComboInNodes(true) } diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts index 45a55b1..462979d 100644 --- a/src/lib/components/ComfyApp.ts +++ b/src/lib/components/ComfyApp.ts @@ -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(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) } } } diff --git a/src/lib/components/ComfyProperties.svelte b/src/lib/components/ComfyProperties.svelte index 8ddb036..3eaa14a 100644 --- a/src/lib/components/ComfyProperties.svelte +++ b/src/lib/components/ComfyProperties.svelte @@ -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() } @@ -281,7 +302,7 @@
{#if spec.type === "string"} updateProperty(spec, e.detail)} on:input={(e) => updateProperty(spec, e.detail)} label={spec.name} @@ -290,7 +311,7 @@ /> {:else if spec.type === "boolean"} updateProperty(spec, e.detail)} @@ -298,7 +319,7 @@ {:else if spec.type === "number"} updateProperty(spec, e.detail)} diff --git a/src/lib/components/ComfyQueue.svelte b/src/lib/components/ComfyQueue.svelte index 0c627f2..181bf98 100644 --- a/src/lib/components/ComfyQueue.svelte +++ b/src/lib/components/ComfyQueue.svelte @@ -81,7 +81,7 @@ Node: {getNodeInfo($queueState.runningNodeId)}
- +
{/if} {#if typeof $queueState.queueRemaining === "number" && $queueState.queueRemaining > 0} diff --git a/src/lib/components/Container.svelte b/src/lib/components/Container.svelte index 74ee19a..bdc05f1 100644 --- a/src/lib/components/Container.svelte +++ b/src/lib/components/Container.svelte @@ -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"} - - {:else if container.attrs.variant === "accordion"} - - {:else} - + {#if edit || !isHidden(container)} + {#if container.attrs.variant === "tabs"} + + {:else if container.attrs.variant === "accordion"} + + {:else} + + {/if} {/if} {/key} {/if} diff --git a/src/lib/components/TabsContainer.svelte b/src/lib/components/TabsContainer.svelte index 09fb3fa..1f163be 100644 --- a/src/lib/components/TabsContainer.svelte +++ b/src/lib/components/TabsContainer.svelte @@ -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)}
{/each}
- {#if container.attrs.hidden && edit} + {#if isHidden(container) && edit}
{/if} {#if showHandles} diff --git a/src/lib/components/WidgetContainer.svelte b/src/lib/components/WidgetContainer.svelte index e3b9b45..e7cdc60 100644 --- a/src/lib/components/WidgetContainer.svelte +++ b/src/lib/components/WidgetContainer.svelte @@ -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}
- {#if widget.attrs.hidden && edit} + {#if hidden && edit}
{/if} {#if showHandles} diff --git a/src/lib/nodes/ComfyActionNodes.ts b/src/lib/nodes/ComfyActionNodes.ts index 4253d34..7ac6332 100644 --- a/src/lib/nodes/ComfyActionNodes.ts +++ b/src/lib/nodes/ComfyActionNodes.ts @@ -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 { } @@ -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 { + 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" +}) diff --git a/src/lib/nodes/ComfyBackendNode.ts b/src/lib/nodes/ComfyBackendNode.ts index bd912d7..4c1e3c3 100644 --- a/src/lib/nodes/ComfyBackendNode.ts +++ b/src/lib/nodes/ComfyBackendNode.ts @@ -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> = {} 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 diff --git a/src/lib/nodes/ComfyGraphNode.ts b/src/lib/nodes/ComfyGraphNode.ts index 47acecc..067b26b 100644 --- a/src/lib/nodes/ComfyGraphNode.ts +++ b/src/lib/nodes/ComfyGraphNode.ts @@ -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"; diff --git a/src/lib/nodes/ComfyPickFirstNode.ts b/src/lib/nodes/ComfyPickFirstNode.ts new file mode 100644 index 0000000..113a01f --- /dev/null +++ b/src/lib/nodes/ComfyPickFirstNode.ts @@ -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 { + 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" +}) diff --git a/src/lib/nodes/ComfyWidgetNodes.ts b/src/lib/nodes/ComfyWidgetNodes.ts index d8fe36a..05632e0 100644 --- a/src/lib/nodes/ComfyWidgetNodes.ts +++ b/src/lib/nodes/ComfyWidgetNodes.ts @@ -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 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 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 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 { override svelteComponentType = ComboWidget override defaultValue = "A"; + comboRefreshed: Writable; + constructor(name?: string) { super(name, "A") + this.comboRefreshed = writable(false) } onConnectOutput( @@ -613,7 +631,7 @@ export class ComfyCheckboxNode extends ComfyWidgetNode { const changed = value != get(this.value); super.setValue(Boolean(value)) if (changed) - this.triggerSlot(1) + this.triggerSlot(1, value) } constructor(name?: string) { diff --git a/src/lib/nodes/index.ts b/src/lib/nodes/index.ts index c86ce49..6f277c6 100644 --- a/src/lib/nodes/index.ts +++ b/src/lib/nodes/index.ts @@ -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" diff --git a/src/lib/stores/layoutState.ts b/src/lib/stores/layoutState.ts index 4572c05..99649b8 100644 --- a/src/lib/stores/layoutState.ts +++ b/src/lib/stores/layoutState.ts @@ -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 instead! */ - attrsChanged: Writable + attrsChanged: Writable } /* @@ -528,7 +589,8 @@ type LayoutStateOps = { groupItems: (dragItems: IDragItem[], attrs?: Partial) => 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 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, diff --git a/src/lib/widgets/ButtonWidget.svelte b/src/lib/widgets/ButtonWidget.svelte index b07ccaf..c92b6f5 100644 --- a/src/lib/widgets/ButtonWidget.svelte +++ b/src/lib/widgets/ButtonWidget.svelte @@ -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 @@
{#key $attrsChanged} - {#if node !== null} + {#if widget !== null && node !== null}