Account for upstream links when refreshing combo widgets
This commit is contained in:
@@ -53,8 +53,6 @@
|
|||||||
children = layoutState.updateChildren(container, evt.detail.items)
|
children = layoutState.updateChildren(container, evt.detail.items)
|
||||||
// Ensure dragging is stopped on drag finish
|
// Ensure dragging is stopped on drag finish
|
||||||
};
|
};
|
||||||
|
|
||||||
const tt = "asd\nasdlkj"
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if container && children}
|
{#if container && children}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink, NodeMode, type Vector2, BuiltInSlotType, type INodeInputSlot, type NodeID, type NodeTypeSpec, type NodeTypeOpts } from "@litegraph-ts/core";
|
import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink, NodeMode, type Vector2, BuiltInSlotType, type INodeInputSlot, type NodeID, type NodeTypeSpec, type NodeTypeOpts, type SlotIndex } from "@litegraph-ts/core";
|
||||||
import type { LConnectionKind, INodeSlot } from "@litegraph-ts/core";
|
import type { LConnectionKind, INodeSlot } from "@litegraph-ts/core";
|
||||||
import ComfyAPI, { type ComfyAPIStatusResponse, type ComfyBoxPromptExtraData, type ComfyPromptRequest, type ComfyNodeID, type PromptID } from "$lib/api"
|
import ComfyAPI, { type ComfyAPIStatusResponse, type ComfyBoxPromptExtraData, type ComfyPromptRequest, type ComfyNodeID, type PromptID } from "$lib/api"
|
||||||
import { getPngMetadata, importA1111 } from "$lib/pnginfo";
|
import { getPngMetadata, importA1111 } from "$lib/pnginfo";
|
||||||
@@ -34,7 +34,7 @@ import notify from "$lib/notify";
|
|||||||
import configState from "$lib/stores/configState";
|
import configState from "$lib/stores/configState";
|
||||||
import { blankGraph } from "$lib/defaultGraph";
|
import { blankGraph } from "$lib/defaultGraph";
|
||||||
import type { ComfyExecutionResult } from "$lib/utils";
|
import type { ComfyExecutionResult } from "$lib/utils";
|
||||||
import ComfyPromptSerializer from "./ComfyPromptSerializer";
|
import ComfyPromptSerializer, { UpstreamNodeLocator, isActiveBackendNode } from "./ComfyPromptSerializer";
|
||||||
import { iterateNodeDefInputs, type ComfyNodeDef, isBackendNodeDefInputType, iterateNodeDefOutputs } from "$lib/ComfyNodeDef";
|
import { iterateNodeDefInputs, type ComfyNodeDef, isBackendNodeDefInputType, iterateNodeDefOutputs } from "$lib/ComfyNodeDef";
|
||||||
import { ComfyComboNode } from "$lib/nodes/widgets";
|
import { ComfyComboNode } from "$lib/nodes/widgets";
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ export type Progress = {
|
|||||||
|
|
||||||
type BackendComboNode = {
|
type BackendComboNode = {
|
||||||
comboNode: ComfyComboNode,
|
comboNode: ComfyComboNode,
|
||||||
inputSlot: IComfyInputSlot,
|
comfyInput: IComfyInputSlot,
|
||||||
backendNode: ComfyBackendNode
|
backendNode: ComfyBackendNode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -709,13 +709,13 @@ export default class ComfyApp {
|
|||||||
async refreshComboInNodes(flashUI: boolean = false) {
|
async refreshComboInNodes(flashUI: boolean = false) {
|
||||||
const defs = await this.api.getNodeDefs();
|
const defs = await this.api.getNodeDefs();
|
||||||
|
|
||||||
const isComfyComboNode = (node: LGraphNode): boolean => {
|
const isComfyComboNode = (node: LGraphNode): node is ComfyComboNode => {
|
||||||
return node
|
return node
|
||||||
&& node.type === "ui/combo"
|
&& node.type === "ui/combo"
|
||||||
&& "doAutoConfig" in node;
|
&& "doAutoConfig" in node;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isComfyComboInput = (input: INodeInputSlot) => {
|
const isComfyComboInput = (input: INodeInputSlot): input is IComfyInputSlot => {
|
||||||
return "config" in input
|
return "config" in input
|
||||||
&& "widgetNodeType" in input
|
&& "widgetNodeType" in input
|
||||||
&& input.widgetNodeType === "ui/combo";
|
&& input.widgetNodeType === "ui/combo";
|
||||||
@@ -729,34 +729,42 @@ export default class ComfyApp {
|
|||||||
// Figure out which combo nodes to update. They need to be connected to
|
// Figure out which combo nodes to update. They need to be connected to
|
||||||
// an input slot on a backend node with a backend config in the input
|
// an input slot on a backend node with a backend config in the input
|
||||||
// slot connected to.
|
// slot connected to.
|
||||||
for (const node of this.lGraph.iterateNodesInOrderRecursive()) {
|
const nodeLocator = new UpstreamNodeLocator(isComfyComboNode)
|
||||||
if (!(node as any).isBackendNode)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
const backendNode = (node as ComfyBackendNode)
|
const findComfyInputAndAttachedCombo = (node: LGraphNode, i: SlotIndex): [IComfyInputSlot, ComfyComboNode] | null => {
|
||||||
const found = range(backendNode.inputs.length)
|
const input = node.inputs[i]
|
||||||
.filter(i => {
|
|
||||||
const input = backendNode.inputs[i]
|
|
||||||
const inputNode = backendNode.getInputNode(i)
|
|
||||||
|
|
||||||
// Does this input autocreate a combo box on creation?
|
// Does this input autocreate a combo box on creation?
|
||||||
const isComfyInput = isComfyComboInput(input)
|
const isComfyInput = isComfyComboInput(input)
|
||||||
const isComfyCombo = isComfyComboNode(inputNode)
|
if (!isComfyInput)
|
||||||
|
return null;
|
||||||
|
|
||||||
// console.debug("[refreshComboInNodes] CHECK", backendNode.type, input.name, "isComfyCombo", isComfyCombo, "isComfyInput", isComfyInput)
|
// Find an attached combo node even if it's inside/outside of a
|
||||||
|
// subgraph, linked after several nodes, etc.
|
||||||
|
const [comboNode, _link] = nodeLocator.locateUpstream(node, i, null);
|
||||||
|
|
||||||
return isComfyCombo && isComfyInput
|
if (comboNode == null)
|
||||||
});
|
return null;
|
||||||
|
|
||||||
for (const inputIndex of found) {
|
const result: [IComfyInputSlot, ComfyComboNode] = [input, comboNode as ComfyComboNode]
|
||||||
const comboNode = backendNode.getInputNode(inputIndex) as ComfyComboNode
|
return result
|
||||||
const inputSlot = backendNode.inputs[inputIndex] as IComfyInputSlot;
|
}
|
||||||
const def = defs[backendNode.type];
|
|
||||||
|
|
||||||
const hasBackendConfig = def["input"]["required"][inputSlot.name] !== undefined
|
for (const node of this.lGraph.iterateNodesInOrderRecursive()) {
|
||||||
|
if (!isActiveBackendNode(node))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const found = range(node.inputs.length)
|
||||||
|
.map((i) => findComfyInputAndAttachedCombo(node, i))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
for (const [comfyInput, comboNode] of found) {
|
||||||
|
const def = defs[node.type];
|
||||||
|
|
||||||
|
const hasBackendConfig = def["input"]["required"][comfyInput.name] !== undefined
|
||||||
|
|
||||||
if (hasBackendConfig) {
|
if (hasBackendConfig) {
|
||||||
backendUpdatedCombos[comboNode.id] = { comboNode, inputSlot, backendNode }
|
backendUpdatedCombos[comboNode.id] = { comboNode, comfyInput, backendNode: node }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -792,12 +800,12 @@ export default class ComfyApp {
|
|||||||
await tick();
|
await tick();
|
||||||
|
|
||||||
// Load definitions from the backend.
|
// Load definitions from the backend.
|
||||||
for (const { comboNode, inputSlot, backendNode } of Object.values(backendUpdatedCombos)) {
|
for (const { comboNode, comfyInput, backendNode } of Object.values(backendUpdatedCombos)) {
|
||||||
const def = defs[backendNode.type];
|
const def = defs[backendNode.type];
|
||||||
const rawValues = def["input"]["required"][inputSlot.name][0];
|
const rawValues = def["input"]["required"][comfyInput.name][0];
|
||||||
|
|
||||||
console.debug("[ComfyApp] Reconfiguring combo widget", backendNode.type, "=>", comboNode.type, rawValues.length)
|
console.debug("[ComfyApp] Reconfiguring combo widget", backendNode.type, "=>", comboNode.type, rawValues.length)
|
||||||
comboNode.doAutoConfig(inputSlot, { includeProperties: new Set(["values"]), setWidgetTitle: false })
|
comboNode.doAutoConfig(comfyInput, { includeProperties: new Set(["values"]), setWidgetTitle: false })
|
||||||
|
|
||||||
comboNode.formatValues(rawValues as string[])
|
comboNode.formatValues(rawValues as string[])
|
||||||
if (!rawValues?.includes(get(comboNode.value))) {
|
if (!rawValues?.includes(get(comboNode.value))) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type ComfyGraph from "$lib/ComfyGraph";
|
import type ComfyGraph from "$lib/ComfyGraph";
|
||||||
import type { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
|
import type { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
|
||||||
import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
|
import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
|
||||||
import { GraphInput, GraphOutput, LGraph, LGraphNode, LLink, NodeMode, Subgraph } from "@litegraph-ts/core";
|
import { GraphInput, GraphOutput, LGraph, LGraphNode, LLink, NodeMode, Subgraph, type SlotIndex } from "@litegraph-ts/core";
|
||||||
import type { SerializedPrompt, SerializedPromptInput, SerializedPromptInputs, SerializedPromptInputsAll } from "./ComfyApp";
|
import type { SerializedPrompt, SerializedPromptInput, SerializedPromptInputs, SerializedPromptInputsAll } from "./ComfyApp";
|
||||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||||
|
|
||||||
@@ -9,8 +9,8 @@ function hasTag(node: LGraphNode, tag: string): boolean {
|
|||||||
return "tags" in node.properties && node.properties.tags.indexOf(tag) !== -1
|
return "tags" in node.properties && node.properties.tags.indexOf(tag) !== -1
|
||||||
}
|
}
|
||||||
|
|
||||||
function isActiveBackendNode(node: ComfyGraphNode, tag: string | null): node is ComfyBackendNode {
|
export function isActiveNode(node: LGraphNode, tag: string | null = null): boolean {
|
||||||
if (!node.isBackendNode)
|
if (!node)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (tag && !hasTag(node, tag)) {
|
if (tag && !hasTag(node, tag)) {
|
||||||
@@ -18,7 +18,7 @@ function isActiveBackendNode(node: ComfyGraphNode, tag: string | null): node is
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.mode === NodeMode.NEVER) {
|
if (node.mode !== NodeMode.ALWAYS) {
|
||||||
// Don't serialize muted nodes
|
// Don't serialize muted nodes
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -26,9 +26,20 @@ function isActiveBackendNode(node: ComfyGraphNode, tag: string | null): node is
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function followSubgraph(subgraph: Subgraph, link: LLink): [LGraph | null, LLink | null] {
|
export function isActiveBackendNode(node: LGraphNode, tag: string | null = null): node is ComfyBackendNode {
|
||||||
|
if (!(node as any).isBackendNode)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return isActiveNode(node, tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpstreamNodeLocator {
|
||||||
|
constructor(private isTheTargetNode: (node: LGraphNode) => boolean) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private followSubgraph(subgraph: Subgraph, link: LLink): [LGraph | null, LLink | null] {
|
||||||
if (link.origin_id != subgraph.id)
|
if (link.origin_id != subgraph.id)
|
||||||
throw new Error("A!")
|
throw new Error("Invalid link and graph output!")
|
||||||
|
|
||||||
const innerGraphOutput = subgraph.getInnerGraphOutputByIndex(link.origin_slot)
|
const innerGraphOutput = subgraph.getInnerGraphOutputByIndex(link.origin_slot)
|
||||||
if (innerGraphOutput == null)
|
if (innerGraphOutput == null)
|
||||||
@@ -36,11 +47,11 @@ function followSubgraph(subgraph: Subgraph, link: LLink): [LGraph | null, LLink
|
|||||||
|
|
||||||
const nextLink = innerGraphOutput.getInputLink(0)
|
const nextLink = innerGraphOutput.getInputLink(0)
|
||||||
return [innerGraphOutput.graph, nextLink];
|
return [innerGraphOutput.graph, nextLink];
|
||||||
}
|
}
|
||||||
|
|
||||||
function followGraphInput(graphInput: GraphInput, link: LLink): [LGraph | null, LLink | null] {
|
private followGraphInput(graphInput: GraphInput, link: LLink): [LGraph | null, LLink | null] {
|
||||||
if (link.origin_id != graphInput.id)
|
if (link.origin_id != graphInput.id)
|
||||||
throw new Error("A!")
|
throw new Error("Invalid link and graph input!")
|
||||||
|
|
||||||
const outerSubgraph = graphInput.getParentSubgraph();
|
const outerSubgraph = graphInput.getParentSubgraph();
|
||||||
if (outerSubgraph == null)
|
if (outerSubgraph == null)
|
||||||
@@ -52,22 +63,85 @@ function followGraphInput(graphInput: GraphInput, link: LLink): [LGraph | null,
|
|||||||
|
|
||||||
const nextLink = outerSubgraph.getInputLink(outerInputIndex)
|
const nextLink = outerSubgraph.getInputLink(outerInputIndex)
|
||||||
return [outerSubgraph.graph, nextLink];
|
return [outerSubgraph.graph, nextLink];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUpstreamLink(parent: LGraphNode, currentLink: LLink): [LGraph | null, LLink | null] {
|
private getUpstreamLink(parent: LGraphNode, currentLink: LLink): [LGraph | null, LLink | null] {
|
||||||
if (parent.is(Subgraph)) {
|
if (parent.is(Subgraph)) {
|
||||||
console.warn("FollowSubgraph")
|
console.debug("FollowSubgraph")
|
||||||
return followSubgraph(parent, currentLink);
|
return this.followSubgraph(parent, currentLink);
|
||||||
}
|
}
|
||||||
else if (parent.is(GraphInput)) {
|
else if (parent.is(GraphInput)) {
|
||||||
console.warn("FollowGraphInput")
|
console.debug("FollowGraphInput")
|
||||||
return followGraphInput(parent, currentLink);
|
return this.followGraphInput(parent, currentLink);
|
||||||
}
|
}
|
||||||
else if ("getUpstreamLink" in parent) {
|
else if ("getUpstreamLink" in parent) {
|
||||||
return [parent.graph, (parent as ComfyGraphNode).getUpstreamLink()];
|
return [parent.graph, (parent as ComfyGraphNode).getUpstreamLink()];
|
||||||
}
|
}
|
||||||
console.warn("[graphToPrompt] Node does not support getUpstreamLink", parent.type)
|
else if (parent.inputs.length === 1) {
|
||||||
|
// Only one input, so assume we can follow it backwards.
|
||||||
|
const link = parent.getInputLink(0);
|
||||||
|
if (link) {
|
||||||
|
return [parent.graph, link]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.warn("[graphToPrompt] Frontend node does not support getUpstreamLink", parent.type)
|
||||||
return [null, null];
|
return [null, null];
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Traverses the graph upstream from outputs towards inputs across
|
||||||
|
* a sequence of nodes dependent on a condition.
|
||||||
|
*
|
||||||
|
* Returns the node and the output link attached to it that leads to the
|
||||||
|
* starting node if any.
|
||||||
|
*/
|
||||||
|
locateUpstream(fromNode: LGraphNode, inputIndex: SlotIndex, tag: string | null): [LGraphNode | null, LLink | null] {
|
||||||
|
let parent = fromNode.getInputNode(inputIndex);
|
||||||
|
if (!parent)
|
||||||
|
return [null, null];
|
||||||
|
|
||||||
|
const seen = {}
|
||||||
|
let currentLink = fromNode.getInputLink(inputIndex);
|
||||||
|
|
||||||
|
const shouldFollowParent = (parent: LGraphNode) => {
|
||||||
|
return isActiveNode(parent, tag) && !this.isTheTargetNode(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are non-target nodes between us and another
|
||||||
|
// backend node, we have to traverse them first. This
|
||||||
|
// behavior is dependent on the type of node. Reroute nodes
|
||||||
|
// will simply follow their single input, while branching
|
||||||
|
// nodes have conditional logic that determines which link
|
||||||
|
// to follow backwards.
|
||||||
|
while (shouldFollowParent(parent)) {
|
||||||
|
const [nextGraph, nextLink] = this.getUpstreamLink(parent, currentLink);
|
||||||
|
|
||||||
|
if (nextLink == null) {
|
||||||
|
console.warn("[graphToPrompt] No upstream link found in frontend node", parent)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextLink && !seen[nextLink.id]) {
|
||||||
|
seen[nextLink.id] = true
|
||||||
|
const nextParent = nextGraph.getNodeById(nextLink.origin_id);
|
||||||
|
if (!isActiveNode(parent, tag)) {
|
||||||
|
parent = null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.debug("[graphToPrompt] Traverse upstream link", parent.id, nextParent?.id, (nextParent as any)?.isBackendNode)
|
||||||
|
currentLink = nextLink;
|
||||||
|
parent = nextParent;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parent = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isActiveNode(parent, tag) || !this.isTheTargetNode(parent) || currentLink == null)
|
||||||
|
return [null, null];
|
||||||
|
|
||||||
|
return [parent, currentLink]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ComfyPromptSerializer {
|
export default class ComfyPromptSerializer {
|
||||||
@@ -129,66 +203,21 @@ export default class ComfyPromptSerializer {
|
|||||||
serializeBackendLinks(node: ComfyBackendNode, tag: string | null): Record<string, SerializedPromptInput> {
|
serializeBackendLinks(node: ComfyBackendNode, tag: string | null): Record<string, SerializedPromptInput> {
|
||||||
const inputs = {}
|
const inputs = {}
|
||||||
|
|
||||||
|
// Find a backend node upstream following before any number of frontend nodes
|
||||||
|
const test = (node: LGraphNode) => (node as any).isBackendNode
|
||||||
|
const nodeLocator = new UpstreamNodeLocator(test)
|
||||||
|
|
||||||
// Store links between backend-only and hybrid nodes
|
// Store links between backend-only and hybrid nodes
|
||||||
for (let i = 0; i < node.inputs.length; i++) {
|
for (let i = 0; i < node.inputs.length; i++) {
|
||||||
let parent = node.getInputNode(i);
|
const [backendNode, linkLeadingTo] = nodeLocator.locateUpstream(node, i, tag)
|
||||||
if (parent) {
|
if (backendNode) {
|
||||||
const seen = {}
|
console.debug("[graphToPrompt] final link", backendNode.id, "-->", node.id)
|
||||||
let currentLink = node.getInputLink(i);
|
|
||||||
|
|
||||||
const isFrontendParent = (parent: LGraphNode) => {
|
|
||||||
if (!parent || (parent as any).isBackendNode)
|
|
||||||
return false;
|
|
||||||
if (tag && !hasTag(parent, tag))
|
|
||||||
return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there are frontend-only nodes between us and another
|
|
||||||
// backend node, we have to traverse them first. This
|
|
||||||
// behavior is dependent on the type of node. Reroute nodes
|
|
||||||
// will simply follow their single input, while branching
|
|
||||||
// nodes have conditional logic that determines which link
|
|
||||||
// to follow backwards.
|
|
||||||
while (isFrontendParent(parent)) {
|
|
||||||
const [nextGraph, nextLink] = getUpstreamLink(parent, currentLink);
|
|
||||||
|
|
||||||
if (nextLink == null) {
|
|
||||||
console.warn("[graphToPrompt] No upstream link found in frontend node", parent)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextLink && !seen[nextLink.id]) {
|
|
||||||
seen[nextLink.id] = true
|
|
||||||
const nextParent = nextGraph.getNodeById(nextLink.origin_id);
|
|
||||||
if (nextParent && tag && !hasTag(nextParent, tag)) {
|
|
||||||
console.debug("[graphToPrompt] Skipping tagged intermediate frontend node", tag, node.properties.tags)
|
|
||||||
parent = null;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.debug("[graphToPrompt] Traverse upstream link", parent.id, nextParent?.id, (nextParent as any)?.isBackendNode)
|
|
||||||
currentLink = nextLink;
|
|
||||||
parent = nextParent;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
parent = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentLink && parent && (parent as any).isBackendNode) {
|
|
||||||
if (tag && !hasTag(parent, tag))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
console.debug("[graphToPrompt] final link", parent.id, node.id)
|
|
||||||
const input = node.inputs[i]
|
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".
|
|
||||||
if (!(input.name in inputs))
|
if (!(input.name in inputs))
|
||||||
inputs[input.name] = [String(currentLink.origin_id), currentLink.origin_slot];
|
inputs[input.name] = [String(linkLeadingTo.origin_id), linkLeadingTo.origin_slot];
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
console.warn("[graphToPrompt] Didn't find upstream link!", currentLink, parent?.id)
|
console.warn("[graphToPrompt] Didn't find upstream link!", node.id, node.type, node.title)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
48
src/lib/nodes/widgets/ComfyButtonNode.ts
Normal file
48
src/lib/nodes/widgets/ComfyButtonNode.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core";
|
||||||
|
|
||||||
|
import ButtonWidget from "$lib/widgets/ButtonWidget.svelte";
|
||||||
|
import ComfyWidgetNode, { type ComfyWidgetProperties } from "./ComfyWidgetNode";
|
||||||
|
|
||||||
|
export interface ComfyButtonProperties extends ComfyWidgetProperties {
|
||||||
|
param: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ComfyButtonNode extends ComfyWidgetNode<boolean> {
|
||||||
|
override properties: ComfyButtonProperties = {
|
||||||
|
tags: [],
|
||||||
|
defaultValue: false,
|
||||||
|
param: "bang"
|
||||||
|
}
|
||||||
|
|
||||||
|
static slotLayout: SlotLayout = {
|
||||||
|
outputs: [
|
||||||
|
{ name: "clicked", type: BuiltInSlotType.EVENT },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
override svelteComponentType = ButtonWidget;
|
||||||
|
override defaultValue = false;
|
||||||
|
override outputSlotName = null;
|
||||||
|
override changedEventName = null;
|
||||||
|
|
||||||
|
constructor(name?: string) {
|
||||||
|
super(name, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override parseValue(param: any): boolean {
|
||||||
|
return Boolean(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick() {
|
||||||
|
this.setValue(true)
|
||||||
|
this.triggerSlot(0, this.properties.param);
|
||||||
|
this.setValue(false) // TODO onRelease
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LiteGraph.registerNodeType({
|
||||||
|
class: ComfyButtonNode,
|
||||||
|
title: "UI.Button",
|
||||||
|
desc: "Button that triggers an event when clicked",
|
||||||
|
type: "ui/button"
|
||||||
|
})
|
||||||
43
src/lib/nodes/widgets/ComfyCheckboxNode.ts
Normal file
43
src/lib/nodes/widgets/ComfyCheckboxNode.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core";
|
||||||
|
|
||||||
|
import CheckboxWidget from "$lib/widgets/CheckboxWidget.svelte";
|
||||||
|
import type { ComfyWidgetProperties } from "./ComfyWidgetNode";
|
||||||
|
import ComfyWidgetNode from "./ComfyWidgetNode";
|
||||||
|
|
||||||
|
export interface ComfyCheckboxProperties extends ComfyWidgetProperties {
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ComfyCheckboxNode extends ComfyWidgetNode<boolean> {
|
||||||
|
override properties: ComfyCheckboxProperties = {
|
||||||
|
tags: [],
|
||||||
|
defaultValue: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
static slotLayout: SlotLayout = {
|
||||||
|
inputs: [
|
||||||
|
{ name: "store", type: BuiltInSlotType.ACTION }
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ name: "value", type: "boolean" },
|
||||||
|
{ name: "changed", type: BuiltInSlotType.EVENT },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
override svelteComponentType = CheckboxWidget;
|
||||||
|
override defaultValue = false;
|
||||||
|
|
||||||
|
constructor(name?: string) {
|
||||||
|
super(name, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override parseValue(param: any) {
|
||||||
|
return Boolean(param);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LiteGraph.registerNodeType({
|
||||||
|
class: ComfyCheckboxNode,
|
||||||
|
title: "UI.Checkbox",
|
||||||
|
desc: "Checkbox that stores a boolean value",
|
||||||
|
type: "ui/checkbox"
|
||||||
|
})
|
||||||
154
src/lib/nodes/widgets/ComfyComboNode.ts
Normal file
154
src/lib/nodes/widgets/ComfyComboNode.ts
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||||
|
import { BuiltInSlotType, LiteGraph, type INodeInputSlot, type LGraphNode, type SerializedLGraphNode, type SlotLayout } from "@litegraph-ts/core";
|
||||||
|
import { writable, type Writable } from "svelte/store";
|
||||||
|
|
||||||
|
import ComboWidget from "$lib/widgets/ComboWidget.svelte";
|
||||||
|
import type { ComfyWidgetProperties } from "./ComfyWidgetNode";
|
||||||
|
import ComfyWidgetNode from "./ComfyWidgetNode";
|
||||||
|
|
||||||
|
|
||||||
|
export interface ComfyComboProperties extends ComfyWidgetProperties {
|
||||||
|
values: string[]
|
||||||
|
|
||||||
|
/* JS Function body that takes a parameter named "value" as a parameter and returns the label for each combo entry */
|
||||||
|
convertValueToLabelCode: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ComfyComboNode extends ComfyWidgetNode<string> {
|
||||||
|
override properties: ComfyComboProperties = {
|
||||||
|
tags: [],
|
||||||
|
defaultValue: "A",
|
||||||
|
values: ["A", "B", "C", "D"],
|
||||||
|
convertValueToLabelCode: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
static slotLayout: SlotLayout = {
|
||||||
|
inputs: [
|
||||||
|
{ name: "store", type: BuiltInSlotType.ACTION }
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ name: "value", type: "string" },
|
||||||
|
{ name: "changed", type: BuiltInSlotType.EVENT }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
override svelteComponentType = ComboWidget
|
||||||
|
override defaultValue = "A";
|
||||||
|
override saveUserState = false;
|
||||||
|
|
||||||
|
// True if at least one combo box refresh has taken place
|
||||||
|
// Wait until the initial graph load for combo to be valid.
|
||||||
|
firstLoad: Writable<boolean>;
|
||||||
|
valuesForCombo: Writable<any[] | null>; // Changed when the combo box has values.
|
||||||
|
|
||||||
|
constructor(name?: string) {
|
||||||
|
super(name, "A")
|
||||||
|
this.firstLoad = writable(false)
|
||||||
|
this.valuesForCombo = writable(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override onPropertyChanged(property: any, value: any) {
|
||||||
|
if (property === "values" || property === "convertValueToLabelCode") {
|
||||||
|
// this.formatValues(this.properties.values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatValues(values: string[]) {
|
||||||
|
if (values == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.properties.values = values;
|
||||||
|
|
||||||
|
let formatter: any;
|
||||||
|
if (this.properties.convertValueToLabelCode)
|
||||||
|
formatter = new Function("value", this.properties.convertValueToLabelCode) as (v: string) => string;
|
||||||
|
else
|
||||||
|
formatter = (value: any) => `${value}`;
|
||||||
|
|
||||||
|
let valuesForCombo = []
|
||||||
|
|
||||||
|
try {
|
||||||
|
valuesForCombo = this.properties.values.map((value, index) => {
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
label: formatter(value),
|
||||||
|
index
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error("Failed formatting!", err)
|
||||||
|
valuesForCombo = this.properties.values.map((value, index) => {
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
label: `${value}`,
|
||||||
|
index
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.firstLoad.set(true)
|
||||||
|
this.valuesForCombo.set(valuesForCombo);
|
||||||
|
}
|
||||||
|
|
||||||
|
onConnectOutput(
|
||||||
|
outputIndex: number,
|
||||||
|
inputType: INodeInputSlot["type"],
|
||||||
|
input: INodeInputSlot,
|
||||||
|
inputNode: LGraphNode,
|
||||||
|
inputIndex: number
|
||||||
|
): boolean {
|
||||||
|
if (!super.onConnectOutput(outputIndex, inputType, input, inputNode, inputIndex))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const thisProps = this.properties;
|
||||||
|
if (!("config" in input))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
const comfyInput = input as IComfyInputSlot;
|
||||||
|
const otherProps = comfyInput.config;
|
||||||
|
|
||||||
|
// Ensure combo options match
|
||||||
|
if (!(otherProps.values instanceof Array))
|
||||||
|
return false;
|
||||||
|
if (thisProps.values.find((v, i) => otherProps.values.indexOf(v) === -1))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
override parseValue(value: any): string {
|
||||||
|
if (typeof value !== "string" || this.properties.values.indexOf(value) === -1)
|
||||||
|
return this.properties.values[0]
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
override clampOneConfig(input: IComfyInputSlot) {
|
||||||
|
if (!input.config.values)
|
||||||
|
this.setValue("")
|
||||||
|
else if (input.config.values.indexOf(this.properties.value) === -1) {
|
||||||
|
if (input.config.values.length === 0)
|
||||||
|
this.setValue("")
|
||||||
|
else
|
||||||
|
this.setValue(input.config.defaultValue || input.config.values[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override onSerialize(o: SerializedLGraphNode) {
|
||||||
|
super.onSerialize(o);
|
||||||
|
// TODO fix saving combo nodes with huge values lists
|
||||||
|
o.properties.values = []
|
||||||
|
}
|
||||||
|
|
||||||
|
override stripUserState(o: SerializedLGraphNode) {
|
||||||
|
super.stripUserState(o);
|
||||||
|
o.properties.values = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LiteGraph.registerNodeType({
|
||||||
|
class: ComfyComboNode,
|
||||||
|
title: "UI.Combo",
|
||||||
|
desc: "Combo box outputting a string value",
|
||||||
|
type: "ui/combo"
|
||||||
|
})
|
||||||
99
src/lib/nodes/widgets/ComfyGalleryNode.ts
Normal file
99
src/lib/nodes/widgets/ComfyGalleryNode.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { parseWhateverIntoImageMetadata, type ComfyBoxImageMetadata, type ComfyUploadImageType } from "$lib/utils";
|
||||||
|
import { BuiltInSlotType, LiteGraph, type IComboWidget, type ITextWidget, type PropertyLayout, type SlotLayout } from "@litegraph-ts/core";
|
||||||
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
|
import GalleryWidget from "$lib/widgets/GalleryWidget.svelte";
|
||||||
|
import type { ComfyWidgetProperties } from "./ComfyWidgetNode";
|
||||||
|
import ComfyWidgetNode from "./ComfyWidgetNode";
|
||||||
|
|
||||||
|
export interface ComfyGalleryProperties extends ComfyWidgetProperties {
|
||||||
|
index: number,
|
||||||
|
updateMode: "replace" | "append",
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ComfyGalleryNode extends ComfyWidgetNode<ComfyBoxImageMetadata[]> {
|
||||||
|
override properties: ComfyGalleryProperties = {
|
||||||
|
tags: [],
|
||||||
|
defaultValue: [],
|
||||||
|
index: 0,
|
||||||
|
updateMode: "replace",
|
||||||
|
}
|
||||||
|
|
||||||
|
static slotLayout: SlotLayout = {
|
||||||
|
inputs: [
|
||||||
|
{ name: "images", type: "OUTPUT" },
|
||||||
|
{ name: "store", type: BuiltInSlotType.ACTION, options: { color_off: "rebeccapurple", color_on: "rebeccapurple" } }
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ name: "images", type: "COMFYBOX_IMAGES" },
|
||||||
|
{ name: "selected_index", type: "number" },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
static propertyLayout: PropertyLayout = [
|
||||||
|
{ name: "updateMode", defaultValue: "replace", type: "enum", options: { values: ["replace", "append"] } }
|
||||||
|
]
|
||||||
|
|
||||||
|
override svelteComponentType = GalleryWidget
|
||||||
|
override defaultValue = []
|
||||||
|
override saveUserState = false;
|
||||||
|
override outputSlotName = null;
|
||||||
|
override changedEventName = null;
|
||||||
|
|
||||||
|
selectedFilename: string | null = null;
|
||||||
|
|
||||||
|
selectedIndexWidget: ITextWidget;
|
||||||
|
modeWidget: IComboWidget;
|
||||||
|
|
||||||
|
constructor(name?: string) {
|
||||||
|
super(name, [])
|
||||||
|
this.selectedIndexWidget = this.addWidget("text", "Selected", String(this.properties.index), "index")
|
||||||
|
this.selectedIndexWidget.disabled = true;
|
||||||
|
this.modeWidget = this.addWidget("combo", "Mode", this.properties.updateMode, null, { property: "updateMode", values: ["replace", "append"] })
|
||||||
|
}
|
||||||
|
|
||||||
|
override onPropertyChanged(property: any, value: any) {
|
||||||
|
if (property === "updateMode") {
|
||||||
|
this.modeWidget.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override onExecute() {
|
||||||
|
this.setOutputData(0, get(this.value))
|
||||||
|
this.setOutputData(1, this.properties.index)
|
||||||
|
}
|
||||||
|
|
||||||
|
override onAction(action: any, param: any, options: { action_call?: string }) {
|
||||||
|
super.onAction(action, param, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
override formatValue(value: ComfyBoxImageMetadata[] | null): string {
|
||||||
|
return `Images: ${value?.length || 0}`
|
||||||
|
}
|
||||||
|
|
||||||
|
override parseValue(param: any): ComfyBoxImageMetadata[] {
|
||||||
|
const meta = parseWhateverIntoImageMetadata(param) || [];
|
||||||
|
|
||||||
|
console.debug("[ComfyGalleryNode] Received output!", param)
|
||||||
|
|
||||||
|
if (this.properties.updateMode === "append") {
|
||||||
|
const currentValue = get(this.value)
|
||||||
|
return currentValue.concat(meta)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override setValue(value: any, noChangedEvent: boolean = false) {
|
||||||
|
super.setValue(value, noChangedEvent)
|
||||||
|
this.setProperty("index", null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LiteGraph.registerNodeType({
|
||||||
|
class: ComfyGalleryNode,
|
||||||
|
title: "UI.Gallery",
|
||||||
|
desc: "Gallery that shows most recent outputs",
|
||||||
|
type: "ui/gallery"
|
||||||
|
})
|
||||||
51
src/lib/nodes/widgets/ComfyImageEditorNode.ts
Normal file
51
src/lib/nodes/widgets/ComfyImageEditorNode.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { parseWhateverIntoImageMetadata, type ComfyBoxImageMetadata } from "$lib/utils";
|
||||||
|
import type { FileData as GradioFileData } from "@gradio/upload";
|
||||||
|
import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core";
|
||||||
|
|
||||||
|
import ImageUploadWidget from "$lib/widgets/ImageUploadWidget.svelte";
|
||||||
|
import type { ComfyWidgetProperties } from "./ComfyWidgetNode";
|
||||||
|
import ComfyWidgetNode from "./ComfyWidgetNode";
|
||||||
|
|
||||||
|
export interface ComfyImageUploadNodeProperties extends ComfyWidgetProperties {
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ComfyImageUploadNode extends ComfyWidgetNode<ComfyBoxImageMetadata[]> {
|
||||||
|
properties: ComfyImageUploadNodeProperties = {
|
||||||
|
defaultValue: [],
|
||||||
|
tags: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
static slotLayout: SlotLayout = {
|
||||||
|
inputs: [
|
||||||
|
{ name: "store", type: BuiltInSlotType.ACTION }
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ name: "images", type: "COMFYBOX_IMAGES" }, // TODO support batches
|
||||||
|
{ name: "changed", type: BuiltInSlotType.EVENT },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
override svelteComponentType = ImageUploadWidget;
|
||||||
|
override defaultValue = [];
|
||||||
|
override storeActionName = "store";
|
||||||
|
override saveUserState = false;
|
||||||
|
|
||||||
|
constructor(name?: string) {
|
||||||
|
super(name, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
override parseValue(value: any): ComfyBoxImageMetadata[] {
|
||||||
|
return parseWhateverIntoImageMetadata(value) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
override formatValue(value: GradioFileData[]): string {
|
||||||
|
return `Images: ${value?.length || 0}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LiteGraph.registerNodeType({
|
||||||
|
class: ComfyImageUploadNode,
|
||||||
|
title: "UI.ImageUpload",
|
||||||
|
desc: "Widget that lets you upload and edit a multi-layered image. Can also act like a standalone image uploader.",
|
||||||
|
type: "ui/image_upload"
|
||||||
|
})
|
||||||
69
src/lib/nodes/widgets/ComfyNumberNode.ts
Normal file
69
src/lib/nodes/widgets/ComfyNumberNode.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||||
|
import { clamp } from "$lib/utils";
|
||||||
|
import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core";
|
||||||
|
|
||||||
|
import RangeWidget from "$lib/widgets/RangeWidget.svelte";
|
||||||
|
import type { ComfyWidgetProperties } from "./ComfyWidgetNode";
|
||||||
|
import ComfyWidgetNode from "./ComfyWidgetNode";
|
||||||
|
|
||||||
|
export interface ComfyNumberProperties extends ComfyWidgetProperties {
|
||||||
|
min: number,
|
||||||
|
max: number,
|
||||||
|
step: number,
|
||||||
|
precision: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ComfyNumberNode extends ComfyWidgetNode<number> {
|
||||||
|
override properties: ComfyNumberProperties = {
|
||||||
|
tags: [],
|
||||||
|
defaultValue: 0,
|
||||||
|
min: 0,
|
||||||
|
max: 10,
|
||||||
|
step: 1,
|
||||||
|
precision: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
override svelteComponentType = RangeWidget
|
||||||
|
override defaultValue = 0;
|
||||||
|
|
||||||
|
static slotLayout: SlotLayout = {
|
||||||
|
inputs: [
|
||||||
|
{ name: "store", type: BuiltInSlotType.ACTION }
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ name: "value", type: "number" },
|
||||||
|
{ 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 parseValue(value: any): number {
|
||||||
|
if (typeof value !== "number")
|
||||||
|
return this.properties.min;
|
||||||
|
return 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(this.properties.defaultValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LiteGraph.registerNodeType({
|
||||||
|
class: ComfyNumberNode,
|
||||||
|
title: "UI.Number",
|
||||||
|
desc: "Displays a number, by default in a slider format.",
|
||||||
|
type: "ui/number"
|
||||||
|
})
|
||||||
82
src/lib/nodes/widgets/ComfyRadioNode.ts
Normal file
82
src/lib/nodes/widgets/ComfyRadioNode.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { clamp } from "$lib/utils";
|
||||||
|
import { BuiltInSlotType, LiteGraph, type INumberWidget, type SlotLayout } from "@litegraph-ts/core";
|
||||||
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
|
import RadioWidget from "$lib/widgets/RadioWidget.svelte";
|
||||||
|
import type { ComfyWidgetProperties } from "./ComfyWidgetNode";
|
||||||
|
import ComfyWidgetNode from "./ComfyWidgetNode";
|
||||||
|
|
||||||
|
|
||||||
|
export interface ComfyRadioProperties extends ComfyWidgetProperties {
|
||||||
|
choices: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ComfyRadioNode extends ComfyWidgetNode<string> {
|
||||||
|
override properties: ComfyRadioProperties = {
|
||||||
|
tags: [],
|
||||||
|
choices: ["Choice A", "Choice B", "Choice C"],
|
||||||
|
defaultValue: "Choice A",
|
||||||
|
}
|
||||||
|
|
||||||
|
static slotLayout: SlotLayout = {
|
||||||
|
inputs: [
|
||||||
|
{ name: "store", type: BuiltInSlotType.ACTION }
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ name: "value", type: "string" },
|
||||||
|
{ name: "index", type: "number" },
|
||||||
|
{ name: "changed", type: BuiltInSlotType.EVENT },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
override svelteComponentType = RadioWidget;
|
||||||
|
override defaultValue = "";
|
||||||
|
|
||||||
|
indexWidget: INumberWidget;
|
||||||
|
|
||||||
|
index = 0;
|
||||||
|
|
||||||
|
constructor(name?: string) {
|
||||||
|
super(name, "Choice A")
|
||||||
|
this.indexWidget = this.addWidget("number", "Index", this.index)
|
||||||
|
this.indexWidget.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
override onExecute(param: any, options: object) {
|
||||||
|
super.onExecute(param, options);
|
||||||
|
this.setOutputData(1, this.index)
|
||||||
|
}
|
||||||
|
|
||||||
|
override setValue(value: string, noChangedEvent: boolean = false) {
|
||||||
|
super.setValue(value, noChangedEvent)
|
||||||
|
|
||||||
|
value = get(this.value);
|
||||||
|
|
||||||
|
const index = this.properties.choices.indexOf(value)
|
||||||
|
if (index === -1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.index = index;
|
||||||
|
this.indexWidget.value = index;
|
||||||
|
this.setOutputData(1, this.index)
|
||||||
|
}
|
||||||
|
|
||||||
|
override parseValue(param: any): string {
|
||||||
|
if (typeof param === "string") {
|
||||||
|
if (this.properties.choices.indexOf(param) === -1)
|
||||||
|
return this.properties.choices[0]
|
||||||
|
return param
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const index = clamp(parseInt(param), 0, this.properties.choices.length - 1)
|
||||||
|
return this.properties.choices[index] || this.properties.defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LiteGraph.registerNodeType({
|
||||||
|
class: ComfyRadioNode,
|
||||||
|
title: "UI.Radio",
|
||||||
|
desc: "Radio that outputs a string and index",
|
||||||
|
type: "ui/radio"
|
||||||
|
})
|
||||||
44
src/lib/nodes/widgets/ComfyTextNode.ts
Normal file
44
src/lib/nodes/widgets/ComfyTextNode.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core";
|
||||||
|
|
||||||
|
import TextWidget from "$lib/widgets/TextWidget.svelte";
|
||||||
|
import ComfyWidgetNode, { type ComfyWidgetProperties } from "./ComfyWidgetNode";
|
||||||
|
|
||||||
|
export interface ComfyTextProperties extends ComfyWidgetProperties {
|
||||||
|
multiline: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ComfyTextNode extends ComfyWidgetNode<string> {
|
||||||
|
override properties: ComfyTextProperties = {
|
||||||
|
tags: [],
|
||||||
|
defaultValue: "",
|
||||||
|
multiline: false
|
||||||
|
}
|
||||||
|
|
||||||
|
static slotLayout: SlotLayout = {
|
||||||
|
inputs: [
|
||||||
|
{ name: "store", type: BuiltInSlotType.ACTION }
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ name: "value", type: "string" },
|
||||||
|
{ name: "changed", type: BuiltInSlotType.EVENT }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
override svelteComponentType = TextWidget
|
||||||
|
override defaultValue = "";
|
||||||
|
|
||||||
|
constructor(name?: string) {
|
||||||
|
super(name, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
override parseValue(value: any): string {
|
||||||
|
return `${value}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LiteGraph.registerNodeType({
|
||||||
|
class: ComfyTextNode,
|
||||||
|
title: "UI.Text",
|
||||||
|
desc: "Textbox outputting a string value",
|
||||||
|
type: "ui/text"
|
||||||
|
})
|
||||||
329
src/lib/nodes/widgets/ComfyWidgetNode.ts
Normal file
329
src/lib/nodes/widgets/ComfyWidgetNode.ts
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||||
|
import layoutState from "$lib/stores/layoutState";
|
||||||
|
import { range } from "$lib/utils";
|
||||||
|
import { LConnectionKind, LGraphCanvas, LLink, LiteGraph, NodeMode, type INodeInputSlot, type INodeOutputSlot, type ITextWidget, type LGraphNode, type SerializedLGraphNode, type Vector2 } from "@litegraph-ts/core";
|
||||||
|
import { Watch } from "@litegraph-ts/nodes-basic";
|
||||||
|
import type { SvelteComponentDev } from "svelte/internal";
|
||||||
|
import { get, writable, type Unsubscriber, type Writable } from "svelte/store";
|
||||||
|
|
||||||
|
import type { ComfyNodeID } from "$lib/api";
|
||||||
|
import type { ComfyGraphNodeProperties } from "../ComfyGraphNode";
|
||||||
|
import ComfyGraphNode from "../ComfyGraphNode";
|
||||||
|
|
||||||
|
export type AutoConfigOptions = {
|
||||||
|
includeProperties?: Set<string> | null,
|
||||||
|
setDefaultValue?: boolean
|
||||||
|
setWidgetTitle?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* NOTE: If you want to add a new widget but it has the same input/output type
|
||||||
|
* as another one of the existing widgets, best to create a new "variant" of
|
||||||
|
* that widget instead.
|
||||||
|
*
|
||||||
|
* - Go to layoutState, look for `ALL_ATTRIBUTES,` insert or find a "variant"
|
||||||
|
* attribute and set `validNodeTypes` to the type of the litegraph node
|
||||||
|
* - Add a new entry in the `values` array, like "knob" or "dial" for ComfySliderWidget
|
||||||
|
* - Add an {#if widget.attrs.variant === <...>} statement in the existing Svelte component
|
||||||
|
*
|
||||||
|
* Also, BEWARE of calling setOutputData() and triggerSlot() on the same frame!
|
||||||
|
* You will have to either implement an internal delay on the event triggering
|
||||||
|
* or use an Event Delay node to ensure the output slot data can propagate to
|
||||||
|
* the rest of the graph first (see `delayChangedEvent` for details)
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ComfyWidgetProperties extends ComfyGraphNodeProperties {
|
||||||
|
defaultValue: any
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A node that is tied to a UI widget in the frontend. When the frontend's
|
||||||
|
* widget is changed, the value of the first output in the node is updated
|
||||||
|
* in the litegraph instance.
|
||||||
|
*/
|
||||||
|
export default abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
||||||
|
abstract properties: ComfyWidgetProperties;
|
||||||
|
|
||||||
|
value: Writable<T>
|
||||||
|
propsChanged: Writable<number> = writable(0) // dummy to indicate if props changed
|
||||||
|
unsubscribe: Unsubscriber;
|
||||||
|
|
||||||
|
/** Svelte class for the frontend logic */
|
||||||
|
abstract svelteComponentType: typeof SvelteComponentDev
|
||||||
|
|
||||||
|
/** If false, user manually set min/max/step, and should not be autoinherited from connected input */
|
||||||
|
autoConfig: boolean = true;
|
||||||
|
|
||||||
|
copyFromInputLink: boolean = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true wait until next frame update to trigger the changed event.
|
||||||
|
* Reason is, if the event is triggered immediately then other stuff that wants to run
|
||||||
|
* their own onExecute on the output value won't have completed yet.
|
||||||
|
*/
|
||||||
|
delayChangedEvent: boolean = true;
|
||||||
|
|
||||||
|
private _aboutToChange: number = 0;
|
||||||
|
private _aboutToChangeValue: any = null;
|
||||||
|
private _noChangedEvent: boolean = false;
|
||||||
|
|
||||||
|
abstract defaultValue: T;
|
||||||
|
|
||||||
|
/** Names of properties to add as inputs */
|
||||||
|
// shownInputProperties: string[] = []
|
||||||
|
|
||||||
|
/** Names of properties to add as outputs */
|
||||||
|
private shownOutputProperties: Record<string, { type: string, index: number }> = {}
|
||||||
|
outputProperties: { name: string, type: string }[] = []
|
||||||
|
|
||||||
|
override isBackendNode = false;
|
||||||
|
override serialize_widgets = true;
|
||||||
|
|
||||||
|
storeActionName: string | null = "store";
|
||||||
|
|
||||||
|
// output slots
|
||||||
|
outputSlotName: string | null = "value";
|
||||||
|
changedEventName: string | null = "changed";
|
||||||
|
|
||||||
|
displayWidget: ITextWidget;
|
||||||
|
|
||||||
|
override size: Vector2 = [60, 40];
|
||||||
|
|
||||||
|
constructor(name: string, value: T) {
|
||||||
|
const color = LGraphCanvas.node_colors["blue"]
|
||||||
|
super(name)
|
||||||
|
this.value = writable(value)
|
||||||
|
this.color ||= color.color
|
||||||
|
this.bgColor ||= color.bgColor
|
||||||
|
this.displayWidget = this.addWidget<ITextWidget>(
|
||||||
|
"text",
|
||||||
|
"Value",
|
||||||
|
""
|
||||||
|
);
|
||||||
|
this.displayWidget.disabled = true; // prevent editing
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
override changeMode(modeTo: NodeMode): boolean {
|
||||||
|
const result = super.changeMode(modeTo);
|
||||||
|
this.notifyPropsChanged();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private onValueUpdated(value: any) {
|
||||||
|
// console.debug("[Widget] valueUpdated", this, value)
|
||||||
|
this.displayWidget.value = this.formatValue(value)
|
||||||
|
|
||||||
|
if (this.outputSlotName !== null) {
|
||||||
|
const outputIndex = this.findOutputSlotIndexByName(this.outputSlotName)
|
||||||
|
if (outputIndex !== -1)
|
||||||
|
this.setOutputData(outputIndex, get(this.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.changedEventName !== null && !this._noChangedEvent) {
|
||||||
|
if (!this.delayChangedEvent)
|
||||||
|
this.triggerChangeEvent(get(this.value))
|
||||||
|
else {
|
||||||
|
// console.debug("[Widget] queueChangeEvent", this, value)
|
||||||
|
this._aboutToChange = 2; // wait 1.5-2 frames, in case we're already in the middle of executing the graph
|
||||||
|
this._aboutToChangeValue = get(this.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._noChangedEvent = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private triggerChangeEvent(value: any) {
|
||||||
|
// console.debug("[Widget] trigger changed", this, value)
|
||||||
|
this.trigger(this.changedEventName, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
parseValue(value: any): T { return value as T };
|
||||||
|
|
||||||
|
getValue(): T {
|
||||||
|
return get(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(value: any, noChangedEvent: boolean = false) {
|
||||||
|
if (noChangedEvent)
|
||||||
|
this._noChangedEvent = true;
|
||||||
|
|
||||||
|
const parsed = this.parseValue(value)
|
||||||
|
this.value.set(parsed)
|
||||||
|
|
||||||
|
// In case value.set() does not trigger onValueUpdated, we need to reset
|
||||||
|
// the counter here also.
|
||||||
|
this._noChangedEvent = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
override onPropertyChanged(property: string, value: any, prevValue?: any) {
|
||||||
|
if (this.shownOutputProperties != null) {
|
||||||
|
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(param: any, options: object) {
|
||||||
|
if (this.outputSlotName != null) {
|
||||||
|
const outputIndex = this.findOutputSlotIndexByName(this.outputSlotName)
|
||||||
|
if (outputIndex !== -1)
|
||||||
|
this.setOutputData(outputIndex, get(this.value))
|
||||||
|
}
|
||||||
|
for (const propName in this.shownOutputProperties) {
|
||||||
|
const data = this.shownOutputProperties[propName]
|
||||||
|
this.setOutputData(data.index, this.properties[propName])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire a pending change event after one full step of the graph has
|
||||||
|
// finished processing
|
||||||
|
if (this._aboutToChange > 0) {
|
||||||
|
this._aboutToChange -= 1
|
||||||
|
if (this._aboutToChange <= 0) {
|
||||||
|
const value = this._aboutToChangeValue;
|
||||||
|
this._aboutToChange = 0;
|
||||||
|
this._aboutToChangeValue = null;
|
||||||
|
this.triggerChangeEvent(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override onAction(action: any, param: any, options: { action_call?: string }) {
|
||||||
|
if (action === this.storeActionName) {
|
||||||
|
let noChangedEvent = false;
|
||||||
|
let value = param;
|
||||||
|
if (param != null && typeof param === "object" && "value" in param) {
|
||||||
|
value = param.value
|
||||||
|
if ("noChangedEvent" in param)
|
||||||
|
noChangedEvent = Boolean(param.noChangedEvent)
|
||||||
|
}
|
||||||
|
this.setValue(value, noChangedEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onConnectOutput(
|
||||||
|
outputIndex: number,
|
||||||
|
inputType: INodeInputSlot["type"],
|
||||||
|
input: INodeInputSlot,
|
||||||
|
inputNode: LGraphNode,
|
||||||
|
inputIndex: number
|
||||||
|
): boolean {
|
||||||
|
const anyConnected = range(this.outputs.length).some(i => this.getOutputLinks(i).length > 0);
|
||||||
|
|
||||||
|
if (this.autoConfig && "config" in input && !anyConnected && (input as IComfyInputSlot).widgetNodeType === this.type) {
|
||||||
|
this.doAutoConfig(input as IComfyInputSlot)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
doAutoConfig(input: IComfyInputSlot, options: AutoConfigOptions = { setDefaultValue: true, setWidgetTitle: true }) {
|
||||||
|
// Copy properties from default config in input slot
|
||||||
|
const comfyInput = input as IComfyInputSlot;
|
||||||
|
for (const key in comfyInput.config) {
|
||||||
|
if (options.includeProperties == null || options.includeProperties.has(key))
|
||||||
|
this.setProperty(key, comfyInput.config[key])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.setDefaultValue) {
|
||||||
|
if ("defaultValue" in this.properties)
|
||||||
|
this.setValue(this.properties.defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.setWidgetTitle) {
|
||||||
|
const widget = layoutState.findLayoutForNode(this.id as ComfyNodeID)
|
||||||
|
if (widget && input.name !== "") {
|
||||||
|
widget.attrs.title = input.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.debug("Property copy", input, this.properties)
|
||||||
|
|
||||||
|
this.setValue(get(this.value))
|
||||||
|
|
||||||
|
this.onAutoConfig(input);
|
||||||
|
|
||||||
|
this.notifyPropsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
onAutoConfig(input: IComfyInputSlot) {
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyPropsChanged() {
|
||||||
|
const layoutEntry = layoutState.findLayoutEntryForNode(this.id as ComfyNodeID)
|
||||||
|
if (layoutEntry && layoutEntry.parent) {
|
||||||
|
layoutEntry.parent.attrsChanged.set(get(layoutEntry.parent.attrsChanged) + 1)
|
||||||
|
}
|
||||||
|
// console.debug("propsChanged", this)
|
||||||
|
this.propsChanged.set(get(this.propsChanged) + 1)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override onConnectionsChange(
|
||||||
|
type: LConnectionKind,
|
||||||
|
slotIndex: number,
|
||||||
|
isConnected: boolean,
|
||||||
|
link: LLink,
|
||||||
|
ioSlot: (INodeOutputSlot | INodeInputSlot)
|
||||||
|
): void {
|
||||||
|
super.onConnectionsChange(type, slotIndex, isConnected, link, ioSlot);
|
||||||
|
this.clampConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
clampConfig() {
|
||||||
|
let changed = false;
|
||||||
|
for (const link of this.getOutputLinks(0)) {
|
||||||
|
if (link) { // can be undefined if the link is removed
|
||||||
|
const node = this.graph._nodes_by_id[link.target_id]
|
||||||
|
if (node) {
|
||||||
|
const input = node.inputs[link.target_slot]
|
||||||
|
if (input && "config" in input) {
|
||||||
|
this.clampOneConfig(input as IComfyInputSlot)
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force reactivity change so the frontend can be updated with the new props
|
||||||
|
this.notifyPropsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
clampOneConfig(input: IComfyInputSlot) { }
|
||||||
|
|
||||||
|
override onSerialize(o: SerializedLGraphNode) {
|
||||||
|
(o as any).comfyValue = get(this.value);
|
||||||
|
(o as any).shownOutputProperties = this.shownOutputProperties
|
||||||
|
super.onSerialize(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
override onConfigure(o: SerializedLGraphNode) {
|
||||||
|
const value = (o as any).comfyValue || LiteGraph.cloneObject(this.defaultValue);
|
||||||
|
this.value.set(value);
|
||||||
|
this.shownOutputProperties = (o as any).shownOutputProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
override stripUserState(o: SerializedLGraphNode) {
|
||||||
|
super.stripUserState(o);
|
||||||
|
(o as any).comfyValue = this.defaultValue;
|
||||||
|
o.properties.defaultValue = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
344
src/lib/nodes/widgets/ImageUploadWidget.svelte
Normal file
344
src/lib/nodes/widgets/ImageUploadWidget.svelte
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||||
|
import { Block } from "@gradio/atoms";
|
||||||
|
import { TextBox } from "@gradio/form";
|
||||||
|
import Row from "$lib/components/gradio/app/Row.svelte";
|
||||||
|
import { get, type Writable } from "svelte/store";
|
||||||
|
import Modal from "$lib/components/Modal.svelte";
|
||||||
|
import { Button } from "@gradio/button";
|
||||||
|
import type { ComfyImageLocation } from "$lib/utils";
|
||||||
|
import { Embed as Klecks } from "klecks";
|
||||||
|
|
||||||
|
import "klecks/style/style.scss";
|
||||||
|
import ImageUpload from "$lib/components/ImageUpload.svelte";
|
||||||
|
import NumberInput from "$lib/components/NumberInput.svelte";
|
||||||
|
import { uploadImageToComfyUI, type ComfyBoxImageMetadata, comfyFileToComfyBoxMetadata, comfyBoxImageToComfyURL, comfyBoxImageToComfyFile, type ComfyUploadImageType } from "$lib/utils";
|
||||||
|
import notify from "$lib/notify";
|
||||||
|
import type { ComfyImageEditorNode } from "$lib/nodes/widgets";
|
||||||
|
|
||||||
|
export let widget: WidgetLayout | null = null;
|
||||||
|
export let isMobile: boolean = false;
|
||||||
|
let node: ComfyImageEditorNode | null = null;
|
||||||
|
let nodeValue: Writable<ComfyBoxImageMetadata[]> | null = null;
|
||||||
|
let attrsChanged: Writable<number> | null = null;
|
||||||
|
|
||||||
|
let imgWidth: number = 0;
|
||||||
|
let imgHeight: number = 0;
|
||||||
|
|
||||||
|
$: widget && setNodeValue(widget);
|
||||||
|
|
||||||
|
$: if ($nodeValue && $nodeValue.length > 0) {
|
||||||
|
// TODO improve
|
||||||
|
if (imgWidth > 0 && imgHeight > 0) {
|
||||||
|
$nodeValue[0].width = imgWidth
|
||||||
|
$nodeValue[0].height = imgHeight
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$nodeValue[0].width = 0
|
||||||
|
$nodeValue[0].height = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNodeValue(widget: WidgetLayout) {
|
||||||
|
if (widget) {
|
||||||
|
node = widget.node as ComfyImageEditorNode
|
||||||
|
nodeValue = node.value;
|
||||||
|
attrsChanged = widget.attrsChanged;
|
||||||
|
status = $nodeValue && $nodeValue.length > 0 ? "uploaded" : "empty"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let editorRoot: HTMLDivElement | null = null;
|
||||||
|
let showModal = false;
|
||||||
|
let kl: Klecks | null = null;
|
||||||
|
|
||||||
|
function disposeEditor() {
|
||||||
|
console.warn("[ImageEditorWidget] CLOSING", widget, $nodeValue)
|
||||||
|
|
||||||
|
if (editorRoot) {
|
||||||
|
while (editorRoot.firstChild) {
|
||||||
|
editorRoot.removeChild(editorRoot.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kl = null;
|
||||||
|
showModal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateBlankCanvas(width: number, height: number, fill: string = "#fff"): HTMLCanvasElement {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.save();
|
||||||
|
ctx.fillStyle = fill,
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.restore();
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadImage(imageURL: string): Promise<HTMLImageElement> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const e = new Image();
|
||||||
|
e.setAttribute('crossorigin', 'anonymous'); // Don't taint the canvas from loading files on-disk
|
||||||
|
e.addEventListener("load", () => { resolve(e); });
|
||||||
|
e.src = imageURL;
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateImageCanvas(imageURL: string): Promise<[HTMLCanvasElement, number, number]> {
|
||||||
|
const image = await loadImage(imageURL);
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = image.width;
|
||||||
|
canvas.height = image.height;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.save();
|
||||||
|
ctx.fillStyle = "rgba(255, 255, 255, 0.0)";
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.drawImage(image, 0, 0);
|
||||||
|
ctx.restore();
|
||||||
|
return [canvas, image.width, image.height];
|
||||||
|
}
|
||||||
|
|
||||||
|
const FILENAME: string = "ComfyUITemp.png";
|
||||||
|
const SUBFOLDER: string = "ComfyBox_Editor";
|
||||||
|
const DIRECTORY: ComfyUploadImageType = "input";
|
||||||
|
|
||||||
|
async function submitKlecksToComfyUI(onSuccess: () => void, onError: () => void) {
|
||||||
|
const blob = kl.getPNG();
|
||||||
|
|
||||||
|
status = "uploading"
|
||||||
|
|
||||||
|
await uploadImageToComfyUI(blob, FILENAME, DIRECTORY, SUBFOLDER)
|
||||||
|
.then((entry: ComfyImageLocation) => {
|
||||||
|
const meta: ComfyBoxImageMetadata = comfyFileToComfyBoxMetadata(entry);
|
||||||
|
$nodeValue = [meta] // TODO more than one image
|
||||||
|
status = "uploaded"
|
||||||
|
notify("Saved image to ComfyUI!", { type: "success" })
|
||||||
|
onSuccess();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
notify(`Failed to upload image from editor: ${err}`, { type: "error", timeout: 10000 })
|
||||||
|
status = "error"
|
||||||
|
uploadError = err;
|
||||||
|
$nodeValue = []
|
||||||
|
onError();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let closeDialog = null;
|
||||||
|
|
||||||
|
async function saveAndClose() {
|
||||||
|
console.log(closeDialog, kl)
|
||||||
|
if (!closeDialog || !kl)
|
||||||
|
return;
|
||||||
|
|
||||||
|
submitKlecksToComfyUI(() => {}, () => {});
|
||||||
|
closeDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
let blankImageWidth = 512;
|
||||||
|
let blankImageHeight = 512;
|
||||||
|
|
||||||
|
async function openImageEditor() {
|
||||||
|
if (!editorRoot)
|
||||||
|
return;
|
||||||
|
|
||||||
|
showModal = true;
|
||||||
|
|
||||||
|
const url = `http://${location.hostname}:8188` // TODO make configurable
|
||||||
|
|
||||||
|
kl = new Klecks({
|
||||||
|
embedUrl: url,
|
||||||
|
onSubmit: submitKlecksToComfyUI,
|
||||||
|
targetEl: editorRoot,
|
||||||
|
warnOnPageClose: false
|
||||||
|
});
|
||||||
|
|
||||||
|
console.warn("[ImageEditorWidget] OPENING", widget, $nodeValue)
|
||||||
|
|
||||||
|
let canvas = null;
|
||||||
|
let width = blankImageWidth;
|
||||||
|
let height = blankImageHeight;
|
||||||
|
|
||||||
|
if ($nodeValue && $nodeValue.length > 0) {
|
||||||
|
const comfyImage = $nodeValue[0];
|
||||||
|
const comfyURL = comfyBoxImageToComfyURL(comfyImage);
|
||||||
|
[canvas, width, height] = await generateImageCanvas(comfyURL);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
canvas = generateBlankCanvas(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
kl.openProject({
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
layers: [{
|
||||||
|
name: 'Image',
|
||||||
|
opacity: 1,
|
||||||
|
mixModeStr: 'source-over',
|
||||||
|
image: canvas
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
kl.klApp?.out("yo");
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = "empty";
|
||||||
|
let uploadError = null;
|
||||||
|
|
||||||
|
function onUploading() {
|
||||||
|
console.warn("UPLOADING!!!")
|
||||||
|
uploadError = null;
|
||||||
|
status = "uploading"
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUploaded(e: CustomEvent<ComfyImageLocation[]>) {
|
||||||
|
console.warn("UPLOADED!!!")
|
||||||
|
uploadError = null;
|
||||||
|
status = "uploaded"
|
||||||
|
$nodeValue = e.detail.map(comfyFileToComfyBoxMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClear() {
|
||||||
|
console.warn("CLEAR!!!")
|
||||||
|
uploadError = null;
|
||||||
|
status = "empty"
|
||||||
|
$nodeValue = []
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUploadError(e: CustomEvent<any>) {
|
||||||
|
console.warn("ERROR!!!")
|
||||||
|
status = "error"
|
||||||
|
uploadError = e.detail
|
||||||
|
$nodeValue = []
|
||||||
|
notify(`Failed to upload image to ComfyUI: ${uploadError}`, { type: "error", timeout: 10000 })
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChange(e: CustomEvent<ComfyImageLocation[]>) {
|
||||||
|
}
|
||||||
|
|
||||||
|
let _value: ComfyImageLocation[] = []
|
||||||
|
$: if ($nodeValue)
|
||||||
|
_value = $nodeValue.map(comfyBoxImageToComfyFile)
|
||||||
|
else
|
||||||
|
_value = []
|
||||||
|
|
||||||
|
$: canEdit = status === "empty" || status === "uploaded";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="wrapper comfy-image-editor">
|
||||||
|
{#if widget.attrs.variant === "fileUpload" || isMobile}
|
||||||
|
<ImageUpload value={_value}
|
||||||
|
bind:imgWidth
|
||||||
|
bind:imgHeight
|
||||||
|
fileCount={"single"}
|
||||||
|
elem_classes={[]}
|
||||||
|
style={""}
|
||||||
|
label={widget.attrs.title}
|
||||||
|
on:uploading={onUploading}
|
||||||
|
on:uploaded={onUploaded}
|
||||||
|
on:upload_error={onUploadError}
|
||||||
|
on:clear={onClear}
|
||||||
|
on:change={onChange}
|
||||||
|
on:image_clicked={openImageEditor}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="comfy-image-editor-panel">
|
||||||
|
<ImageUpload value={_value}
|
||||||
|
bind:imgWidth
|
||||||
|
bind:imgHeight
|
||||||
|
fileCount={"single"}
|
||||||
|
elem_classes={[]}
|
||||||
|
style={""}
|
||||||
|
label={widget.attrs.title}
|
||||||
|
on:uploading={onUploading}
|
||||||
|
on:uploaded={onUploaded}
|
||||||
|
on:upload_error={onUploadError}
|
||||||
|
on:clear={onClear}
|
||||||
|
on:change={onChange}
|
||||||
|
on:image_clicked={openImageEditor}
|
||||||
|
/>
|
||||||
|
<Modal bind:showModal closeOnClick={false} on:close={disposeEditor} bind:closeDialog>
|
||||||
|
<div>
|
||||||
|
<div id="klecks-loading-screen">
|
||||||
|
<span id="klecks-loading-screen-text"></span>
|
||||||
|
</div>
|
||||||
|
<div class="image-editor-root" bind:this={editorRoot} />
|
||||||
|
</div>
|
||||||
|
<div slot="buttons">
|
||||||
|
<Button variant="primary" on:click={saveAndClose}>
|
||||||
|
Save and Close
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" on:click={closeDialog}>
|
||||||
|
Discard Edits
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
<Block>
|
||||||
|
{#if !$nodeValue || $nodeValue.length === 0}
|
||||||
|
<Row>
|
||||||
|
<Row>
|
||||||
|
<Button variant="secondary" disabled={!canEdit} on:click={openImageEditor}>
|
||||||
|
Create Image
|
||||||
|
</Button>
|
||||||
|
<div>
|
||||||
|
<TextBox show_label={false} disabled={true} value="Status: {status}"/>
|
||||||
|
</div>
|
||||||
|
{#if uploadError}
|
||||||
|
<div>
|
||||||
|
Upload error: {uploadError}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<NumberInput label={"Width"} min={64} max={2048} step={64} bind:value={blankImageWidth} />
|
||||||
|
<NumberInput label={"Height"} min={64} max={2048} step={64} bind:value={blankImageHeight} />
|
||||||
|
</Row>
|
||||||
|
</Row>
|
||||||
|
{:else}
|
||||||
|
<Row>
|
||||||
|
<Button variant="secondary" disabled={!canEdit} on:click={openImageEditor}>
|
||||||
|
Edit Image
|
||||||
|
</Button>
|
||||||
|
<div>
|
||||||
|
<TextBox show_label={false} disabled={true} value="Status: {status}"/>
|
||||||
|
</div>
|
||||||
|
{#if uploadError}
|
||||||
|
<div>
|
||||||
|
Upload error: {uploadError}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Row>
|
||||||
|
{/if}
|
||||||
|
</Block>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.image-editor-root {
|
||||||
|
width: 75vw;
|
||||||
|
height: 75vh;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
color: black;
|
||||||
|
|
||||||
|
:global(> .g-root) {
|
||||||
|
height: calc(100% - 59px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-image-editor {
|
||||||
|
:global(> dialog) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.kl-popup) {
|
||||||
|
z-index: 999999999999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
9
src/lib/nodes/widgets/index.ts
Normal file
9
src/lib/nodes/widgets/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export { default as ComfyWidgetNode } from "./ComfyWidgetNode"
|
||||||
|
export { default as ComfyButtonNode } from "./ComfyButtonNode"
|
||||||
|
export { default as ComfyCheckboxNode } from "./ComfyCheckboxNode"
|
||||||
|
export { default as ComfyComboNode } from "./ComfyComboNode"
|
||||||
|
export { default as ComfyGalleryNode } from "./ComfyGalleryNode"
|
||||||
|
export { default as ComfyImageEditorNode } from "./ComfyImageEditorNode"
|
||||||
|
export { default as ComfyRadioNode } from "./ComfyRadioNode"
|
||||||
|
export { default as ComfyNumberNode } from "./ComfyNumberNode"
|
||||||
|
export { default as ComfyTextNode } from "./ComfyTextNode"
|
||||||
Reference in New Issue
Block a user