diff --git a/litegraph b/litegraph index 0e4bbd2..fd56d0c 160000 --- a/litegraph +++ b/litegraph @@ -1 +1 @@ -Subproject commit 0e4bbd2169661f1ea47d7abccdba071bc0ba1c9a +Subproject commit fd56d0c4e698b7a5bfccb1aa86e15db0f16120df diff --git a/package.json b/package.json index 3b33289..23ae656 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@litegraph-ts/core": "workspace:*", "@litegraph-ts/nodes-basic": "workspace:*", "@litegraph-ts/nodes-events": "workspace:*", + "@litegraph-ts/nodes-math": "workspace:*", "@litegraph-ts/tsconfig": "workspace:*", "@sveltejs/vite-plugin-svelte": "^2.1.1", "@tsconfig/svelte": "^4.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4eb0430..0fccfd3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,6 +40,9 @@ importers: '@litegraph-ts/nodes-events': specifier: workspace:* version: link:litegraph/packages/nodes-events + '@litegraph-ts/nodes-math': + specifier: workspace:* + version: link:litegraph/packages/nodes-math '@litegraph-ts/tsconfig': specifier: workspace:* version: link:litegraph/packages/tsconfig @@ -786,6 +789,22 @@ importers: specifier: ^4.2.1 version: 4.3.1 + litegraph/packages/nodes-math: + 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/tsconfig: {} packages: diff --git a/src/lib/ComfyGraph.ts b/src/lib/ComfyGraph.ts index 6e5d82a..28778be 100644 --- a/src/lib/ComfyGraph.ts +++ b/src/lib/ComfyGraph.ts @@ -8,6 +8,7 @@ import { get } from "svelte/store"; import type ComfyGraphNode from "./nodes/ComfyGraphNode"; import type IComfyInputSlot from "./IComfyInputSlot"; import type { ComfyBackendNode } from "./nodes/ComfyBackendNode"; +import type { ComfyWidgetNode } from "./nodes"; type ComfyGraphEvents = { configured: (graph: LGraph) => void @@ -52,31 +53,56 @@ export default class ComfyGraph extends LGraph { layoutState.nodeAdded(node) this.graphSync.onNodeAdded(node); - if ("comfyClass" in node // Is this a comfy node - && !("svelteComponentType" in node) // ...and not also a ComfyWidgetNode - && !options.addedByDeserialize // ...and we're not trying to deserialize an existing workflow - && get(uiState).autoAddUI) { - console.debug("[ComfyGraph] AutoAdd UI") - const comfyNode = node as ComfyBackendNode; - const widgetNodesAdded = [] - for (let index = 0; index < comfyNode.inputs.length; index++) { - const input = comfyNode.inputs[index]; - if ("config" in input) { - const comfyInput = input as IComfyInputSlot; - if (comfyInput.defaultWidgetNode) { - const widgetNode = LiteGraph.createNode(comfyInput.defaultWidgetNode) - const inputPos = comfyNode.getConnectionPos(true, index); - this.add(widgetNode) - widgetNode.connect(0, comfyNode, index); - widgetNode.collapse(); - widgetNode.pos = [inputPos[0] - 140, inputPos[1] + LiteGraph.NODE_SLOT_HEIGHT / 2]; - widgetNodesAdded.push(widgetNode) + if ("outputProperties" in node) { + const widgetNode = node as ComfyWidgetNode; + for (const propName of widgetNode.outputProperties) { + widgetNode.addPropertyAsOutput(propName.name, propName.type) + } + } + + // Check if the class declared a default widget layout + if ("defaultWidgets" in node && !("svelteComponentType" in node)) { + const comfyNode = node as ComfyGraphNode; + const widgets = comfyNode.defaultWidgets; + + if (widgets) { + if (widgets.inputs) { + for (const pair of Object.entries(comfyNode.defaultWidgets.inputs)) { + const [index, spec] = pair + const input = comfyNode.inputs[index] as IComfyInputSlot; + input.defaultWidgetNode = spec.defaultWidgetNode; + if (spec.config) + input.config = spec.config } } } - const dragItems = widgetNodesAdded.map(wn => get(layoutState).allItemsByNode[wn.id]?.dragItem).filter(di => di) - console.debug("[ComfyGraph] Group new widgets", dragItems) - layoutState.groupItems(dragItems, { title: comfyNode.comfyClass }) + } + + if (get(uiState).autoAddUI) { + if (!("svelteComponentType" in node) && !options.addedByDeserialize) { + console.debug("[ComfyGraph] AutoAdd UI") + const comfyNode = node as ComfyGraphNode; + const widgetNodesAdded = [] + for (let index = 0; index < comfyNode.inputs.length; index++) { + const input = comfyNode.inputs[index]; + if ("config" in input) { + const comfyInput = input as IComfyInputSlot; + if (comfyInput.defaultWidgetNode) { + const widgetNode = LiteGraph.createNode(comfyInput.defaultWidgetNode) + const inputPos = comfyNode.getConnectionPos(true, index); + this.add(widgetNode) + widgetNode.connect(0, comfyNode, index); + widgetNode.collapse(); + widgetNode.pos = [inputPos[0] - 140, inputPos[1] + LiteGraph.NODE_SLOT_HEIGHT / 2]; + widgetNodesAdded.push(widgetNode) + } + } + } + const dragItems = widgetNodesAdded.map(wn => get(layoutState).allItemsByNode[wn.id]?.dragItem).filter(di => di) + console.debug("[ComfyGraph] Group new widgets", dragItems) + + layoutState.groupItems(dragItems, { title: node.title }) + } } console.debug("Added", node); diff --git a/src/lib/components/ComfyApp.svelte b/src/lib/components/ComfyApp.svelte index ed7b3ce..e785f7b 100644 --- a/src/lib/components/ComfyApp.svelte +++ b/src/lib/components/ComfyApp.svelte @@ -26,7 +26,7 @@ let resizeTimeout: NodeJS.Timeout | null; let hasShownUIHelpToast: boolean = false; - let debugLayout: boolean = true; + let debugLayout: boolean = false; const toastOptions = { intro: { duration: 200 }, diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts index 22193eb..2d149a8 100644 --- a/src/lib/components/ComfyApp.ts +++ b/src/lib/components/ComfyApp.ts @@ -1,7 +1,6 @@ import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink, NodeMode, type Vector2, BuiltInSlotType } from "@litegraph-ts/core"; import type { LConnectionKind, INodeSlot } from "@litegraph-ts/core"; import ComfyAPI from "$lib/api" -import { ComfyWidgets } from "$lib/widgets" import defaultGraph from "$lib/defaultGraph" import { getPngMetadata, importA1111 } from "$lib/pnginfo"; import EventEmitter from "events"; @@ -10,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-math" import * as nodes from "$lib/nodes/index" import ComfyGraphCanvas, { type SerializedGraphCanvasState } from "$lib/ComfyGraphCanvas"; @@ -482,17 +482,19 @@ export default class ComfyApp { for (let i = 0; i < node.inputs.length; i++) { let parent: ComfyGraphNode = node.getInputNode(i) as ComfyGraphNode; if (parent) { + const seen = {} let link = node.getInputLink(i); while (parent && !parent.isBackendNode) { link = parent.getInputLink(link.origin_slot); - if (link) { + if (link && !seen[link.id]) { + seen[link.id] = true parent = parent.getInputNode(link.origin_slot) as ComfyGraphNode; } else { parent = null; } } - if (link) { + if (link && parent && parent.isBackendNode) { const input = node.inputs[i] // TODO can null be a legitimate value in some cases? // Nodes like CLIPLoader will never have a value in the frontend, hence "null". diff --git a/src/lib/defaultGraph.ts b/src/lib/defaultGraph.ts index 23ca580..1046398 100644 --- a/src/lib/defaultGraph.ts +++ b/src/lib/defaultGraph.ts @@ -1,19 +1,21 @@ import type SerializedAppState from "./ComfyApp" const defaultGraph: SerializedAppState = { - createdBy: "ComfyBox", - version: 1, - workflow: { - last_node_id: 0, - last_link_id: 0, - nodes: [], - links: [], - groups: [], - config: {}, - extra: {}, - version: 0 - }, - panes: {} + createdBy: "ComfyBox", + version: 1, + workflow: { + last_node_id: 0, + last_link_id: 0, + nodes: [], + links: [], + groups: [], + config: {}, + extra: {}, + version: 0 + }, + panes: {} } export default defaultGraph; + +// {"createdBy":"ComfyBox","version":1,"workflow":{"last_node_id":55,"last_link_id":54,"nodes":[{"id":17,"type":"ui/slider","pos":[77.09750000000287,195.9643000000003],"size":[210,78],"flags":{"collapsed":true},"order":0,"mode":0,"inputs":[{"name":"value","type":"number","link":null}],"outputs":[{"name":"value","type":"number","links":[16]},{"name":"changed","type":-2,"links":null,"shape":1}],"title":"UI.Slider","properties":{"defaultValue":0,"min":0,"max":18446744073709552000,"step":1,"precision":0},"widgets_values":["9507460842217379840.000"],"color":"#223","bgColor":"#335","comfyValue":9507460842217380000},{"id":18,"type":"ui/slider","pos":[77.09750000000287,215.9643000000003],"size":[210,78],"flags":{"collapsed":true},"order":1,"mode":0,"inputs":[{"name":"value","type":"number","link":null}],"outputs":[{"name":"value","type":"number","links":[17]},{"name":"changed","type":-2,"links":null,"shape":1}],"title":"UI.Slider","properties":{"defaultValue":20,"min":1,"max":10000,"step":1,"precision":0},"widgets_values":["20.000"],"color":"#223","bgColor":"#335","comfyValue":20},{"id":19,"type":"ui/slider","pos":[77.09750000000287,235.9643000000003],"size":[210,78],"flags":{"collapsed":true},"order":2,"mode":0,"inputs":[{"name":"value","type":"number","link":null}],"outputs":[{"name":"value","type":"number","links":[18]},{"name":"changed","type":-2,"links":null,"shape":1}],"title":"UI.Slider","properties":{"defaultValue":8,"min":0,"max":100,"step":0.5,"precision":0},"widgets_values":["8.000"],"color":"#223","bgColor":"#335","comfyValue":8},{"id":20,"type":"ui/combo","pos":[77.09750000000287,255.9643000000003],"size":[210,78],"flags":{"collapsed":true},"order":3,"mode":0,"inputs":[{"name":"value","type":"string","link":null}],"outputs":[{"name":"value","type":"string","links":[19]},{"name":"changed","type":-2,"links":null,"shape":1}],"title":"UI.Combo","properties":{"defaultValue":"euler","values":["euler","euler_ancestral","heun","dpm_2","dpm_2_ancestral","lms","dpm_fast","dpm_adaptive","dpmpp_2s_ancestral","dpmpp_sde","dpmpp_2m","ddim","uni_pc","uni_pc_bh2"]},"widgets_values":["euler"],"color":"#223","bgColor":"#335","comfyValue":"euler"},{"id":21,"type":"ui/combo","pos":[77.09750000000287,275.9643000000003],"size":[210,78],"flags":{"collapsed":true},"order":4,"mode":0,"inputs":[{"name":"value","type":"string","link":null}],"outputs":[{"name":"value","type":"string","links":[20]},{"name":"changed","type":-2,"links":null,"shape":1}],"title":"UI.Combo","properties":{"defaultValue":"karras","values":["karras","normal","simple","ddim_uniform"]},"widgets_values":["karras"],"color":"#223","bgColor":"#335","comfyValue":"karras"},{"id":22,"type":"ui/slider","pos":[77.09750000000287,355.9643000000003],"size":[210,78],"flags":{"collapsed":true},"order":5,"mode":0,"inputs":[{"name":"value","type":"number","link":null}],"outputs":[{"name":"value","type":"number","links":[21]},{"name":"changed","type":-2,"links":null,"shape":1}],"title":"UI.Slider","properties":{"defaultValue":1,"min":0,"max":1,"step":0.01,"precision":0},"widgets_values":["1.000"],"color":"#223","bgColor":"#335","comfyValue":1},{"id":31,"type":"ui/combo","pos":[-448.0424999999954,103.36430000000071],"size":[210,78],"flags":{"collapsed":true},"order":6,"mode":0,"inputs":[{"name":"value","type":"string","link":null}],"outputs":[{"name":"value","type":"string","links":[28]},{"name":"changed","type":-2,"links":null,"shape":1}],"title":"UI.Combo","properties":{"defaultValue":"AbyssOrangeMix2_nsfw.safetensors","values":["AbyssOrangeMix2_nsfw.safetensors"]},"widgets_values":["AbyssOrangeMix2_nsfw.safetensors"],"color":"#223","bgColor":"#335","comfyValue":"AbyssOrangeMix2_nsfw.safetensors"},{"id":33,"type":"ui/text","pos":[-347.6124999999959,309.06430000000114],"size":[210,78],"flags":{"collapsed":true},"order":7,"mode":0,"inputs":[{"name":"value","type":"string","link":null}],"outputs":[{"name":"value","type":"string","links":[30]},{"name":"changed","type":-2,"links":null,"shape":1}],"title":"UI.Text","properties":{"defaultValue":"","multiline":true},"widgets_values":["masterpiece, best quality, 1girl, dress"],"color":"#223","bgColor":"#335","comfyValue":"masterpiece, best quality, 1girl, dress"},{"id":35,"type":"ui/text","pos":[-346.40249999999617,399.8143000000014],"size":[210,78],"flags":{"collapsed":true},"order":8,"mode":0,"inputs":[{"name":"value","type":"string","link":null}],"outputs":[{"name":"value","type":"string","links":[31]},{"name":"changed","type":-2,"links":null,"shape":1}],"title":"UI.Text","properties":{"defaultValue":"","multiline":true},"widgets_values":["worst quality, bad quality, nsfw"],"color":"#223","bgColor":"#335","comfyValue":"worst quality, bad quality, nsfw"},{"id":37,"type":"ui/slider","pos":[-350.03249999999764,514.0383000000008],"size":[210,78],"flags":{"collapsed":true},"order":9,"mode":0,"inputs":[{"name":"value","type":"number","link":null}],"outputs":[{"name":"value","type":"number","links":[36]},{"name":"changed","type":-2,"links":null,"shape":1}],"title":"UI.Slider","properties":{"defaultValue":512,"min":64,"max":8192,"step":8,"precision":0},"widgets_values":["64.000"],"color":"#223","bgColor":"#335","comfyValue":64},{"id":38,"type":"ui/slider","pos":[-350.03249999999764,534.0383000000008],"size":[210,78],"flags":{"collapsed":true},"order":10,"mode":0,"inputs":[{"name":"value","type":"number","link":null}],"outputs":[{"name":"value","type":"number","links":[37]},{"name":"changed","type":-2,"links":null,"shape":1}],"title":"UI.Slider","properties":{"defaultValue":512,"min":64,"max":8192,"step":8,"precision":0},"widgets_values":["64.000"],"color":"#223","bgColor":"#335","comfyValue":64},{"id":39,"type":"ui/slider","pos":[-350.03249999999764,554.0383000000008],"size":[210,78],"flags":{"collapsed":true},"order":11,"mode":0,"inputs":[{"name":"value","type":"number","link":null}],"outputs":[{"name":"value","type":"number","links":[38]},{"name":"changed","type":-2,"links":null,"shape":1}],"title":"UI.Slider","properties":{"defaultValue":1,"min":1,"max":64,"step":1,"precision":0},"widgets_values":["4.000"],"color":"#223","bgColor":"#335","comfyValue":4},{"id":16,"type":"KSampler","pos":[242,156],"size":[241.79999999999998,206],"flags":{},"order":24,"mode":0,"inputs":[{"name":"model","type":"MODEL","link":29,"color_off":"orange","color_on":"orange"},{"name":"seed","type":"number","link":16,"config":{"min":0,"max":18446744073709552000,"step":1,"precision":0,"defaultValue":0},"serialize":true},{"name":"steps","type":"number","link":17,"config":{"min":1,"max":10000,"step":1,"precision":0,"defaultValue":20},"serialize":true},{"name":"cfg","type":"number","link":18,"config":{"min":0,"max":100,"step":0.5,"precision":0,"defaultValue":8},"serialize":true},{"name":"sampler_name","type":"string","link":19,"config":{"values":["euler","euler_ancestral","heun","dpm_2","dpm_2_ancestral","lms","dpm_fast","dpm_adaptive","dpmpp_2s_ancestral","dpmpp_sde","dpmpp_2m","ddim","uni_pc","uni_pc_bh2"],"defaultValue":"euler"},"serialize":true},{"name":"scheduler","type":"string","link":20,"config":{"values":["karras","normal","simple","ddim_uniform"],"defaultValue":"karras"},"serialize":true},{"name":"positive","type":"CONDITIONING","link":33,"color_off":"orange","color_on":"orange"},{"name":"negative","type":"CONDITIONING","link":32,"color_off":"orange","color_on":"orange"},{"name":"latent_image","type":"LATENT","link":39,"color_off":"orange","color_on":"orange"},{"name":"denoise","type":"number","link":21,"config":{"min":0,"max":1,"step":0.01,"precision":0,"defaultValue":1},"serialize":true}],"outputs":[{"name":"LATENT","type":"LATENT","links":[40],"color_off":"orange","color_on":"orange","slot_index":0}],"title":"KSampler","properties":{},"color":"#432","bgColor":"#653"},{"id":40,"type":"VAEDecode","pos":[532.5875000000013,160.9183000000009],"size":[210,46],"flags":{},"order":25,"mode":0,"inputs":[{"name":"samples","type":"LATENT","link":40,"color_off":"orange","color_on":"orange"},{"name":"vae","type":"VAE","link":41,"color_off":"orange","color_on":"orange"}],"outputs":[{"name":"IMAGE","type":"IMAGE","links":[44],"color_off":"orange","color_on":"orange","slot_index":0}],"title":"VAEDecode","properties":{},"color":"#432","bgColor":"#653"},{"id":45,"type":"ui/text","pos":[779.2900000000001,208.44999999999965],"size":[210,78],"flags":{"collapsed":true},"order":12,"mode":0,"inputs":[{"name":"value","type":"string","link":null}],"outputs":[{"name":"value","type":"string","links":[43]},{"name":"changed","type":-2,"links":null,"shape":1}],"title":"UI.Text","properties":{"defaultValue":"ComfyUI","multiline":false},"widgets_values":["ComfyUI"],"color":"#223","bgColor":"#335","comfyValue":"ComfyUI"},{"id":32,"type":"CLIPTextEncode","pos":[-218,285],"size":[216.60000000000002,46],"flags":{},"order":20,"mode":0,"inputs":[{"name":"text","type":"string","link":30,"config":{"defaultValue":"","multiline":true},"serialize":true},{"name":"clip","type":"CLIP","link":34,"color_off":"orange","color_on":"orange"}],"outputs":[{"name":"CONDITIONING","type":"CONDITIONING","links":[33],"color_off":"orange","color_on":"orange","slot_index":0}],"title":"CLIPTextEncode","properties":{},"color":"#432","bgColor":"#653"},{"id":34,"type":"CLIPTextEncode","pos":[-216,376],"size":[216.60000000000002,46],"flags":{},"order":21,"mode":0,"inputs":[{"name":"text","type":"string","link":31,"config":{"defaultValue":"","multiline":true},"serialize":true},{"name":"clip","type":"CLIP","link":35,"color_off":"orange","color_on":"orange"}],"outputs":[{"name":"CONDITIONING","type":"CONDITIONING","links":[32],"color_off":"orange","color_on":"orange","slot_index":0}],"title":"CLIPTextEncode","properties":{},"color":"#432","bgColor":"#653"},{"id":30,"type":"CheckpointLoaderSimple","pos":[-318,79],"size":[277.2,66],"flags":{},"order":16,"mode":0,"inputs":[{"name":"ckpt_name","type":"string","link":28,"config":{"values":["AbyssOrangeMix2_nsfw.safetensors"],"defaultValue":"AbyssOrangeMix2_nsfw.safetensors"},"serialize":true}],"outputs":[{"name":"MODEL","type":"MODEL","links":[29],"color_off":"orange","color_on":"orange","slot_index":0},{"name":"CLIP","type":"CLIP","links":[34,35],"color_off":"orange","color_on":"orange","slot_index":1},{"name":"VAE","type":"VAE","links":[41],"color_off":"orange","color_on":"orange","slot_index":2}],"title":"CheckpointLoaderSimple","properties":{},"color":"#432","bgColor":"#653"},{"id":36,"type":"EmptyLatentImage","pos":[-220,490],"size":[216.60000000000002,66],"flags":{},"order":17,"mode":0,"inputs":[{"name":"width","type":"number","link":36,"config":{"min":64,"max":8192,"step":8,"precision":0,"defaultValue":512},"serialize":true},{"name":"height","type":"number","link":37,"config":{"min":64,"max":8192,"step":8,"precision":0,"defaultValue":512},"serialize":true},{"name":"batch_size","type":"number","link":38,"config":{"min":1,"max":64,"step":1,"precision":0,"defaultValue":1},"serialize":true}],"outputs":[{"name":"LATENT","type":"LATENT","links":[39],"color_off":"orange","color_on":"orange","slot_index":0}],"title":"EmptyLatentImage","properties":{},"color":"#432","bgColor":"#653"},{"id":47,"type":"ui/gallery","pos":[1284,165],"size":[210,58],"flags":{},"order":27,"mode":0,"inputs":[{"name":"images","type":"OUTPUT","link":46}],"outputs":[],"title":"UI.Gallery","properties":{"defaultValue":[]},"widgets_values":["Images: 0"],"color":"#223","bgColor":"#335","comfyValue":[]},{"id":44,"type":"SaveImage","pos":[888,164],"size":[315,78],"flags":{},"order":26,"mode":0,"inputs":[{"name":"images","type":"IMAGE","link":44,"color_off":"orange","color_on":"orange","slot_index":0},{"name":"filename_prefix","type":"string","link":43,"config":{"defaultValue":"ComfyUI","multiline":false},"serialize":true}],"outputs":[{"name":"output","type":"OUTPUT","links":[46],"slot_index":0}],"title":"SaveImage","properties":{},"color":"#432","bgColor":"#653"},{"id":49,"type":"events/log","pos":[771,22],"size":[140,26],"flags":{},"order":18,"mode":0,"inputs":[{"name":"event","type":-1,"link":47,"shape":1}],"outputs":[],"title":"Log Event","properties":{}},{"id":48,"type":"ui/button","pos":[372,12],"size":[210,78],"flags":{},"order":13,"mode":0,"inputs":[],"outputs":[{"name":"event","type":-2,"links":[47],"shape":1,"slot_index":0},{"name":"isClicked","type":"boolean","links":null,"slot_index":1}],"title":"UI.Button","properties":{"defaultValue":false,"message":"bang"},"widgets_values":["false"],"color":"#223","bgColor":"#335","comfyValue":false},{"id":52,"type":"basic/watch","pos":[1063.1426107321954,-118.99396962098862],"size":[140,26],"flags":{},"order":22,"mode":0,"inputs":[{"name":"value","type":0,"link":50,"label":"null","slot_index":0}],"outputs":[],"title":"Watch","properties":{"value":1}},{"id":54,"type":"ui/slider","pos":[1038,-226],"size":[210,78],"flags":{},"order":23,"mode":0,"inputs":[{"name":"value","type":"number","link":52}],"outputs":[{"name":"value","type":"number","links":null},{"name":"changed","type":-2,"links":null,"shape":1}],"title":"UI.Slider","properties":{"defaultValue":0,"min":0,"max":10,"step":1,"precision":1},"widgets_values":["2.000"],"color":"#223","bgColor":"#335","comfyValue":2},{"id":53,"type":"ui/slider","pos":[354.5182107321954,-135.09906962098862],"size":[210,78],"flags":{},"order":14,"mode":0,"inputs":[{"name":"value","type":"number","link":null}],"outputs":[{"name":"value","type":"number","links":[51],"slot_index":0},{"name":"changed","type":-2,"links":[],"shape":1,"slot_index":1}],"title":"UI.Slider","properties":{"defaultValue":0,"min":0,"max":10,"step":1,"precision":1},"widgets_values":["2.000"],"color":"#223","bgColor":"#335","comfyValue":2},{"id":50,"type":"actions/copy","pos":[729,-125],"size":[210,78],"flags":{},"order":19,"mode":0,"inputs":[{"name":"","type":"*","link":51},{"name":"copy","type":-1,"link":54,"shape":1}],"outputs":[{"name":"","type":"*","links":[50,52],"slot_index":0}],"title":"Comfy.CopyAction","properties":{"value":2}},{"id":55,"type":"actions/after_queued","pos":[382,-333],"size":[193.2,46],"flags":{},"order":15,"mode":0,"inputs":[],"outputs":[{"name":"afterQueued","type":-2,"links":[54],"shape":1,"slot_index":0},{"name":"prompt","type":"*","links":null}],"title":"Comfy.AfterQueuedAction","properties":{"prompt":null}}],"links":[[16,17,0,16,1,"number"],[17,18,0,16,2,"number"],[18,19,0,16,3,"number"],[19,20,0,16,4,"string"],[20,21,0,16,5,"string"],[21,22,0,16,9,"number"],[28,31,0,30,0,"string"],[29,30,0,16,0,"MODEL"],[30,33,0,32,0,"string"],[31,35,0,34,0,"string"],[32,34,0,16,7,"CONDITIONING"],[33,32,0,16,6,"CONDITIONING"],[34,30,1,32,1,"CLIP"],[35,30,1,34,1,"CLIP"],[36,37,0,36,0,"number"],[37,38,0,36,1,"number"],[38,39,0,36,2,"number"],[39,36,0,16,8,"LATENT"],[40,16,0,40,0,"LATENT"],[41,30,2,40,1,"VAE"],[43,45,0,44,1,"string"],[44,40,0,44,0,"IMAGE"],[46,44,0,47,0,"OUTPUT"],[47,48,0,49,0,-1],[50,50,0,52,0,"*"],[51,53,0,50,0,"*"],[52,50,0,54,0,"number"],[54,55,0,50,1,-1]],"groups":[],"config":{},"extra":{},"version":10},"layout":{"root":"0","allItems":{"0":{"dragItem":{"type":"container","id":"0","attrs":{"title":"Container","showTitle":false,"direction":"horizontal","classes":""}},"children":["1","2"]},"1":{"dragItem":{"type":"container","id":"1","attrs":{"title":"Container","showTitle":false,"direction":"vertical","classes":""}},"children":["16","33","25","41"],"parent":"0"},"2":{"dragItem":{"type":"container","id":"2","attrs":{"title":"Container","showTitle":false,"direction":"vertical","classes":""}},"children":["37","35","27"],"parent":"0"},"10":{"dragItem":{"type":"widget","id":"10","nodeId":17,"attrs":{"title":"seed","showTitle":true,"direction":"horizontal","classes":""}},"children":[],"parent":"16"},"11":{"dragItem":{"type":"widget","id":"11","nodeId":18,"attrs":{"title":"steps","showTitle":true,"direction":"horizontal","classes":""}},"children":[],"parent":"16"},"12":{"dragItem":{"type":"widget","id":"12","nodeId":19,"attrs":{"title":"cfg","showTitle":true,"direction":"horizontal","classes":""}},"children":[],"parent":"16"},"13":{"dragItem":{"type":"widget","id":"13","nodeId":20,"attrs":{"title":"sampler_name","showTitle":true,"direction":"horizontal","classes":""}},"children":[],"parent":"16"},"14":{"dragItem":{"type":"widget","id":"14","nodeId":21,"attrs":{"title":"scheduler","showTitle":true,"direction":"horizontal","classes":""}},"children":[],"parent":"16"},"15":{"dragItem":{"type":"widget","id":"15","nodeId":22,"attrs":{"title":"denoise","showTitle":true,"direction":"horizontal","classes":""}},"children":[],"parent":"16"},"16":{"dragItem":{"type":"container","id":"16","attrs":{"title":"KSampler","showTitle":true,"direction":"vertical","classes":""}},"children":["10","11","12","13","14","15"],"parent":"1"},"24":{"dragItem":{"type":"widget","id":"24","nodeId":31,"attrs":{"title":"ckpt_name","showTitle":true,"direction":"horizontal","classes":""}},"children":[],"parent":"25"},"25":{"dragItem":{"type":"container","id":"25","attrs":{"title":"CheckpointLoaderSimple","showTitle":true,"direction":"vertical","classes":""}},"children":["24"],"parent":"1"},"26":{"dragItem":{"type":"widget","id":"26","nodeId":33,"attrs":{"title":"text","showTitle":true,"direction":"horizontal","classes":""}},"children":[],"parent":"27"},"27":{"dragItem":{"type":"container","id":"27","attrs":{"title":"Conditioning","showTitle":true,"direction":"vertical","classes":""}},"children":["26","28"],"parent":"2"},"28":{"dragItem":{"type":"widget","id":"28","nodeId":35,"attrs":{"title":"text","showTitle":true,"direction":"horizontal","classes":""}},"children":[],"parent":"27"},"30":{"dragItem":{"type":"widget","id":"30","nodeId":37,"attrs":{"title":"width","showTitle":true,"direction":"horizontal","classes":""}},"children":[],"parent":"33"},"31":{"dragItem":{"type":"widget","id":"31","nodeId":38,"attrs":{"title":"height","showTitle":true,"direction":"horizontal","classes":""}},"children":[],"parent":"33"},"32":{"dragItem":{"type":"widget","id":"32","nodeId":39,"attrs":{"title":"batch_size","showTitle":true,"direction":"horizontal","classes":""}},"children":[],"parent":"33"},"33":{"dragItem":{"type":"container","id":"33","attrs":{"title":"EmptyLatentImage","showTitle":true,"direction":"vertical","classes":""}},"children":["30","31","32"],"parent":"1"},"34":{"dragItem":{"type":"widget","id":"34","nodeId":45,"attrs":{"title":"filename_prefix","showTitle":true,"direction":"horizontal","classes":""}},"children":[],"parent":"35"},"35":{"dragItem":{"type":"container","id":"35","attrs":{"title":"SaveImage","showTitle":true,"direction":"vertical","classes":""}},"children":["34"],"parent":"2"},"37":{"dragItem":{"type":"widget","id":"37","nodeId":47,"attrs":{"title":"Widget","showTitle":true,"direction":"horizontal","classes":""}},"children":[],"parent":"2"},"38":{"dragItem":{"type":"widget","id":"38","nodeId":48,"attrs":{"title":"copy","showTitle":true,"direction":"horizontal","classes":""}},"children":[],"parent":"41"},"39":{"dragItem":{"type":"widget","id":"39","nodeId":53,"attrs":{"title":"copy","showTitle":true,"direction":"horizontal","classes":""}},"children":[],"parent":"41"},"40":{"dragItem":{"type":"widget","id":"40","nodeId":54,"attrs":{"title":"Widget","showTitle":true,"direction":"horizontal","classes":""}},"children":[],"parent":"41"},"41":{"dragItem":{"type":"container","id":"41","attrs":{"title":"Copy Test","showTitle":true,"direction":"horizontal","classes":""}},"children":["39","38","40"],"parent":"1"}},"currentId":42},"canvas":{"offset":[340.9292892678049,426.94297316713084],"scale":0.6830134553650706}} diff --git a/src/lib/nodes/ComfyGraphNode.ts b/src/lib/nodes/ComfyGraphNode.ts index bbe87d1..38cc9a5 100644 --- a/src/lib/nodes/ComfyGraphNode.ts +++ b/src/lib/nodes/ComfyGraphNode.ts @@ -1,10 +1,24 @@ +import type { ComfyInputConfig } from "$lib/IComfyInputSlot"; import type { SerializedPrompt } from "$lib/components/ComfyApp"; import type ComfyWidget from "$lib/components/widgets/ComfyWidget"; import { LGraph, LGraphNode } from "@litegraph-ts/core"; +import type { SvelteComponentDev } from "svelte/internal"; +import type { ComfyWidgetNode } from "./ComfyWidgetNodes"; + +export type DefaultWidgetSpec = { + defaultWidgetNode: new (name?: string) => ComfyWidgetNode, + config?: ComfyInputConfig +} + +export type DefaultWidgetLayout = { + inputs?: Record, +} export default class ComfyGraphNode extends LGraphNode { isBackendNode?: boolean; afterQueued?(prompt: SerializedPrompt): void; onExecuted?(output: any): void; + + defaultWidgets?: DefaultWidgetLayout } diff --git a/src/lib/nodes/ComfySelector.ts b/src/lib/nodes/ComfySelector.ts new file mode 100644 index 0000000..d152d9c --- /dev/null +++ b/src/lib/nodes/ComfySelector.ts @@ -0,0 +1,120 @@ +import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core"; +import ComfyGraphNode from "./ComfyGraphNode"; + +export interface ComfySelectorProperties extends Record { + value: any +} + +export default class ComfySelector extends ComfyGraphNode { + override properties: ComfySelectorProperties = { + value: null + } + + static slotLayout: SlotLayout = { + inputs: [ + { name: "select", type: "number" }, + { name: "A", type: "*" }, + { name: "B", type: "*" }, + { name: "C", type: "*" }, + { name: "D", type: "*" }, + ], + outputs: [ + { name: "out", type: "*" } + ], + } + + private selected: number = 0; + + constructor(title?: string) { + super(title); + } + + override onDrawBackground(ctx: CanvasRenderingContext2D) { + if (this.flags.collapsed) { + return; + } + ctx.fillStyle = "#AFB"; + var y = (this.selected + 1) * 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 onExecute() { + var sel = this.getInputData(0); + if (sel == null || sel.constructor !== Number) + sel = 0; + this.selected = sel = Math.round(sel) % (this.inputs.length - 1); + var v = this.getInputData(sel + 1); + if (v !== undefined) { + this.setOutputData(0, v); + } + } +} + +LiteGraph.registerNodeType({ + class: ComfySelector, + title: "Comfy.Selector", + desc: "Selects an output from two or more inputs", + type: "utils/selector" +}) + +export interface ComfySelectorTwoProperties extends Record { + value: any +} + +export class ComfySelectorTwo extends ComfyGraphNode { + override properties: ComfySelectorTwoProperties = { + value: null + } + + static slotLayout: SlotLayout = { + inputs: [ + { name: "select", type: "boolean" }, + { name: "true", type: "*" }, + { name: "false", type: "*" }, + ], + outputs: [ + { name: "out", type: "*" } + ], + } + + private selected: number = 0; + + constructor(title?: string) { + super(title); + } + + override onDrawBackground(ctx: CanvasRenderingContext2D) { + if (this.flags.collapsed) { + return; + } + ctx.fillStyle = "#AFB"; + var y = (this.selected + 1) * 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 onExecute() { + var sel = this.getInputData(0); + if (sel == null || sel.constructor !== Boolean) + sel = 0; + this.selected = sel ? 0 : 1; + var v = this.getInputData(this.selected + 1); + if (v !== undefined) { + this.setOutputData(0, v); + } + } +} + +LiteGraph.registerNodeType({ + class: ComfySelectorTwo, + title: "Comfy.Selector2", + desc: "Selects an output from two inputs with a boolean", + type: "utils/selector2" +}) diff --git a/src/lib/nodes/ComfyValueControl.ts b/src/lib/nodes/ComfyValueControl.ts new file mode 100644 index 0000000..fed07ec --- /dev/null +++ b/src/lib/nodes/ComfyValueControl.ts @@ -0,0 +1,107 @@ +import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core"; +import ComfyGraphNode, { type DefaultWidgetLayout } from "./ComfyGraphNode"; +import { clamp } from "$lib/utils"; +import ComboWidget from "$lib/widgets/ComboWidget.svelte"; +import { ComfyComboNode } from "./ComfyWidgetNodes"; + +export interface ComfyValueControlProperties extends Record { + value: any, + action: "fixed" | "increment" | "decrement" | "randomize", + min: number, + max: number, + step: number +} + +const INT_MAX = 1125899906842624; + +export default class ComfyValueControl extends ComfyGraphNode { + override properties: ComfyValueControlProperties = { + value: null, + action: "fixed", + min: -INT_MAX, + max: INT_MAX, + step: 1 + } + + static slotLayout: SlotLayout = { + inputs: [ + { name: "value", type: "number" }, + { name: "trigger", type: BuiltInSlotType.ACTION }, + { name: "action", type: "string" }, + { name: "min", type: "number" }, + { name: "max", type: "number" }, + { name: "step", type: "number" } + ], + outputs: [ + { name: "value", type: "*" } + ], + } + + override defaultWidgets: DefaultWidgetLayout = { + inputs: { + 2: { + defaultWidgetNode: ComfyComboNode, + config: { + defaultValue: "randomize", + values: ["fixed", "increment", "decrement", "randomize"] + } + } + } + } + + constructor(title?: string) { + super(title); + } + + override onExecute() { + this.setProperty("action", this.getInputData(2) || "fixed") + this.setProperty("min", this.getInputData(3)) + this.setProperty("max", this.getInputData(4)) + this.setProperty("step", this.getInputData(5) || 1) + } + + override onAction(action: any, param: any) { + var v = this.getInputData(0) + if (typeof v !== "number") + return + + let min = this.properties.min + let max = this.properties.max + if (min == null) min = -INT_MAX + if (max == null) max = INT_MAX + + // limit to something that javascript can handle + min = Math.max(-INT_MAX, this.properties.min); + max = Math.min(INT_MAX, this.properties.max); + let range = (max - min) / (this.properties.step); + + //adjust values based on valueControl Behaviour + switch (this.properties.action) { + case "fixed": + break; + case "increment": + v += this.properties.step; + break; + case "decrement": + v -= this.properties.step; + break; + case "randomize": + v = Math.floor(Math.random() * range) * (this.properties.step) + min; + default: + break; + } + + v = clamp(v, min, max) + this.setProperty("value", v) + this.setOutputData(0, v) + + console.debug("ValueControl", v, this.properties) + }; +} + +LiteGraph.registerNodeType({ + class: ComfyValueControl, + title: "Comfy.ValueControl", + desc: "Adjusts an incoming value based on behavior", + type: "utils/value_control" +}) diff --git a/src/lib/nodes/ComfyWidgetNodes.ts b/src/lib/nodes/ComfyWidgetNodes.ts index 2012414..fce009f 100644 --- a/src/lib/nodes/ComfyWidgetNodes.ts +++ b/src/lib/nodes/ComfyWidgetNodes.ts @@ -36,6 +36,15 @@ export abstract class ComfyWidgetNode extends ComfyGraphNode { /** If false, user manually set min/max/step, and should not be autoinherited from connected input */ autoConfig: boolean = true; + copyFromInputLink: boolean = true; + + /** Names of properties to add as inputs */ + // shownInputProperties: string[] = [] + + /** Names of properties to add as outputs */ + private shownOutputProperties: Record = {} + outputProperties: { name: string, type: string }[] = [] + override isBackendNode = false; override serialize_widgets = true; @@ -62,6 +71,18 @@ export abstract class ComfyWidgetNode extends ComfyGraphNode { this.unsubscribe = this.value.subscribe(this.onValueUpdated.bind(this)) } + addPropertyAsOutput(propertyName: string, type: string) { + if (this.shownOutputProperties[propertyName]) + return; + + if (!(propertyName in this.properties)) { + throw `No property named ${propertyName} found!` + } + + this.shownOutputProperties[propertyName] = { type, index: this.outputs.length } + this.addOutput(propertyName, type) + } + formatValue(value: any): string { return Watch.toString(value) } @@ -84,21 +105,33 @@ export abstract class ComfyWidgetNode extends ComfyGraphNode { this.value.set(value) } - abstract validateValue(value: any): boolean; + override onPropertyChanged(property: string, value: any, prevValue?: any) { + const data = this.shownOutputProperties[property] + if (data) + this.setOutputData(data.index, value) + } /* * Logic to run if this widget can be treated as output (slider, combo, text) */ override onExecute() { - if (this.inputs.length >= this.inputIndex) { - const data = this.getInputData(this.inputIndex) - if (data && this.validateValue(data)) { // TODO can "null" be a legitimate value here? - this.setValue(data) + if (this.copyFromInputLink) { + if (this.inputs.length >= this.inputIndex) { + const data = this.getInputData(this.inputIndex) + if (data) { // TODO can "null" be a legitimate value here? + this.setValue(data) + const input = this.getInputLink(this.inputIndex) + input.data = null; + } } } if (this.outputs.length >= this.outputIndex) { this.setOutputData(this.outputIndex, get(this.value)) } + for (const propName in this.shownOutputProperties) { + const data = this.shownOutputProperties[propName] + this.setOutputData(data.index, this.properties[propName]) + } } /** Called when a backend node sends a ComfyUI output over a link */ @@ -118,18 +151,19 @@ export abstract class ComfyWidgetNode extends ComfyGraphNode { const comfyInput = input as IComfyInputSlot; for (const key in comfyInput.config) this.setProperty(key, comfyInput.config[key]) + + if ("defaultValue" in this.properties) + this.setValue(this.properties.defaultValue) + + const widget = layoutState.findLayoutForNode(this.id) + if (widget && input.name !== "") { + widget.attrs.title = input.name; + } + + console.debug("Property copy", input, this.properties) + + this.setValue(get(this.value)) } - if ("defaultValue" in this.properties) - this.setValue(this.properties.defaultValue) - - const widget = layoutState.findLayoutForNode(this.id) - if (widget && input.name !== "") { - widget.attrs.title = input.name; - } - - console.debug("Property copy", input, this.properties) - - this.setValue(get(this.value)) } return true; @@ -167,11 +201,13 @@ export abstract class ComfyWidgetNode extends ComfyGraphNode { clampOneConfig(input: IComfyInputSlot) { } override onSerialize(o: SerializedLGraphNode) { - (o as any).comfyValue = get(this.value) + (o as any).comfyValue = get(this.value); + (o as any).shownOutputProperties = this.shownOutputProperties } override onConfigure(o: SerializedLGraphNode) { - this.value.set((o as any).comfyValue) + this.value.set((o as any).comfyValue); + this.shownOutputProperties = (o as any).shownOutputProperties; } } @@ -199,25 +235,32 @@ export class ComfySliderNode extends ComfyWidgetNode { ], outputs: [ { name: "value", type: "number" }, - { name: "changed", type: BuiltInSlotType.EVENT } + { name: "changed", type: BuiltInSlotType.EVENT }, ] } + override outputProperties = [ + { name: "min", type: "number" }, + { name: "max", type: "number" }, + { name: "step", type: "number" }, + { name: "precision", type: "number" }, + ] + constructor(name?: string) { super(name, 0) } - override validateValue(value: any): boolean { - return typeof value === "number" - && value >= this.properties.min - && value <= this.properties.max + override setValue(value: any) { + if (typeof value !== "number") + return; + super.setValue(clamp(value, this.properties.min, this.properties.max)) } override clampOneConfig(input: IComfyInputSlot) { // this.setProperty("min", clamp(this.properties.min, input.config.min, input.config.max)) // this.setProperty("max", clamp(this.properties.max, input.config.max, input.config.min)) // this.setProperty("step", Math.min(this.properties.step, input.config.step)) - this.setValue(clamp(this.properties.defaultValue, this.properties.min, this.properties.max)) + this.setValue(this.properties.defaultValue) } } @@ -280,10 +323,10 @@ export class ComfyComboNode extends ComfyWidgetNode { return true; } - override validateValue(value: any): boolean { - if (typeof value !== "string") - return false; - return this.properties.values.indexOf(value) !== -1; + override setValue(value: any) { + if (typeof value !== "string" || this.properties.values.indexOf(value) === -1) + return; + super.setValue(value) } override clampOneConfig(input: IComfyInputSlot) { @@ -291,7 +334,7 @@ export class ComfyComboNode extends ComfyWidgetNode { if (input.config.values.length === 0) this.setValue("") else - this.setValue(input.config.values[0]) + this.setValue(input.config.defaultValue || input.config.values[0]) } } } @@ -329,8 +372,8 @@ export class ComfyTextNode extends ComfyWidgetNode { super(name, "") } - override validateValue(value: any): boolean { - return typeof value === "string" + override setValue(value: any) { + super.setValue(`${value}`) } } @@ -368,6 +411,7 @@ export class ComfyGalleryNode extends ComfyWidgetNode { } override svelteComponentType = GalleryWidget + override copyFromInputLink = false; constructor(name?: string) { super(name, []) @@ -380,12 +424,29 @@ export class ComfyGalleryNode extends ComfyWidgetNode { } } - override formatValue(value: GradioFileData[]): string { - return `Images: ${value.length}` + override formatValue(value: GradioFileData[] | null): string { + return `Images: ${value?.length || 0}` } - override validateValue(value: any): boolean { - return Array.isArray(value) && value.every(e => "images" in e) + private convertItems(output: GalleryOutput): GradioFileData[] { + return output.images.map(r => { + // TODO configure backend URL + const url = "http://localhost:8188/view?" + const params = new URLSearchParams(r) + return { + name: null, + data: url + params + } + }); + } + + override setValue(value: any) { + if (Array.isArray(value)) { + super.setValue(value) + } + else { + super.setValue([]) + } } receiveOutput() { @@ -394,15 +455,7 @@ export class ComfyGalleryNode extends ComfyWidgetNode { const data = link.data as GalleryOutput console.debug("[ComfyGalleryNode] Received output!", data) - const galleryItems: GradioFileData[] = data.images.map(r => { - // TODO configure backend URL - const url = "http://localhost:8188/view?" - const params = new URLSearchParams(r) - return { - name: null, - data: url + params - } - }); + const galleryItems: GradioFileData[] = this.convertItems(link.data) const currentValue = get(this.value) this.setValue(currentValue.concat(galleryItems)) @@ -429,7 +482,7 @@ export class ComfyButtonNode extends ComfyWidgetNode { static slotLayout: SlotLayout = { outputs: [ - { name: "event", type: BuiltInSlotType.EVENT }, + { name: "clicked", type: BuiltInSlotType.EVENT }, { name: "isClicked", type: "boolean" }, ] } @@ -437,8 +490,8 @@ export class ComfyButtonNode extends ComfyWidgetNode { override outputIndex = 1; override svelteComponentType = ButtonWidget; - override validateValue(value: any): boolean { - return typeof value === "boolean" + override setValue(value: any) { + super.setValue(Boolean(value)) } onClick() { diff --git a/src/lib/nodes/index.ts b/src/lib/nodes/index.ts index 4907332..8c22c73 100644 --- a/src/lib/nodes/index.ts +++ b/src/lib/nodes/index.ts @@ -1,3 +1,5 @@ export { default as ComfyReroute } from "./ComfyReroute" export { ComfyWidgetNode, ComfySliderNode, ComfyComboNode, ComfyTextNode } from "./ComfyWidgetNodes" export { ComfyCopyAction } from "./ComfyActionNodes" +export { default as ComfyValueControl } from "./ComfyValueControl" +export { default as ComfySelector } from "./ComfySelector"