diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts index deb2166..22cd784 100644 --- a/src/lib/components/ComfyApp.ts +++ b/src/lib/components/ComfyApp.ts @@ -18,6 +18,7 @@ import "@litegraph-ts/nodes-math" import "@litegraph-ts/nodes-strings" import "$lib/nodes/index" import "$lib/nodes/widgets/index" +import "$lib/nodes/actions/index" import * as nodes from "$lib/nodes/index" import * as widgets from "$lib/nodes/widgets/index" diff --git a/src/lib/nodes/ComfyActionNodes.ts b/src/lib/nodes/ComfyActionNodes.ts deleted file mode 100644 index 1d286a7..0000000 --- a/src/lib/nodes/ComfyActionNodes.ts +++ /dev/null @@ -1,612 +0,0 @@ -import type { SerializedPrompt } from "$lib/components/ComfyApp"; -import notify from "$lib/notify"; -import { type DragItemID } from "$lib/stores/layoutStates"; -import queueState from "$lib/stores/queueState"; -import { BuiltInSlotType, LiteGraph, NodeMode, type ITextWidget, type IToggleWidget, type SerializedLGraphNode, type SlotLayout, type PropertyLayout } from "@litegraph-ts/core"; -import { get } from "svelte/store"; -import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode"; -import type { ComfyWidgetNode } from "$lib/nodes/widgets"; -import type { NotifyOptions } from "$lib/notify"; -import type { FileData as GradioFileData } from "@gradio/upload"; -import { type SerializedPromptOutput, type ComfyImageLocation, convertComfyOutputToGradio, type ComfyUploadImageAPIResponse, parseWhateverIntoComfyImageLocations } from "$lib/utils"; - -export class ComfyQueueEvents extends ComfyGraphNode { - static slotLayout: SlotLayout = { - outputs: [ - { name: "beforeQueued", type: BuiltInSlotType.EVENT }, - { name: "afterQueued", type: BuiltInSlotType.EVENT }, - { name: "onDefaultQueueAction", type: BuiltInSlotType.EVENT }, - ], - } - - private getActionParams(subgraph: string | null): any { - let queue = get(queueState) - let queueRemaining = 0; - - if (typeof queue.queueRemaining === "number") - queueRemaining = queue.queueRemaining - - return { - queueRemaining, - subgraph - } - } - - override beforeQueued(subgraph: string | null) { - this.triggerSlot(0, this.getActionParams(subgraph)) - } - - override afterQueued(p: SerializedPrompt, subgraph: string | null) { - this.triggerSlot(1, this.getActionParams(subgraph)) - } - - override onDefaultQueueAction() { - let queue = get(queueState) - let queueRemaining = 0; - - if (typeof queue.queueRemaining === "number") - queueRemaining = queue.queueRemaining - - this.triggerSlot(2, { queueRemaining }) - } - - override onSerialize(o: SerializedLGraphNode) { - super.onSerialize(o) - } -} - -LiteGraph.registerNodeType({ - class: ComfyQueueEvents, - title: "Comfy.QueueEvents", - desc: "Triggers a 'bang' event when a prompt is queued.", - type: "events/queue_events" -}) - -export interface ComfyStoreImagesActionProperties extends ComfyGraphNodeProperties { - images: SerializedPromptOutput | null -} - -export class ComfyStoreImagesAction extends ComfyGraphNode { - override properties: ComfyStoreImagesActionProperties = { - tags: [], - images: null - } - - static slotLayout: SlotLayout = { - inputs: [ - { name: "output", type: BuiltInSlotType.ACTION, options: { color_off: "rebeccapurple", color_on: "rebeccapurple" } } - ], - outputs: [ - { name: "images", type: "OUTPUT" }, - ], - } - - override onExecute() { - if (this.properties.images !== null) - this.setOutputData(0, this.properties.images) - } - - override onAction(action: any, param: any) { - if (action !== "store" || !param || !("images" in param)) - return; - - this.setProperty("images", param as SerializedPromptOutput) - this.setOutputData(0, this.properties.images) - } -} - -LiteGraph.registerNodeType({ - class: ComfyStoreImagesAction, - title: "Comfy.StoreImagesAction", - desc: "Stores images from an onExecuted callback", - type: "actions/store_images" -}) - -export interface ComfyCopyActionProperties extends ComfyGraphNodeProperties { - value: any -} - -export class ComfyCopyAction extends ComfyGraphNode { - override properties: ComfyCopyActionProperties = { - value: null, - tags: [] - } - - static slotLayout: SlotLayout = { - inputs: [ - { name: "in", type: "*" }, - { name: "copy", type: BuiltInSlotType.ACTION } - ], - outputs: [ - { name: "out", type: BuiltInSlotType.EVENT } - ], - } - - displayWidget: ITextWidget; - - constructor(title?: string) { - super(title); - this.displayWidget = this.addWidget( - "text", - "Value", - "", - "value" - ); - this.displayWidget.disabled = true; - } - - override onExecute() { - if (this.getInputLink(0)) - this.setProperty("value", this.getInputData(0)) - } - - override onAction(action: any, param: any) { - if (action === "copy") { - this.setProperty("value", this.getInputData(0)) - this.triggerSlot(0, this.properties.value) - } - }; -} - -LiteGraph.registerNodeType({ - class: ComfyCopyAction, - title: "Comfy.CopyAction", - desc: "Copies its input to its output when an event is received", - type: "actions/copy" -}) - -export interface ComfySwapActionProperties extends ComfyGraphNodeProperties { -} - -export class ComfySwapAction extends ComfyGraphNode { - override properties: ComfySwapActionProperties = { - } - - static slotLayout: SlotLayout = { - inputs: [ - { name: "A", type: "*" }, - { name: "B", type: "*" }, - { name: "swap", type: BuiltInSlotType.ACTION } - ], - outputs: [ - { name: "B", type: BuiltInSlotType.EVENT }, - { name: "A", type: BuiltInSlotType.EVENT } - ], - } - - override onAction(action: any, param: any) { - const a = this.getInputData(0) - const b = this.getInputData(1) - this.triggerSlot(0, b) - this.triggerSlot(1, a) - }; -} - -LiteGraph.registerNodeType({ - class: ComfySwapAction, - title: "Comfy.SwapAction", - desc: "Swaps two inputs when triggered", - type: "actions/swap" -}) - -export interface ComfyNotifyActionProperties extends ComfyGraphNodeProperties { - message: string, - type: string -} - -export class ComfyNotifyAction extends ComfyGraphNode { - override properties: ComfyNotifyActionProperties = { - tags: [], - message: "Nya.", - type: "info" - } - - static slotLayout: SlotLayout = { - inputs: [ - { name: "message", type: "string" }, - { name: "trigger", type: BuiltInSlotType.ACTION } - ], - } - - override onAction(action: any, param: any) { - const message = this.getInputData(0) || this.properties.message; - if (!message) - return; - - const options: NotifyOptions = { - type: this.properties.type, - showOn: "all" - } - - // Check if this event was triggered from a backend node and has the - // onExecuted arguments. If so then use the first image as the icon for - // native notifications. - if (param != null && typeof param === "object") { - if ("images" in param) { - const output = param as SerializedPromptOutput; - const converted = convertComfyOutputToGradio(output); - if (converted.length > 0) - options.imageUrl = converted[0].data; - } - } - - notify(message, options); - }; -} - -LiteGraph.registerNodeType({ - class: ComfyNotifyAction, - title: "Comfy.NotifyAction", - desc: "Displays a message.", - type: "actions/notify" -}) - -export interface ComfyPlaySoundActionProperties extends ComfyGraphNodeProperties { - sound: string, -} - -export class ComfyPlaySoundAction extends ComfyGraphNode { - override properties: ComfyPlaySoundActionProperties = { - tags: [], - sound: "notification.mp3" - } - - static slotLayout: SlotLayout = { - inputs: [ - { name: "sound", type: "string" }, - { name: "trigger", type: BuiltInSlotType.ACTION } - ], - } - - override onAction(action: any, param: any) { - const sound = this.getInputData(0) || this.properties.sound; - if (sound) { - const url = `${location.origin}/sound/${sound}`; - const audio = new Audio(url); - audio.play(); - } - }; -} - -LiteGraph.registerNodeType({ - class: ComfyPlaySoundAction, - title: "Comfy.PlaySoundAction", - desc: "Plays a sound located under the sound/ directory.", - type: "actions/play_sound" -}) - -export interface ComfyExecuteSubgraphActionProperties extends ComfyGraphNodeProperties { - targetTag: string -} - -export class ComfyExecuteSubgraphAction extends ComfyGraphNode { - override properties: ComfyExecuteSubgraphActionProperties = { - tags: [], - targetTag: "" - } - - static slotLayout: SlotLayout = { - inputs: [ - { name: "execute", type: BuiltInSlotType.ACTION }, - { name: "targetTag", type: "string" } - ], - } - - displayWidget: ITextWidget; - - constructor(title?: string) { - super(title) - this.displayWidget = this.addWidget("text", "targetTag", this.properties.targetTag, "targetTag") - } - - override onExecute() { - const tag = this.getInputData(1) - if (tag) - this.setProperty("tag", tag) - } - - override onAction(action: any, param: any) { - const tag = this.getInputData(1) || this.properties.targetTag; - - const app = (window as any)?.app; - if (!app) - return; - - // Hold control to queue at the front - const num = app.ctrlDown ? -1 : 0; - app.queuePrompt(num, 1, tag); - } -} - -LiteGraph.registerNodeType({ - class: ComfyExecuteSubgraphAction, - title: "Comfy.ExecuteSubgraphAction", - desc: "Runs a part of the graph based on a tag", - type: "actions/execute_subgraph" -}) - -export interface ComfySetNodeModeActionProperties extends ComfyGraphNodeProperties { - targetTags: string, - enable: boolean, -} - -export class ComfySetNodeModeAction extends ComfyGraphNode { - override properties: ComfySetNodeModeActionProperties = { - targetTags: "", - enable: false, - tags: [] - } - - 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 === "enable") { - this.enableWidget.value = value - } - } - - override onAction(action: any, param: any) { - let input = this.getInputData(0) - if (input == null) - input = this.properties.enable - - let enabled = Boolean(input) - - 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); - if ("notifyPropsChanged" in node) - (node as ComfyWidgetNode).notifyPropsChanged(); - } - } - } - - for (const entry of Object.values(get(this.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; - } - 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" -}) - -export type TagAction = { - tag: string, - enable: boolean -} - -export interface ComfySetNodeModeAdvancedActionProperties extends ComfyGraphNodeProperties { - targetTags: TagAction[], - enable: boolean, -} - -export class ComfySetNodeModeAdvancedAction extends ComfyGraphNode { - override properties: ComfySetNodeModeAdvancedActionProperties = { - targetTags: [{ tag: "myTag", enable: true }, { tag: "anotherTag", enable: false }], - enable: true, - tags: [] - } - - static slotLayout: SlotLayout = { - inputs: [ - { name: "enabled", type: "boolean" }, - { name: "set", type: BuiltInSlotType.ACTION }, - ], - } - - static propertyLayout: PropertyLayout = [ - { name: "enable", defaultValue: true, type: "boolean", }, - { name: "targetTags", defaultValue: [{ tag: "myTag", enable: true }, { tag: "anotherTag", enable: false }], type: "array", options: { multiline: true, inputStyle: { fontFamily: "monospace" } } } - ] - - displayWidget: ITextWidget; - enableWidget: IToggleWidget; - - constructor(title?: string) { - super(title) - this.displayWidget = this.addWidget("text", "Tags", this.formatTags(), null); - this.displayWidget.disabled = true; - this.enableWidget = this.addWidget("toggle", "Enable", this.properties.enable, "enable"); - } - - override onPropertyChanged(property: any, value: any) { - if (property === "enable") { - this.enableWidget.value = value - } - else if (property === "targetTags") { - this.displayWidget.value = this.formatTags() - } - } - - private formatTags(): string { - if (!Array.isArray(this.properties.targetTags) || this.properties.targetTags.length === 0) - return "(No tags)"; - return this.properties.targetTags.map(t => { - let s = t.tag - if (t.enable) - s = "+" + s - else - s = "!" + s - return s - }).join(", ") - } - - private getModeChanges(action: TagAction, enable: boolean, nodeChanges: Record, widgetChanges: Record) { - for (const node of this.graph.iterateNodesInOrderRecursive()) { - if ("tags" in node.properties) { - const comfyNode = node as ComfyGraphNode; - const hasTag = comfyNode.properties.tags.indexOf(action.tag) != -1; - - if (hasTag) { - let newMode: NodeMode; - if (enable && action.enable) { - newMode = NodeMode.ALWAYS; - } else { - newMode = NodeMode.NEVER; - } - nodeChanges[node.id] = newMode - } - } - } - - for (const entry of Object.values(get(this.layoutState).allItems)) { - if (entry.dragItem.type === "container") { - const container = entry.dragItem; - const hasTag = container.attrs.tags.indexOf(action.tag) != -1; - if (hasTag) { - const hidden = !(enable && action.enable) - widgetChanges[container.id] = hidden - } - } - } - } - - override onExecute() { - this.boxcolor = LiteGraph.NODE_DEFAULT_BOXCOLOR; - - for (const action of this.properties.targetTags) { - if (typeof action !== "object" || !("tag" in action) || !("enable" in action)) { - this.boxcolor = "red"; - break; - } - } - } - - override onAction(action: any, param: any) { - let input = this.getInputData(0) - if (input == null) - input = this.properties.enable - - let enabled = Boolean(input) - - if (typeof param === "object" && "enabled" in param) - enabled = param["enabled"] - - const nodeChanges: Record = {} // nodeID => newState - const widgetChanges: Record = {} // dragItemID => isHidden - - for (const action of this.properties.targetTags) { - this.getModeChanges(action, enabled, nodeChanges, widgetChanges) - } - - for (const [nodeId, newMode] of Object.entries(nodeChanges)) { - this.graph.getNodeByIdRecursive(nodeId).changeMode(newMode); - } - - const layout = get(this.layoutState); - for (const [dragItemID, isHidden] of Object.entries(widgetChanges)) { - const container = layout.allItems[dragItemID].dragItem - container.attrs.hidden = isHidden; - container.attrsChanged.set(get(container.attrsChanged) + 1) - } - } -} - -LiteGraph.registerNodeType({ - class: ComfySetNodeModeAdvancedAction, - title: "Comfy.SetNodeModeAdvancedAction", - desc: "Turns multiple groups of nodes on/off at once based on an array of rules [{ tag: string, enable: boolean }, ...]", - type: "actions/set_node_mode_advanced" -}) - -export class ComfyNoChangeEvent extends ComfyGraphNode { - static slotLayout: SlotLayout = { - inputs: [ - { name: "in", type: BuiltInSlotType.ACTION }, - ], - outputs: [ - { name: "out", type: BuiltInSlotType.EVENT }, - ], - } - - override onAction(action: any, param: any, options: { action_call?: string }) { - if (param && typeof param === "object" && "noChangedEvent" in param) { - param.noChangedEvent = true; - } - else { - param = { - value: param, - noChangedEvent: true - } - } - - this.triggerSlot(0, param, null, options); - } -} - -LiteGraph.registerNodeType({ - class: ComfyNoChangeEvent, - title: "Comfy.NoChangeEvent", - desc: "Wraps an event's parameter such that passing it into a ComfyWidgetNode's 'store' action will not trigger its 'changed' event", - type: "events/no_change" -}) - -export interface ComfySetPromptThumbnailsActionProperties extends ComfyGraphNodeProperties { - defaultFolderType: string | null -} - -export class ComfySetPromptThumbnailsAction extends ComfyGraphNode { - override properties: ComfySetPromptThumbnailsActionProperties = { - tags: [], - defaultFolderType: "input", - } - - static slotLayout: SlotLayout = { - inputs: [ - { name: "filenames", type: "*" }, - ] - } - - _value: any = null; - - override getPromptThumbnails(): ComfyImageLocation[] | null { - const data = this.getInputData(0) - return parseWhateverIntoComfyImageLocations(data); - } -} - -LiteGraph.registerNodeType({ - class: ComfySetPromptThumbnailsAction, - title: "Comfy.SetPromptThumbnailsAction", - desc: "When a subgraph containing this node is executed, sets the thumbnails in the queue sidebar to the input filename(s).", - type: "actions/set_prompt_thumbnails" -}) diff --git a/src/lib/nodes/ComfyNoChangeEvent.ts b/src/lib/nodes/ComfyNoChangeEvent.ts new file mode 100644 index 0000000..f385ddb --- /dev/null +++ b/src/lib/nodes/ComfyNoChangeEvent.ts @@ -0,0 +1,34 @@ +import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core"; +import ComfyGraphNode from "./ComfyGraphNode"; + +export default class ComfyNoChangeEvent extends ComfyGraphNode { + static slotLayout: SlotLayout = { + inputs: [ + { name: "in", type: BuiltInSlotType.ACTION }, + ], + outputs: [ + { name: "out", type: BuiltInSlotType.EVENT }, + ], + } + + override onAction(action: any, param: any, options: { action_call?: string }) { + if (param && typeof param === "object" && "noChangedEvent" in param) { + param.noChangedEvent = true; + } + else { + param = { + value: param, + noChangedEvent: true + } + } + + this.triggerSlot(0, param, null, options); + } +} + +LiteGraph.registerNodeType({ + class: ComfyNoChangeEvent, + title: "Comfy.NoChangeEvent", + desc: "Wraps an event's parameter such that passing it into a ComfyWidgetNode's 'store' action will not trigger its 'changed' event", + type: "events/no_change" +}) diff --git a/src/lib/nodes/actions/ComfyCopyAction.ts b/src/lib/nodes/actions/ComfyCopyAction.ts new file mode 100644 index 0000000..d56445e --- /dev/null +++ b/src/lib/nodes/actions/ComfyCopyAction.ts @@ -0,0 +1,55 @@ +import { BuiltInSlotType, LiteGraph, type ITextWidget, type SlotLayout } from "@litegraph-ts/core"; +import ComfyGraphNode, { type ComfyGraphNodeProperties } from "../ComfyGraphNode"; + +export interface ComfyCopyActionProperties extends ComfyGraphNodeProperties { + value: any +} + +export default class ComfyCopyAction extends ComfyGraphNode { + override properties: ComfyCopyActionProperties = { + value: null, + tags: [] + } + + static slotLayout: SlotLayout = { + inputs: [ + { name: "in", type: "*" }, + { name: "copy", type: BuiltInSlotType.ACTION } + ], + outputs: [ + { name: "out", type: BuiltInSlotType.EVENT } + ], + } + + displayWidget: ITextWidget; + + constructor(title?: string) { + super(title); + this.displayWidget = this.addWidget( + "text", + "Value", + "", + "value" + ); + this.displayWidget.disabled = true; + } + + override onExecute() { + if (this.getInputLink(0)) + this.setProperty("value", this.getInputData(0)) + } + + override onAction(action: any, param: any) { + if (action === "copy") { + this.setProperty("value", this.getInputData(0)) + this.triggerSlot(0, this.properties.value) + } + }; +} + +LiteGraph.registerNodeType({ + class: ComfyCopyAction, + title: "Comfy.CopyAction", + desc: "Copies its input to its output when an event is received", + type: "actions/copy" +}) diff --git a/src/lib/nodes/actions/ComfyExecuteSubgraphAction.ts b/src/lib/nodes/actions/ComfyExecuteSubgraphAction.ts new file mode 100644 index 0000000..69b8b41 --- /dev/null +++ b/src/lib/nodes/actions/ComfyExecuteSubgraphAction.ts @@ -0,0 +1,52 @@ +import { BuiltInSlotType, LiteGraph, type ITextWidget, type SlotLayout } from "@litegraph-ts/core"; +import ComfyGraphNode, { type ComfyGraphNodeProperties } from "../ComfyGraphNode"; + +export interface ComfyExecuteSubgraphActionProperties extends ComfyGraphNodeProperties { + targetTag: string +} + +export default class ComfyExecuteSubgraphAction extends ComfyGraphNode { + override properties: ComfyExecuteSubgraphActionProperties = { + tags: [], + targetTag: "" + } + + static slotLayout: SlotLayout = { + inputs: [ + { name: "execute", type: BuiltInSlotType.ACTION }, + { name: "targetTag", type: "string" } + ], + } + + displayWidget: ITextWidget; + + constructor(title?: string) { + super(title) + this.displayWidget = this.addWidget("text", "targetTag", this.properties.targetTag, "targetTag") + } + + override onExecute() { + const tag = this.getInputData(1) + if (tag) + this.setProperty("tag", tag) + } + + override onAction(action: any, param: any) { + const tag = this.getInputData(1) || this.properties.targetTag; + + const app = (window as any)?.app; + if (!app) + return; + + // Hold control to queue at the front + const num = app.ctrlDown ? -1 : 0; + app.queuePrompt(num, 1, tag); + } +} + +LiteGraph.registerNodeType({ + class: ComfyExecuteSubgraphAction, + title: "Comfy.ExecuteSubgraphAction", + desc: "Runs a part of the graph based on a tag", + type: "actions/execute_subgraph" +}) diff --git a/src/lib/nodes/actions/ComfyNotifyAction.ts b/src/lib/nodes/actions/ComfyNotifyAction.ts new file mode 100644 index 0000000..5c495f0 --- /dev/null +++ b/src/lib/nodes/actions/ComfyNotifyAction.ts @@ -0,0 +1,57 @@ +import type { NotifyOptions } from "$lib/notify"; +import notify from "$lib/notify"; +import { convertComfyOutputToGradio, type SerializedPromptOutput } from "$lib/utils"; +import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core"; +import ComfyGraphNode, { type ComfyGraphNodeProperties } from "../ComfyGraphNode"; + +export interface ComfyNotifyActionProperties extends ComfyGraphNodeProperties { + message: string, + type: string +} + +export default class ComfyNotifyAction extends ComfyGraphNode { + override properties: ComfyNotifyActionProperties = { + tags: [], + message: "Nya.", + type: "info" + } + + static slotLayout: SlotLayout = { + inputs: [ + { name: "message", type: "string" }, + { name: "trigger", type: BuiltInSlotType.ACTION } + ], + } + + override onAction(action: any, param: any) { + const message = this.getInputData(0) || this.properties.message; + if (!message) + return; + + const options: NotifyOptions = { + type: this.properties.type, + showOn: "all" + } + + // Check if this event was triggered from a backend node and has the + // onExecuted arguments. If so then use the first image as the icon for + // native notifications. + if (param != null && typeof param === "object") { + if ("images" in param) { + const output = param as SerializedPromptOutput; + const converted = convertComfyOutputToGradio(output); + if (converted.length > 0) + options.imageUrl = converted[0].data; + } + } + + notify(message, options); + }; +} + +LiteGraph.registerNodeType({ + class: ComfyNotifyAction, + title: "Comfy.NotifyAction", + desc: "Displays a message.", + type: "actions/notify" +}) diff --git a/src/lib/nodes/actions/ComfyPlaySoundAction.ts b/src/lib/nodes/actions/ComfyPlaySoundAction.ts new file mode 100644 index 0000000..22c34ca --- /dev/null +++ b/src/lib/nodes/actions/ComfyPlaySoundAction.ts @@ -0,0 +1,36 @@ +import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core"; +import ComfyGraphNode, { type ComfyGraphNodeProperties } from "../ComfyGraphNode"; + +export interface ComfyPlaySoundActionProperties extends ComfyGraphNodeProperties { + sound: string, +} + +export default class ComfyPlaySoundAction extends ComfyGraphNode { + override properties: ComfyPlaySoundActionProperties = { + tags: [], + sound: "notification.mp3" + } + + static slotLayout: SlotLayout = { + inputs: [ + { name: "sound", type: "string" }, + { name: "trigger", type: BuiltInSlotType.ACTION } + ], + } + + override onAction(action: any, param: any) { + const sound = this.getInputData(0) || this.properties.sound; + if (sound) { + const url = `${location.origin}/sound/${sound}`; + const audio = new Audio(url); + audio.play(); + } + }; +} + +LiteGraph.registerNodeType({ + class: ComfyPlaySoundAction, + title: "Comfy.PlaySoundAction", + desc: "Plays a sound located under the sound/ directory.", + type: "actions/play_sound" +}) diff --git a/src/lib/nodes/actions/ComfyQueueEvents.ts b/src/lib/nodes/actions/ComfyQueueEvents.ts new file mode 100644 index 0000000..a528e34 --- /dev/null +++ b/src/lib/nodes/actions/ComfyQueueEvents.ts @@ -0,0 +1,57 @@ +import type { SerializedPrompt } from "$lib/components/ComfyApp"; +import queueState from "$lib/stores/queueState"; +import { BuiltInSlotType, LiteGraph, type SerializedLGraphNode, type SlotLayout } from "@litegraph-ts/core"; +import { get } from "svelte/store"; +import ComfyGraphNode from "../ComfyGraphNode"; + +export default class ComfyQueueEvents extends ComfyGraphNode { + static slotLayout: SlotLayout = { + outputs: [ + { name: "beforeQueued", type: BuiltInSlotType.EVENT }, + { name: "afterQueued", type: BuiltInSlotType.EVENT }, + { name: "onDefaultQueueAction", type: BuiltInSlotType.EVENT }, + ], + } + + private getActionParams(subgraph: string | null): any { + let queue = get(queueState) + let queueRemaining = 0; + + if (typeof queue.queueRemaining === "number") + queueRemaining = queue.queueRemaining + + return { + queueRemaining, + subgraph + } + } + + override beforeQueued(subgraph: string | null) { + this.triggerSlot(0, this.getActionParams(subgraph)) + } + + override afterQueued(p: SerializedPrompt, subgraph: string | null) { + this.triggerSlot(1, this.getActionParams(subgraph)) + } + + override onDefaultQueueAction() { + let queue = get(queueState) + let queueRemaining = 0; + + if (typeof queue.queueRemaining === "number") + queueRemaining = queue.queueRemaining + + this.triggerSlot(2, { queueRemaining }) + } + + override onSerialize(o: SerializedLGraphNode) { + super.onSerialize(o) + } +} + +LiteGraph.registerNodeType({ + class: ComfyQueueEvents, + title: "Comfy.QueueEvents", + desc: "Triggers a 'bang' event when a prompt is queued.", + type: "events/queue_events" +}) diff --git a/src/lib/nodes/actions/ComfySetNodeModeAction.ts b/src/lib/nodes/actions/ComfySetNodeModeAction.ts new file mode 100644 index 0000000..01197d5 --- /dev/null +++ b/src/lib/nodes/actions/ComfySetNodeModeAction.ts @@ -0,0 +1,88 @@ +import type { ComfyWidgetNode } from "$lib/nodes/widgets"; +import { BuiltInSlotType, LiteGraph, NodeMode, type ITextWidget, type IToggleWidget, type SlotLayout } from "@litegraph-ts/core"; +import { get } from "svelte/store"; +import ComfyGraphNode, { type ComfyGraphNodeProperties } from "../ComfyGraphNode"; + +export interface ComfySetNodeModeActionProperties extends ComfyGraphNodeProperties { + targetTags: string, + enable: boolean, +} + +export default class ComfySetNodeModeAction extends ComfyGraphNode { + override properties: ComfySetNodeModeActionProperties = { + targetTags: "", + enable: false, + tags: [] + } + + 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 === "enable") { + this.enableWidget.value = value + } + } + + override onAction(action: any, param: any) { + let input = this.getInputData(0) + if (input == null) + input = this.properties.enable + + let enabled = Boolean(input) + + 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); + if ("notifyPropsChanged" in node) + (node as ComfyWidgetNode).notifyPropsChanged(); + } + } + } + + for (const entry of Object.values(get(this.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; + } + 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/actions/ComfySetNodeModeAdvancedAction.ts b/src/lib/nodes/actions/ComfySetNodeModeAdvancedAction.ts new file mode 100644 index 0000000..3f86ce2 --- /dev/null +++ b/src/lib/nodes/actions/ComfySetNodeModeAdvancedAction.ts @@ -0,0 +1,143 @@ +import { type DragItemID } from "$lib/stores/layoutStates"; +import { BuiltInSlotType, LiteGraph, NodeMode, type ITextWidget, type IToggleWidget, type PropertyLayout, type SlotLayout } from "@litegraph-ts/core"; +import { get } from "svelte/store"; +import ComfyGraphNode, { type ComfyGraphNodeProperties } from "../ComfyGraphNode"; + +export type TagAction = { + tag: string, + enable: boolean +} + +export interface ComfySetNodeModeAdvancedActionProperties extends ComfyGraphNodeProperties { + targetTags: TagAction[], + enable: boolean, +} + +export default class ComfySetNodeModeAdvancedAction extends ComfyGraphNode { + override properties: ComfySetNodeModeAdvancedActionProperties = { + targetTags: [{ tag: "myTag", enable: true }, { tag: "anotherTag", enable: false }], + enable: true, + tags: [] + } + + static slotLayout: SlotLayout = { + inputs: [ + { name: "enabled", type: "boolean" }, + { name: "set", type: BuiltInSlotType.ACTION }, + ], + } + + static propertyLayout: PropertyLayout = [ + { name: "enable", defaultValue: true, type: "boolean", }, + { name: "targetTags", defaultValue: [{ tag: "myTag", enable: true }, { tag: "anotherTag", enable: false }], type: "array", options: { multiline: true, inputStyle: { fontFamily: "monospace" } } } + ] + + displayWidget: ITextWidget; + enableWidget: IToggleWidget; + + constructor(title?: string) { + super(title) + this.displayWidget = this.addWidget("text", "Tags", this.formatTags(), null); + this.displayWidget.disabled = true; + this.enableWidget = this.addWidget("toggle", "Enable", this.properties.enable, "enable"); + } + + override onPropertyChanged(property: any, value: any) { + if (property === "enable") { + this.enableWidget.value = value + } + else if (property === "targetTags") { + this.displayWidget.value = this.formatTags() + } + } + + private formatTags(): string { + if (!Array.isArray(this.properties.targetTags) || this.properties.targetTags.length === 0) + return "(No tags)"; + return this.properties.targetTags.map(t => { + let s = t.tag + if (t.enable) + s = "+" + s + else + s = "!" + s + return s + }).join(", ") + } + + private getModeChanges(action: TagAction, enable: boolean, nodeChanges: Record, widgetChanges: Record) { + for (const node of this.graph.iterateNodesInOrderRecursive()) { + if ("tags" in node.properties) { + const comfyNode = node as ComfyGraphNode; + const hasTag = comfyNode.properties.tags.indexOf(action.tag) != -1; + + if (hasTag) { + let newMode: NodeMode; + if (enable && action.enable) { + newMode = NodeMode.ALWAYS; + } else { + newMode = NodeMode.NEVER; + } + nodeChanges[node.id] = newMode + } + } + } + + for (const entry of Object.values(get(this.layoutState).allItems)) { + if (entry.dragItem.type === "container") { + const container = entry.dragItem; + const hasTag = container.attrs.tags.indexOf(action.tag) != -1; + if (hasTag) { + const hidden = !(enable && action.enable) + widgetChanges[container.id] = hidden + } + } + } + } + + override onExecute() { + this.boxcolor = LiteGraph.NODE_DEFAULT_BOXCOLOR; + + for (const action of this.properties.targetTags) { + if (typeof action !== "object" || !("tag" in action) || !("enable" in action)) { + this.boxcolor = "red"; + break; + } + } + } + + override onAction(action: any, param: any) { + let input = this.getInputData(0) + if (input == null) + input = this.properties.enable + + let enabled = Boolean(input) + + if (typeof param === "object" && "enabled" in param) + enabled = param["enabled"] + + const nodeChanges: Record = {} // nodeID => newState + const widgetChanges: Record = {} // dragItemID => isHidden + + for (const action of this.properties.targetTags) { + this.getModeChanges(action, enabled, nodeChanges, widgetChanges) + } + + for (const [nodeId, newMode] of Object.entries(nodeChanges)) { + this.graph.getNodeByIdRecursive(nodeId).changeMode(newMode); + } + + const layout = get(this.layoutState); + for (const [dragItemID, isHidden] of Object.entries(widgetChanges)) { + const container = layout.allItems[dragItemID].dragItem + container.attrs.hidden = isHidden; + container.attrsChanged.set(get(container.attrsChanged) + 1) + } + } +} + +LiteGraph.registerNodeType({ + class: ComfySetNodeModeAdvancedAction, + title: "Comfy.SetNodeModeAdvancedAction", + desc: "Turns multiple groups of nodes on/off at once based on an array of rules [{ tag: string, enable: boolean }, ...]", + type: "actions/set_node_mode_advanced" +}) diff --git a/src/lib/nodes/actions/ComfySetPromptThumbnailsAction.ts b/src/lib/nodes/actions/ComfySetPromptThumbnailsAction.ts new file mode 100644 index 0000000..9e5b67d --- /dev/null +++ b/src/lib/nodes/actions/ComfySetPromptThumbnailsAction.ts @@ -0,0 +1,34 @@ +import { parseWhateverIntoComfyImageLocations, type ComfyImageLocation } from "$lib/utils"; +import { LiteGraph, type SlotLayout } from "@litegraph-ts/core"; +import ComfyGraphNode, { type ComfyGraphNodeProperties } from "../ComfyGraphNode"; + +export interface ComfySetPromptThumbnailsActionProperties extends ComfyGraphNodeProperties { + defaultFolderType: string | null +} + +export default class ComfySetPromptThumbnailsAction extends ComfyGraphNode { + override properties: ComfySetPromptThumbnailsActionProperties = { + tags: [], + defaultFolderType: "input", + } + + static slotLayout: SlotLayout = { + inputs: [ + { name: "filenames", type: "*" }, + ] + } + + _value: any = null; + + override getPromptThumbnails(): ComfyImageLocation[] | null { + const data = this.getInputData(0) + return parseWhateverIntoComfyImageLocations(data); + } +} + +LiteGraph.registerNodeType({ + class: ComfySetPromptThumbnailsAction, + title: "Comfy.SetPromptThumbnailsAction", + desc: "When a subgraph containing this node is executed, sets the thumbnails in the queue sidebar to the input filename(s).", + type: "actions/set_prompt_thumbnails" +}) diff --git a/src/lib/nodes/actions/ComfyStoreImagesAction.ts b/src/lib/nodes/actions/ComfyStoreImagesAction.ts new file mode 100644 index 0000000..33957e2 --- /dev/null +++ b/src/lib/nodes/actions/ComfyStoreImagesAction.ts @@ -0,0 +1,43 @@ +import { type SerializedPromptOutput } from "$lib/utils"; +import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core"; +import ComfyGraphNode, { type ComfyGraphNodeProperties } from "../ComfyGraphNode"; + +export interface ComfyStoreImagesActionProperties extends ComfyGraphNodeProperties { + images: SerializedPromptOutput | null +} + +export default class ComfyStoreImagesAction extends ComfyGraphNode { + override properties: ComfyStoreImagesActionProperties = { + tags: [], + images: null + } + + static slotLayout: SlotLayout = { + inputs: [ + { name: "output", type: BuiltInSlotType.ACTION, options: { color_off: "rebeccapurple", color_on: "rebeccapurple" } } + ], + outputs: [ + { name: "images", type: "OUTPUT" }, + ], + } + + override onExecute() { + if (this.properties.images !== null) + this.setOutputData(0, this.properties.images) + } + + override onAction(action: any, param: any) { + if (action !== "store" || !param || !("images" in param)) + return; + + this.setProperty("images", param as SerializedPromptOutput) + this.setOutputData(0, this.properties.images) + } +} + +LiteGraph.registerNodeType({ + class: ComfyStoreImagesAction, + title: "Comfy.StoreImagesAction", + desc: "Stores images from an onExecuted callback", + type: "actions/store_images" +}) diff --git a/src/lib/nodes/actions/ComfySwapAction.ts b/src/lib/nodes/actions/ComfySwapAction.ts new file mode 100644 index 0000000..9b1ecec --- /dev/null +++ b/src/lib/nodes/actions/ComfySwapAction.ts @@ -0,0 +1,30 @@ +import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core"; +import ComfyGraphNode from "../ComfyGraphNode"; + +export default class ComfySwapAction extends ComfyGraphNode { + static slotLayout: SlotLayout = { + inputs: [ + { name: "A", type: "*" }, + { name: "B", type: "*" }, + { name: "swap", type: BuiltInSlotType.ACTION } + ], + outputs: [ + { name: "B", type: BuiltInSlotType.EVENT }, + { name: "A", type: BuiltInSlotType.EVENT } + ], + } + + override onAction(action: any, param: any) { + const a = this.getInputData(0) + const b = this.getInputData(1) + this.triggerSlot(0, b) + this.triggerSlot(1, a) + }; +} + +LiteGraph.registerNodeType({ + class: ComfySwapAction, + title: "Comfy.SwapAction", + desc: "Swaps two inputs when triggered", + type: "actions/swap" +}) diff --git a/src/lib/nodes/actions/index.ts b/src/lib/nodes/actions/index.ts new file mode 100644 index 0000000..383601a --- /dev/null +++ b/src/lib/nodes/actions/index.ts @@ -0,0 +1,10 @@ +export { default as ComfyCopyAction } from "./ComfyCopyAction" +export { default as ComfyExecuteSubgraphAction } from "./ComfyExecuteSubgraphAction" +export { default as ComfyNotifyAction } from "./ComfyNotifyAction" +export { default as ComfyPlaySoundAction } from "./ComfyPlaySoundAction" +export { default as ComfyQueueEvents } from "./ComfyQueueEvents" +export { default as ComfySetNodeModeAction } from "./ComfySetNodeModeAction" +export { default as ComfySetNodeModeAdvancedAction } from "./ComfySetNodeModeAdvancedAction" +export { default as ComfySetPromptThumbnailsAction } from "./ComfySetPromptThumbnailsAction" +export { default as ComfyStoreImagesAction } from "./ComfyStoreImagesAction" +export { default as ComfySwapAction } from "./ComfySwapAction" diff --git a/src/lib/nodes/index.ts b/src/lib/nodes/index.ts index fbe0e68..738410f 100644 --- a/src/lib/nodes/index.ts +++ b/src/lib/nodes/index.ts @@ -1,18 +1,8 @@ export { default as ComfyReroute } from "./ComfyReroute" -export { - ComfyQueueEvents, - ComfyCopyAction, - ComfySwapAction, - ComfyNotifyAction, - ComfyPlaySoundAction, - ComfyStoreImagesAction, - ComfyExecuteSubgraphAction, - ComfySetNodeModeAction, - ComfySetNodeModeAdvancedAction -} from "./ComfyActionNodes" export { default as ComfyPickFirstNode } from "./ComfyPickFirstNode" export { default as ComfyValueControl } from "./ComfyValueControl" export { default as ComfySelector } from "./ComfySelector" export { default as ComfyTriggerNewEventNode } from "./ComfyTriggerNewEventNode" export { default as ComfyConfigureQueuePromptButton } from "./ComfyConfigureQueuePromptButton" export { default as ComfyPickImageNode } from "./ComfyPickImageNode" +export { default as ComfyNoChangeEvent } from "./ComfyNoChangeEvent"