diff --git a/src/lib/components/ComfyJourneyView.svelte b/src/lib/components/ComfyJourneyView.svelte index 7404245..02a67ba 100644 --- a/src/lib/components/ComfyJourneyView.svelte +++ b/src/lib/components/ComfyJourneyView.svelte @@ -9,6 +9,7 @@ import workflowState from '$lib/stores/workflowState'; import type { WritableJourneyStateStore } from '$lib/stores/journeyStates'; import JourneyRenderer from './JourneyRenderer.svelte'; + import { Plus } from "svelte-bootstrap-icons"; export let app: ComfyApp; @@ -21,11 +22,42 @@
+
+ +
diff --git a/src/lib/restoreParameters.ts b/src/lib/restoreParameters.ts index ed6f54e..3a1de88 100644 --- a/src/lib/restoreParameters.ts +++ b/src/lib/restoreParameters.ts @@ -29,10 +29,10 @@ export interface RestoreParamSource { */ export interface RestoreParamSourceWorkflowNode extends RestoreParamSource<"workflow"> { type: "workflow", - - sourceNode: SerializedComfyWidgetNode } +export type RestoreParamWorkflowNodeTargets = Record + /* * A value received by the ComfyUI *backend* that corresponds to a value that * was held in a ComfyWidgetNode. These may not necessarily be one-to-one @@ -59,6 +59,9 @@ export interface RestoreParamSourceBackendNodeInput extends RestoreParamSource<" /* * A value contained in the standard prompt extracted from the saved workflow. + * + * This should only be necessary to fall back on if one workflow's parameters + * are to be used in a completely separate workflow's. */ export interface RestoreParamSourceStdPrompt extends RestoreParamSource<"stdPrompt"> { type: "stdPrompt", @@ -102,19 +105,7 @@ export interface RestoreParamSourceStdPrompt extends Resto finalValue: any } -export type RestoreParamTarget = { - /* - * Node that will receive the parameter from the prompt - */ - targetNode: ComfyWidgetNode; - - /* - * Possible sources of values to insert into the target node - */ - sources: RestoreParamSource[] -} - -export type RestoreParamTargets = Record +export type RestoreParamTargets = Record function isSerializedComfyWidgetNode(param: any): param is SerializedComfyWidgetNode { return param != null && typeof param === "object" && "id" in param && "comfyValue" in param @@ -146,24 +137,35 @@ function findUpstreamSerializedWidgetNode(prompt: SerializedPrompt, input: INode } const addSource = (result: RestoreParamTargets, targetNode: ComfyWidgetNode, source: RestoreParamSource) => { - result[targetNode.id] ||= { targetNode, sources: [] } - result[targetNode.id].sources.push(source); + result[targetNode.id] ||= [] + result[targetNode.id].push(source); } -const mergeSources = (a: RestoreParamTargets, b: RestoreParamTargets) => { - for (const [k, vs] of Object.entries(b)) { - a[vs.targetNode.id] ||= { targetNode: vs.targetNode, sources: [] } - for (const source of vs.sources) { - a[vs.targetNode.id].sources.push(source); +export function concatRestoreParams(a: RestoreParamTargets, b: Record): RestoreParamTargets { + for (const [targetNodeID, source] of Object.entries(b)) { + a[targetNodeID] ||= [] + a[targetNodeID].push(source); + } + return a; +} + +export function concatRestoreParams2(a: RestoreParamTargets, b: RestoreParamTargets): RestoreParamTargets { + for (const [targetNodeID, vs] of Object.entries(b)) { + a[targetNodeID] ||= [] + for (const source of vs) { + a[targetNodeID].push(source); } } + return a; } -export function getWorkflowRestoreParams(workflow: ComfyBoxWorkflow, prompt: SerializedPrompt): RestoreParamTargets { +export function getWorkflowRestoreParams(workflow: ComfyBoxWorkflow, prompt: SerializedPrompt): RestoreParamWorkflowNodeTargets { const result = {} const graph = workflow.graph; + // Find nodes that correspond to *this* workflow exactly, since we can + // easily match up the nodes between each (their IDs will be the same) for (const serNode of prompt.workflow.nodes) { const foundNode = graph.getNodeByIdRecursive(serNode.id); if (isComfyWidgetNode(foundNode) && foundNode.type === serNode.type) { @@ -172,9 +174,8 @@ export function getWorkflowRestoreParams(workflow: ComfyBoxWorkflow, prompt: Ser const source: RestoreParamSourceWorkflowNode = { type: "workflow", finalValue, - sourceNode: serNode } - addSource(result, foundNode, source) + result[foundNode.id] = source; } } } @@ -182,11 +183,14 @@ export function getWorkflowRestoreParams(workflow: ComfyBoxWorkflow, prompt: Ser return result } -export function getBackendRestoreParams(workflow: ComfyBoxWorkflow, prompt: SerializedPrompt): RestoreParamTargets { +export function getBackendRestoreParams(workflow: ComfyBoxWorkflow, prompt: SerializedPrompt): Record { const result = {} const graph = workflow.graph; + // Figure out what parameters the backend received. If there was a widget + // node attached to a backend node's input upstream, then we can use that + // value. for (const [serNodeID, inputs] of Object.entries(prompt.output)) { const serNode = prompt.workflow.nodes.find(sn => sn.id === serNodeID) if (serNode == null) @@ -223,16 +227,11 @@ export function getBackendRestoreParams(workflow: ComfyBoxWorkflow, prompt: Seri export default function restoreParameters(workflow: ComfyBoxWorkflow, prompt: SerializedPrompt): RestoreParamTargets { const result = {} - // Step 1: Find nodes that correspond to *this* workflow exactly, since we - // can easily match up the nodes between each (their IDs will be the same) const workflowParams = getWorkflowRestoreParams(workflow, prompt); - mergeSources(result, workflowParams); + concatRestoreParams(result, workflowParams); - // Step 2: Figure out what parameters the backend received. If there was a - // widget node attached to a backend node's input upstream, then we can - // use that value. const backendParams = getBackendRestoreParams(workflow, prompt); - mergeSources(result, backendParams); + concatRestoreParams2(result, backendParams); // Step 3: Extract the standard prompt from the workflow and use that to // infer parameter types diff --git a/src/lib/stores/journeyStates.ts b/src/lib/stores/journeyStates.ts index f5d7888..a609eab 100644 --- a/src/lib/stores/journeyStates.ts +++ b/src/lib/stores/journeyStates.ts @@ -1,7 +1,66 @@ import { writable } from 'svelte/store'; import type { Readable, Writable } from 'svelte/store'; import type { DragItemID, IDragItem } from './layoutStates'; -import type { LGraphNode, NodeID } from '@litegraph-ts/core'; +import type { LGraphNode, NodeID, UUID } from '@litegraph-ts/core'; +import type { SerializedAppState } from '$lib/components/ComfyApp'; +import type { RestoreParamTargets, RestoreParamWorkflowNodeTargets } from '$lib/restoreParameters'; + +export type JourneyNodeType = "root" | "patch"; + +export type JourneyNodeID = UUID; + +export interface JourneyNode { + id: JourneyNodeID, + type: JourneyNodeType, + children: JourneyPatchNode[] +} + +export interface JourneyRootNode extends JourneyNode { + type: "root" + + /* + * This contains all the values of the workflow to set + */ + base: RestoreParamWorkflowNodeTargets +} + +export interface JourneyPatchNode extends JourneyNode { + type: "patch" + + parent: JourneyNode, + + /* + * This contains only the subset of parameters that were changed from the + * parent + */ + patch: RestoreParamWorkflowNodeTargets +} + +export function resolvePatch(node: JourneyNode): RestoreParamWorkflowNodeTargets { + if (node.type === "root") { + return { ...(node as JourneyRootNode).base } + } + + const patchNode = (node as JourneyPatchNode); + const patch = { ...patchNode.patch }; + const base = resolvePatch(patchNode.parent); + for (const [k, v] of Object.entries(patch)) { + base[k] = v; + } + return base; +} + +function diffParams(base: RestoreParamWorkflowNodeTargets, updated: RestoreParamWorkflowNodeTargets): RestoreParamWorkflowNodeTargets { + const result = {} + + for (const [k, v] of Object.entries(updated)) { + if (!(k in base) || base[k].finalValue !== v) { + result[k] = v + } + } + + return result; +} /* * A "journey" is like browser history for prompts, except organized in a @@ -9,27 +68,8 @@ import type { LGraphNode, NodeID } from '@litegraph-ts/core'; * jump between past and present sets of parameters. */ export type JourneyState = { - /* - * Selected drag items. - * NOTE: Order is important, for node grouping actions. - */ - currentJourney: DragItemID[], - - /* - * Hovered drag items. - */ - currentHovered: Set, - - /* - * Selected LGraphNodes inside the litegraph canvas. - * NOTE: Order is important, for node grouping actions. - */ - currentJourneyNodes: LGraphNode[], - - /* - * Currently hovered nodes. - */ - currentHoveredNodes: Set + tree: JourneyNode, + nodesByID: Record } type JourneyStateOps = { @@ -41,18 +81,10 @@ export type WritableJourneyStateStore = Writable & JourneyStateOps function create() { const store: Writable = writable( { - currentJourney: [], - currentJourneyNodes: [], - currentHovered: new Set(), - currentHoveredNodes: new Set(), }) function clear() { store.set({ - currentJourney: [], - currentJourneyNodes: [], - currentHovered: new Set(), - currentHoveredNodes: new Set(), }) }