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 @@