From 26ab7989c826083cc67ba53bfb650b97445982f0 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Mon, 29 May 2023 00:16:02 -0500 Subject: [PATCH 01/13] Start restore parameters --- src/lib/ComfyBoxStdPrompt.ts | 6 +- src/lib/ComfyBoxStdPromptSerializer.ts | 133 +++++++++++-- src/lib/components/ComfyApp.ts | 3 - src/lib/components/ComfyQueue.svelte | 11 +- src/lib/components/ComfySettingsView.svelte | 2 +- src/lib/components/PromptDisplay.svelte | 205 +++++++++++++------- src/lib/restoreParameters.ts | 205 ++++++++++++++++++++ src/lib/utils.ts | 8 +- src/lib/widgets/ImageUploadWidget.svelte | 12 +- vite.config.ts | 2 +- 10 files changed, 481 insertions(+), 106 deletions(-) create mode 100644 src/lib/restoreParameters.ts diff --git a/src/lib/ComfyBoxStdPrompt.ts b/src/lib/ComfyBoxStdPrompt.ts index 61833a0..0f93f12 100644 --- a/src/lib/ComfyBoxStdPrompt.ts +++ b/src/lib/ComfyBoxStdPrompt.ts @@ -224,13 +224,13 @@ const Metadata = z.object({ extra_data: ExtraData }) -const ComfyBoxStdPrompt = z.object({ +const StdPrompt = z.object({ version: z.number(), metadata: Metadata, parameters: Parameters }) -export default ComfyBoxStdPrompt +export default StdPrompt /* * A standardized Stable Diffusion parameter format that should be used with an @@ -260,4 +260,4 @@ export default ComfyBoxStdPrompt * "see" width 1024 and height 1024, even though the only parameter exposed from * the frontend was the scale of 2.) */ -export type ComfyBoxStdPrompt = z.infer +export type ComfyBoxStdPrompt = z.infer diff --git a/src/lib/ComfyBoxStdPromptSerializer.ts b/src/lib/ComfyBoxStdPromptSerializer.ts index 86f9714..7e33d21 100644 --- a/src/lib/ComfyBoxStdPromptSerializer.ts +++ b/src/lib/ComfyBoxStdPromptSerializer.ts @@ -1,31 +1,88 @@ import type { ComfyBoxStdGroupLoRA, ComfyBoxStdPrompt } from "$lib/ComfyBoxStdPrompt"; -import type { SerializedPrompt, SerializedPromptInputs } from "./components/ComfyApp"; +import StdPrompt from "$lib/ComfyBoxStdPrompt"; +import type { SafeParseReturnType, ZodError } from "zod"; +import type { ComfyNodeID } from "./api"; +import type { SerializedAppState, SerializedPrompt, SerializedPromptInputs, SerializedPromptInputsAll } from "./components/ComfyApp"; +import { ComfyComboNode, type ComfyWidgetNode } from "./nodes/widgets"; +import { basename, isSerializedPromptInputLink } from "./utils"; -export type ComfyPromptConverter = (stdPrompt: ComfyBoxStdPrompt, inputs: SerializedPromptInputs, nodeID: ComfyNodeID) => void; +export type ComfyPromptConverter = { + encoder: ComfyPromptEncoder, + decoder: ComfyPromptDecoder +} + +// +export type ComfyDecodeArgument = { + groupName: string, + keyName: string, + value: any, + widgetNode: ComfyWidgetNode +}; + +export type ComfyPromptEncoder = (stdPrompt: ComfyBoxStdPrompt, inputs: SerializedPromptInputs, nodeID: ComfyNodeID) => void; +export type ComfyPromptDecoder = (args: ComfyDecodeArgument[]) => void; -function LoraLoader(stdPrompt: ComfyBoxStdPrompt, inputs: SerializedPromptInputs) { - const params = stdPrompt.parameters +const LoraLoader: ComfyPromptConverter = { + encoder: (stdPrompt: ComfyBoxStdPrompt, inputs: SerializedPromptInputs) => { + const params = stdPrompt.parameters + const loras: ComfyBoxStdGroupLoRA[] = params.lora - const lora: ComfyBoxStdGroupLoRA = { - model_name: inputs["lora_name"], - strength_unet: inputs["strength_model"], - strength_tenc: inputs["strength_clip"] + for (const lora of loras) { + lora.model_hashes = { + addnet_shorthash: null // TODO find hashes for model! + } + } + }, + decoder: (args: ComfyDecodeArgument[]) => { + // Find corresponding model names in the ComfyUI models folder from the model base filename + for (const arg of args) { + if (arg.groupName === "lora" && arg.keyName === "model_name" && arg.widgetNode.is(ComfyComboNode)) { + const modelBasename = basename(arg.value); + const found = arg.widgetNode.properties.values.find(k => k.indexOf(modelBasename) !== -1) + if (found) + arg.value = found; + } + } } +} + +// input name -> group/key in standard prompt +type ComfyStdPromptMapping = Record - if (params.lora) - params.lora.push(lora) - else - params.lora = [lora] +type ComfyStdPromptSpec = { + paramMapping: ComfyStdPromptMapping, + extraParams?: Record, + converter?: ComfyPromptConverter, } -const ALL_CONVERTERS: Record = { - LoraLoader +const ALL_SPECS: Record = { + "KSampler": { + paramMapping: { + cfg: "k_sampler.cfg_scale", + seed: "k_sampler.seed", + steps: "k_sampler.steps", + sampler_name: "k_sampler.sampler_name", + scheduler: "k_sampler.scheduler", + denoise: "k_sampler.denoise", + }, + }, + "LoraLoader": { + paramMapping: { + lora_name: "lora.model_name", + strength_model: "lora.strength_unet", + strength_clip: "lora.strength_tenc", + }, + extraParams: { + "lora.module_name": "LoRA", + }, + converter: LoraLoader, + } } const COMMIT_HASH: string = __GIT_COMMIT_HASH__; export default class ComfyBoxStdPromptSerializer { - serialize(prompt: SerializedPrompt): ComfyBoxStdPrompt { + serialize(prompt: SerializedPromptInputsAll, workflow?: SerializedAppState): [SafeParseReturnType, any] { const stdPrompt: ComfyBoxStdPrompt = { version: 1, metadata: { @@ -33,23 +90,57 @@ export default class ComfyBoxStdPromptSerializer { commit_hash: COMMIT_HASH, extra_data: { comfybox: { + workflows: [] // TODO!!! } } }, parameters: {} } - for (const [nodeID, inputs] of Object.entries(prompt.output)) { + for (const [nodeID, inputs] of Object.entries(prompt)) { const classType = inputs.class_type - const converter = ALL_CONVERTERS[classType] - if (converter) { - converter(stdPrompt, inputs.inputs, nodeID) + const spec = ALL_SPECS[classType] + if (spec) { + console.warn("SPEC", spec, inputs) + let targets = {} + for (const [comfyKey, stdPromptKey] of Object.entries(spec.paramMapping)) { + const inputValue = inputs.inputs[comfyKey]; + if (inputValue != null && !isSerializedPromptInputLink(inputValue)) { + console.warn("GET", comfyKey, inputValue) + const trail = stdPromptKey.split("."); + let target = null; + + console.warn(trail, trail.length - 2); + for (let index = 0; index < trail.length - 1; index++) { + const name = trail[index]; + if (index === 0) { + targets[name] ||= {} + target = targets[name] + } + else { + target = target[name] + } + console.warn(index, name, target) + } + + let name = trail[trail.length - 1] + target[name] = inputValue + console.warn(stdPrompt.parameters) + } + } + + // TODO converter.encode + + for (const [groupName, group] of Object.entries(targets)) { + stdPrompt.parameters[groupName] ||= [] + stdPrompt.parameters[groupName].push(group) + } } else { - console.warn("No StdPrompt type converter for comfy class!", classType) + console.warn("No StdPrompt type spec for comfy class!", classType) } } - return stdPrompt + return [StdPrompt.safeParse(stdPrompt), stdPrompt]; } } diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts index 97abc08..a711f05 100644 --- a/src/lib/components/ComfyApp.ts +++ b/src/lib/components/ComfyApp.ts @@ -1064,9 +1064,6 @@ export default class ComfyApp { // console.debug(graphToGraphVis(workflow.graph)) // console.debug(promptToGraphVis(p)) - const stdPrompt = this.stdPromptSerializer.serialize(p); - // console.warn("STD", stdPrompt); - const extraData: ComfyBoxPromptExtraData = { extra_pnginfo: { comfyBoxWorkflow: wf, diff --git a/src/lib/components/ComfyQueue.svelte b/src/lib/components/ComfyQueue.svelte index 31719e2..a34af04 100644 --- a/src/lib/components/ComfyQueue.svelte +++ b/src/lib/components/ComfyQueue.svelte @@ -32,6 +32,7 @@ import ComfyQueueGridDisplay from "./ComfyQueueGridDisplay.svelte"; import { WORKFLOWS_VIEW } from "./ComfyBoxWorkflowsView.svelte"; import uiQueueState from "$lib/stores/uiQueueState"; + import type { SerializedAppState, SerializedPromptInputsAll } from "./ComfyApp"; export let app: ComfyApp; @@ -124,19 +125,22 @@ let showModal = false; let expandAll = false; - let selectedPrompt = null; + let selectedPrompt: SerializedPromptInputsAll | null = null; + let selectedWorkflow: SerializedAppState | null = null; let selectedImages = []; function showPrompt(entry: QueueUIEntry) { if (entry.error != null) { showModal = false; expandAll = false; selectedPrompt = null; + selectedWorkflow = null; selectedImages = []; showError(entry.entry.promptID); } else { - selectedPrompt = entry.entry.prompt; + selectedPrompt = entry.entry.prompt, + selectedWorkflow = entry.entry.extraData.extra_pnginfo.comfyBoxWorkflow selectedImages = entry.images; showModal = true; expandAll = false @@ -145,6 +149,7 @@ function closeModal() { selectedPrompt = null + selectedWorkflow = null; selectedImages = [] showModal = false; expandAll = false; @@ -165,7 +170,7 @@ {#if selectedPrompt} - { closeModal(); closeDialog(); }} {app} prompt={selectedPrompt} images={selectedImages} {expandAll} /> + { closeModal(); closeDialog(); }} {app} prompt={selectedPrompt} workflow={selectedWorkflow} images={selectedImages} {expandAll} /> {/if}
diff --git a/src/lib/components/ComfySettingsView.svelte b/src/lib/components/ComfySettingsView.svelte index 15d3dcd..f2643a1 100644 --- a/src/lib/components/ComfySettingsView.svelte +++ b/src/lib/components/ComfySettingsView.svelte @@ -173,7 +173,7 @@ } .comfy-settings-entries { - padding: 3rem 3rem; + padding: 2rem 0.75rem; height: 100%; } diff --git a/src/lib/components/PromptDisplay.svelte b/src/lib/components/PromptDisplay.svelte index b96e2c7..88cc5d8 100644 --- a/src/lib/components/PromptDisplay.svelte +++ b/src/lib/components/PromptDisplay.svelte @@ -1,6 +1,6 @@ + +
+ + {#each nodes as node} + + {/each} + + {#each edges as edge} + + {/each} + +
+ + diff --git a/src/lib/components/ComfyPaneView.svelte b/src/lib/components/ComfyPaneView.svelte index 36b6a0b..fcff94a 100644 --- a/src/lib/components/ComfyPaneView.svelte +++ b/src/lib/components/ComfyPaneView.svelte @@ -1,5 +1,5 @@ + + + +
+ {#if cyInstance} + + {/if} +
+ + diff --git a/src/lib/components/graph/GraphEdge.svelte b/src/lib/components/graph/GraphEdge.svelte new file mode 100644 index 0000000..2150c33 --- /dev/null +++ b/src/lib/components/graph/GraphEdge.svelte @@ -0,0 +1,15 @@ + diff --git a/src/lib/components/graph/GraphNode.svelte b/src/lib/components/graph/GraphNode.svelte new file mode 100644 index 0000000..a9ce900 --- /dev/null +++ b/src/lib/components/graph/GraphNode.svelte @@ -0,0 +1,15 @@ + diff --git a/src/lib/components/graph/GraphStyles.ts b/src/lib/components/graph/GraphStyles.ts new file mode 100644 index 0000000..ed3528a --- /dev/null +++ b/src/lib/components/graph/GraphStyles.ts @@ -0,0 +1,57 @@ +export default [ + { + selector: 'node', + style: { + 'width': '50', + 'height': '50', + 'font-size': '18', + 'font-weight': 'bold', + 'content': `data(label)`, + 'text-valign': 'center', + 'text-wrap': 'wrap', + 'text-max-width': '140', + 'background-color': 'gold', + 'border-color': 'orange', + 'border-width': '3', + 'color': 'darkred' + } + }, + { + selector: 'node:selected', + style: { + 'background-color': 'darkred', + color: 'white', + 'border-color': 'darkred', + 'line-color': '#0e76ba', + 'target-arrow-color': '#0e76ba' + } + }, + { + selector: 'edge', + style: { + 'curve-style': 'bezier', + 'color': 'darkred', + 'text-background-color': '#ffffff', + 'text-background-opacity': '1', + 'text-background-padding': '3', + 'width': '3', + 'target-arrow-shape': 'triangle', + 'line-color': 'darkred', + 'target-arrow-color': 'darkred', + 'font-weight': 'bold' + } + }, + { + selector: 'edge[label]', + style: { + 'content': `data(label)`, + } + }, + { + selector: 'edge.label', + style: { + 'line-color': 'orange', + 'target-arrow-color': 'orange' + } + } +] From c8a3276336d7c1dfd8a77a383808d7c9356529c3 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Fri, 2 Jun 2023 15:51:53 -0500 Subject: [PATCH 04/13] journeystuff --- src/lib/components/ComfyJourneyView.svelte | 44 +++--------- src/lib/components/JourneyRenderer.svelte | 38 ++++++++++ src/lib/components/graph/Graph.svelte | 81 ++++++++++++++-------- src/lib/components/graph/GraphEdge.svelte | 4 +- src/lib/components/graph/GraphNode.svelte | 20 +++--- src/lib/components/graph/GraphStyles.ts | 22 +++--- src/lib/stores/journeyStates.ts | 74 ++++++++++++++++++++ src/lib/stores/workflowState.ts | 10 ++- 8 files changed, 206 insertions(+), 87 deletions(-) create mode 100644 src/lib/components/JourneyRenderer.svelte create mode 100644 src/lib/stores/journeyStates.ts diff --git a/src/lib/components/ComfyJourneyView.svelte b/src/lib/components/ComfyJourneyView.svelte index 087feb2..7404245 100644 --- a/src/lib/components/ComfyJourneyView.svelte +++ b/src/lib/components/ComfyJourneyView.svelte @@ -4,49 +4,23 @@ jump between past and present sets of parameters. -->
- - {#each nodes as node} - - {/each} - - {#each edges as edge} - - {/each} - +
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); +} + +export function concatRestoreParams(a: RestoreParamTargets, b: Record): RestoreParamTargets { + for (const [targetNodeID, source] of Object.entries(b)) { + a[targetNodeID] ||= [] + a[targetNodeID].push(source); + } + return a; } -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 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,35 +1,75 @@ 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'; -/* - * A "journey" is like browser history for prompts, except organized in a - * tree-like graph. It lets you save incremental changes to your workflow and - * jump between past and present sets of parameters. - */ -export type JourneyState = { - /* - * Selected drag items. - * NOTE: Order is important, for node grouping actions. - */ - currentJourney: DragItemID[], +export type JourneyNodeType = "root" | "patch"; - /* - * Hovered drag items. - */ - currentHovered: Set, +export type JourneyNodeID = UUID; + +export interface JourneyNode { + id: JourneyNodeID, + type: JourneyNodeType, + children: JourneyPatchNode[] +} + +export interface JourneyRootNode extends JourneyNode { + type: "root" /* - * Selected LGraphNodes inside the litegraph canvas. - * NOTE: Order is important, for node grouping actions. + * This contains all the values of the workflow to set */ - currentJourneyNodes: LGraphNode[], + base: RestoreParamWorkflowNodeTargets +} + +export interface JourneyPatchNode extends JourneyNode { + type: "patch" + + parent: JourneyNode, /* - * Currently hovered nodes. + * This contains only the subset of parameters that were changed from the + * parent */ - currentHoveredNodes: Set + 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 + * tree-like graph. It lets you save incremental changes to your workflow and + * jump between past and present sets of parameters. + */ +export type JourneyState = { + 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(), }) } From 4f237d01a512e53be82978e73ab6cabd62ca4258 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Fri, 2 Jun 2023 21:01:30 -0500 Subject: [PATCH 06/13] Very basic graph parameters --- src/lib/components/ComfyJourneyView.svelte | 18 ++- src/lib/components/JourneyRenderer.svelte | 133 ++++++++++++++++----- src/lib/components/graph/Graph.svelte | 8 ++ src/lib/components/graph/GraphStyles.ts | 86 +++++++------ src/lib/restoreParameters.ts | 31 ++++- src/lib/stores/journeyStates.ts | 122 ++++++++++++++++++- 6 files changed, 325 insertions(+), 73 deletions(-) diff --git a/src/lib/components/ComfyJourneyView.svelte b/src/lib/components/ComfyJourneyView.svelte index 02a67ba..74a0d39 100644 --- a/src/lib/components/ComfyJourneyView.svelte +++ b/src/lib/components/ComfyJourneyView.svelte @@ -10,6 +10,8 @@ import type { WritableJourneyStateStore } from '$lib/stores/journeyStates'; import JourneyRenderer from './JourneyRenderer.svelte'; import { Plus } from "svelte-bootstrap-icons"; + import { getWorkflowRestoreParams, getWorkflowRestoreParamsFromWorkflow } from '$lib/restoreParameters'; + import notify from '$lib/notify'; export let app: ComfyApp; @@ -18,6 +20,20 @@ $: workflow = $workflowState.activeWorkflow $: journey = workflow?.journey + + function doAdd() { + if (!workflow) { + notify("No active workflow!", { type: "error" }) + return; + } + + const nodes = Array.from(journey.iterateBreadthFirst()); + let parent = null; + if (nodes.length > 0) + parent = nodes[nodes.length - 1] + const workflowParams = getWorkflowRestoreParamsFromWorkflow(workflow) + journey.addNode(workflowParams, parent?.id); + }
@@ -25,7 +41,7 @@
diff --git a/src/lib/components/JourneyRenderer.svelte b/src/lib/components/JourneyRenderer.svelte index fa0f9f0..1336076 100644 --- a/src/lib/components/JourneyRenderer.svelte +++ b/src/lib/components/JourneyRenderer.svelte @@ -1,38 +1,115 @@ {#if workflow && journey} - + {/if} diff --git a/src/lib/components/graph/Graph.svelte b/src/lib/components/graph/Graph.svelte index cde6236..9c2f591 100644 --- a/src/lib/components/graph/Graph.svelte +++ b/src/lib/components/graph/Graph.svelte @@ -12,12 +12,17 @@ import GraphStyles from "./GraphStyles" import type { EdgeDataDefinition } from "cytoscape"; import type { NodeDataDefinition } from "cytoscape"; + import { createEventDispatcher } from "svelte"; export let nodes: ReadonlyArray; export let edges: ReadonlyArray; export let style: string = "" + const dispatch = createEventDispatcher<{ + rebuilt: { cyto: cytoscape.Core }; + }>(); + $: if (nodes != null && edges != null && refElement != null) { rebuildGraph() } @@ -35,6 +40,7 @@ container: refElement, style: GraphStyles, wheelSensitivity: 0.1, + maxZoom: 1, }) cyInstance.on("add", () => { @@ -60,6 +66,8 @@ data: { ...edge } }) } + + dispatch("rebuilt", { cyto: cyInstance }) } let refElement = null diff --git a/src/lib/components/graph/GraphStyles.ts b/src/lib/components/graph/GraphStyles.ts index d4e4cc0..32db14f 100644 --- a/src/lib/components/graph/GraphStyles.ts +++ b/src/lib/components/graph/GraphStyles.ts @@ -2,59 +2,73 @@ import type { Stylesheet } from "cytoscape"; const styles: Stylesheet[] = [ { - selector: 'node', + selector: "core", style: { - 'width': '50', - 'height': '50', - 'font-family': 'Arial', - 'font-size': '18', - 'font-weight': 'normal', - 'content': `data(label)`, - 'text-valign': 'center', - 'text-wrap': 'wrap', - 'text-max-width': '140', - 'background-color': '#60a5fa', - 'border-color': '#2563eb', - 'border-width': '3', - 'color': '#1d3660' + "selection-box-color": "#ddd", + "selection-box-opacity": 0.65, + "selection-box-border-color": "#aaa", + "selection-box-border-width": 1, + "active-bg-color": "#4b5563", + "active-bg-opacity": 0.35, + "active-bg-size": 30, + "outside-texture-bg-color": "#000", + "outside-texture-bg-opacity": 0.125, } }, { - selector: 'node:selected', + selector: "node", style: { - 'background-color': '#f97316', - color: 'white', - 'border-color': '#ea580c', - 'line-color': '#0e76ba', - 'target-arrow-color': '#0e76ba' + "width": "50", + "height": "50", + "font-family": "Arial", + "font-size": "18", + "font-weight": "normal", + "content": `data(label)`, + "text-valign": "center", + "text-wrap": "wrap", + "text-max-width": "140", + "background-color": "#60a5fa", + "border-color": "#2563eb", + "border-width": "3", + "color": "#1d3660" } }, { - selector: 'edge', + selector: "node:selected", style: { - 'curve-style': 'bezier', - 'color': 'darkred', - 'text-background-color': '#ffffff', - 'text-background-opacity': '1', - 'text-background-padding': '3', - 'width': '3', - 'target-arrow-shape': 'triangle', - 'line-color': '#1d4ed8', - 'target-arrow-color': '#1d4ed8', - 'font-weight': 'bold' + "background-color": "#f97316", + "color": "white", + "border-color": "#ea580c", + "line-color": "#0e76ba", + "target-arrow-color": "#0e76ba" } }, { - selector: 'edge[label]', + selector: "edge", style: { - 'content': `data(label)`, + "curve-style": "bezier", + "color": "darkred", + "text-background-color": "#ffffff", + "text-background-opacity": 1, + "text-background-padding": "3", + "width": 3, + "target-arrow-shape": "triangle", + "line-color": "#1d4ed8", + "target-arrow-color": "#1d4ed8", + "font-weight": "bold" } }, { - selector: 'edge.label', + selector: "edge[label]", style: { - 'line-color': 'orange', - 'target-arrow-color': 'orange' + "content": `data(label)`, + } + }, + { + selector: "edge.label", + style: { + "line-color": "orange", + "target-arrow-color": "orange" } } ] diff --git a/src/lib/restoreParameters.ts b/src/lib/restoreParameters.ts index 3a1de88..baead8a 100644 --- a/src/lib/restoreParameters.ts +++ b/src/lib/restoreParameters.ts @@ -1,4 +1,4 @@ -import type { INodeInputSlot, NodeID } from "@litegraph-ts/core"; +import type { INodeInputSlot, NodeID, SerializedLGraph } from "@litegraph-ts/core"; import type { SerializedPrompt } from "./components/ComfyApp"; import type { ComfyWidgetNode } from "./nodes/widgets"; import type { SerializedComfyWidgetNode } from "./nodes/widgets/ComfyWidgetNode"; @@ -159,14 +159,37 @@ export function concatRestoreParams2(a: RestoreParamTargets, b: RestoreParamTarg return a; } -export function getWorkflowRestoreParams(workflow: ComfyBoxWorkflow, prompt: SerializedPrompt): RestoreParamWorkflowNodeTargets { +/* + * Like getWorkflowRestoreParams but applies to an instanced (non-serialized) workflow + */ +export function getWorkflowRestoreParamsFromWorkflow(workflow: ComfyBoxWorkflow): RestoreParamWorkflowNodeTargets { + const result = {} + + for (const node of workflow.graph.iterateNodesInOrderRecursive()) { + if (!isComfyWidgetNode(node)) + continue; + + const finalValue = node.getValue(); + if (finalValue != null) { + const source: RestoreParamSourceWorkflowNode = { + type: "workflow", + finalValue, + } + result[node.id] = source; + } + } + + return result +} + +export function getWorkflowRestoreParams(workflow: ComfyBoxWorkflow, prompt: SerializedLGraph): 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) { + for (const serNode of prompt.nodes) { const foundNode = graph.getNodeByIdRecursive(serNode.id); if (isComfyWidgetNode(foundNode) && foundNode.type === serNode.type) { const finalValue = (serNode as SerializedComfyWidgetNode).comfyValue; @@ -227,7 +250,7 @@ export function getBackendRestoreParams(workflow: ComfyBoxWorkflow, prompt: Seri export default function restoreParameters(workflow: ComfyBoxWorkflow, prompt: SerializedPrompt): RestoreParamTargets { const result = {} - const workflowParams = getWorkflowRestoreParams(workflow, prompt); + const workflowParams = getWorkflowRestoreParams(workflow, prompt.workflow); concatRestoreParams(result, workflowParams); const backendParams = getBackendRestoreParams(workflow, prompt); diff --git a/src/lib/stores/journeyStates.ts b/src/lib/stores/journeyStates.ts index a609eab..820df69 100644 --- a/src/lib/stores/journeyStates.ts +++ b/src/lib/stores/journeyStates.ts @@ -1,9 +1,10 @@ -import { writable } from 'svelte/store'; +import { get, writable } from 'svelte/store'; import type { Readable, Writable } from 'svelte/store'; import type { DragItemID, IDragItem } from './layoutStates'; import type { LGraphNode, NodeID, UUID } from '@litegraph-ts/core'; import type { SerializedAppState } from '$lib/components/ComfyApp'; import type { RestoreParamTargets, RestoreParamWorkflowNodeTargets } from '$lib/restoreParameters'; +import { v4 as uuidv4 } from "uuid"; export type JourneyNodeType = "root" | "patch"; @@ -62,18 +63,33 @@ function diffParams(base: RestoreParamWorkflowNodeTargets, updated: RestoreParam return result; } +function calculatePatch(parent: JourneyNode, newParams: RestoreParamWorkflowNodeTargets): RestoreParamWorkflowNodeTargets { + const patch = resolvePatch(parent); + const diff = diffParams(patch, newParams) + return diff; +} + /* * A "journey" is like browser history for prompts, except organized in a * tree-like graph. It lets you save incremental changes to your workflow and * jump between past and present sets of parameters. */ export type JourneyState = { - tree: JourneyNode, - nodesByID: Record + root: JourneyRootNode | null, + nodesByID: Record, + activeNodeID: JourneyNodeID | null, + + /* + * Incremented when graph structure is updated + */ + version: number } type JourneyStateOps = { clear: () => void, + addNode: (params: RestoreParamWorkflowNodeTargets, parent?: JourneyNodeID) => JourneyNode, + selectNode: (id?: JourneyNodeID) => void, + iterateBreadthFirst: (id?: JourneyNodeID | null) => Iterable } export type WritableJourneyStateStore = Writable & JourneyStateOps; @@ -81,16 +97,114 @@ export type WritableJourneyStateStore = Writable & JourneyStateOps function create() { const store: Writable = writable( { + root: null, + nodesByID: {}, + activeNodeID: null, + version: 0 }) function clear() { store.set({ + root: null, + nodesByID: {}, + activeNodeID: null, + version: 0 }) } + /* + * params: full state of widgets in the UI + * parent: parent node to patch against + */ + function addNode(params: RestoreParamWorkflowNodeTargets, parent?: JourneyNodeID): JourneyNode { + let _node: JourneyRootNode | JourneyPatchNode; + store.update(s => { + let parentNode: JourneyNode | null = null + if (parent != null) { + parentNode = s.nodesByID[parent]; + if (parentNode == null) { + throw new Error(`Could not find parent node ${parent} to insert into!`) + } + } + if (parentNode == null) { + _node = { + id: uuidv4(), + type: "root", + children: [], + base: { ...params } + } + s.root = _node + } + else { + _node = { + id: uuidv4(), + type: "patch", + parent: parentNode, + children: [], + patch: calculatePatch(parentNode, params) + } + parentNode.children.push(_node); + } + s.nodesByID[_node.id] = _node; + s.version += 1; + return s; + }); + return _node; + } + + function selectNode(id?: JourneyNodeID) { + store.update(s => { + s.activeNodeID = id; + return s; + }) + } + + // function removeNode(id: JourneyNodeID) { + // store.update(s => { + // const node = s.nodesByID[id]; + // if (node == null) { + // throw new Error(`Journey node not found: ${id}`) + // } + + // if (node.type === "patch") { + + // } + // else { + // s.root = null; + // } + + // delete s.nodesByID[id]; + // s.version += 1; + + // return s; + // }); + // } + + function* iterateBreadthFirst(id?: JourneyNodeID | null): Iterable { + const state = get(store); + + id ||= state.root?.id; + if (id == null) + return; + + const queue = [state.nodesByID[id]]; + while (queue.length > 0) { + const node = queue.shift(); + yield node; + if (node.children) { + for (const child of node.children) { + queue.push(state.nodesByID[child.id]); + } + } + } + } + return { ...store, - clear + clear, + addNode, + selectNode, + iterateBreadthFirst } } From ab6266704c90f63ae8a68bdfb3d9abd9f7e1673d Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Fri, 2 Jun 2023 22:28:18 -0500 Subject: [PATCH 07/13] Journey functionality --- package.json | 5 +- pnpm-lock.yaml | 286 ++++++++++++++++++++- src/lib/components/ComfyJourneyView.svelte | 81 +++++- src/lib/components/JourneyRenderer.svelte | 67 +++-- src/lib/components/WidgetContainer.svelte | 5 + src/lib/components/graph/Graph.svelte | 7 +- src/lib/stores/journeyStates.ts | 39 ++- src/lib/stores/selectionState.ts | 13 +- src/lib/stores/workflowState.ts | 18 +- src/tests/stores/journeyStatesTests.ts | 71 +++++ src/tests/testSuite.ts | 1 + 11 files changed, 537 insertions(+), 56 deletions(-) create mode 100644 src/tests/stores/journeyStatesTests.ts diff --git a/package.json b/package.json index 42dd2c7..268994f 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "devDependencies": { "@floating-ui/core": "^1.2.6", "@floating-ui/dom": "^1.2.8", + "@types/cytoscape": "^3.19.9", + "@types/dompurify": "^3.0.2", "@zerodevx/svelte-toast": "^0.9.3", "eslint": "^8.37.0", "eslint-config-prettier": "^8.8.0", @@ -34,8 +36,6 @@ "svelte-check": "^3.2.0", "svelte-dnd-action": "^0.9.22", "typescript": "^5.0.3", - "@types/cytoscape": "^3.19.9", - "@types/dompurify": "^3.0.2", "vite": "^4.3.8", "vite-plugin-glsl": "^1.1.2", "vite-plugin-static-copy": "^0.14.0", @@ -84,6 +84,7 @@ "csv-parse": "^5.3.10", "cytoscape": "^3.25.0", "cytoscape-dagre": "^2.5.0", + "deep-equal": "^2.2.1", "dompurify": "^3.0.3", "events": "^3.3.0", "framework7": "^8.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 451b183..fc56c77 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -121,6 +121,9 @@ importers: cytoscape-dagre: specifier: ^2.5.0 version: 2.5.0(cytoscape@3.25.0) + deep-equal: + specifier: ^2.2.1 + version: 2.2.1 dompurify: specifier: ^3.0.3 version: 3.0.3 @@ -1372,7 +1375,6 @@ packages: '@codemirror/language': ^6.0.0 '@codemirror/state': ^6.0.0 '@codemirror/view': ^6.0.0 - '@lezer/common': ^1.0.0 dependencies: '@codemirror/language': 6.6.0 '@codemirror/state': 6.2.0 @@ -2715,6 +2717,13 @@ packages: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: true + /array-buffer-byte-length@1.0.0: + resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + dependencies: + call-bind: 1.0.2 + is-array-buffer: 3.0.2 + dev: false + /array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -2765,6 +2774,11 @@ packages: postcss-value-parser: 4.2.0 dev: true + /available-typed-arrays@1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + dev: false + /aws-sign2@0.7.0: resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} dev: false @@ -3131,8 +3145,6 @@ packages: '@codemirror/search': 6.4.0 '@codemirror/state': 6.2.0 '@codemirror/view': 6.11.0 - transitivePeerDependencies: - - '@lezer/common' dev: false /codemirror@6.0.1(@lezer/common@1.0.2): @@ -3576,6 +3588,29 @@ packages: dependencies: type-detect: 4.0.8 + /deep-equal@2.2.1: + resolution: {integrity: sha512-lKdkdV6EOGoVn65XaOsPdH4rMxTZOnmFyuIkMjM1i5HHCbfjC97dawgTAy0deYNfuqUqW+Q5VrVaQYtUpSd6yQ==} + dependencies: + array-buffer-byte-length: 1.0.0 + call-bind: 1.0.2 + es-get-iterator: 1.1.3 + get-intrinsic: 1.2.0 + is-arguments: 1.1.1 + is-array-buffer: 3.0.2 + is-date-object: 1.0.5 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + isarray: 2.0.5 + object-is: 1.1.5 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.5.0 + side-channel: 1.0.4 + which-boxed-primitive: 1.0.2 + which-collection: 1.0.1 + which-typed-array: 1.1.9 + dev: false + /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true @@ -3589,6 +3624,14 @@ packages: engines: {node: '>=8'} dev: true + /define-properties@1.2.0: + resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} + engines: {node: '>= 0.4'} + dependencies: + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 + dev: false + /delaunator@5.0.0: resolution: {integrity: sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==} dependencies: @@ -3690,6 +3733,20 @@ packages: is-arrayish: 0.2.1 dev: true + /es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.0 + has-symbols: 1.0.3 + is-arguments: 1.1.1 + is-map: 2.0.2 + is-set: 2.0.2 + is-string: 1.0.7 + isarray: 2.0.5 + stop-iteration-iterator: 1.0.0 + dev: false + /es6-promise@3.3.1: resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} @@ -4295,6 +4352,12 @@ packages: resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} dev: true + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: false + /forever-agent@0.6.1: resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} dev: false @@ -4385,6 +4448,10 @@ packages: /function-bind@1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: false + /gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -4495,6 +4562,12 @@ packages: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} dev: true + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.0 + dev: false + /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -4533,6 +4606,10 @@ packages: har-schema: 2.0.0 dev: false + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: false + /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} @@ -4543,11 +4620,24 @@ packages: engines: {node: '>=8'} dev: true + /has-property-descriptors@1.0.0: + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + dependencies: + get-intrinsic: 1.2.0 + dev: false + /has-symbols@1.0.3: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} dev: false + /has-tostringtag@1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: false + /has@1.0.3: resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} engines: {node: '>= 0.4.0'} @@ -4686,26 +4776,77 @@ packages: /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + /internal-slot@1.0.5: + resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.0 + has: 1.0.3 + side-channel: 1.0.4 + dev: false + /internmap@2.0.3: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} dev: false + /is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: false + + /is-array-buffer@3.0.2: + resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.0 + is-typed-array: 1.1.10 + dev: false + /is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} dev: true + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: false + /is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} dependencies: binary-extensions: 2.2.0 + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: false + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: false + /is-core-module@2.12.0: resolution: {integrity: sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==} dependencies: has: 1.0.3 + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + /is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} @@ -4732,6 +4873,17 @@ packages: dependencies: is-extglob: 2.1.1 + /is-map@2.0.2: + resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==} + dev: false + + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + /is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -4745,15 +4897,69 @@ packages: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} dev: true + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: false + + /is-set@2.0.2: + resolution: {integrity: sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==} + dev: false + + /is-shared-array-buffer@1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.2 + dev: false + /is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} dev: true + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: false + + /is-typed-array@1.1.10: + resolution: {integrity: sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + dev: false + /is-typedarray@1.0.0: resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} dev: false + /is-weakmap@2.0.1: + resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==} + dev: false + + /is-weakset@2.0.2: + resolution: {integrity: sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.0 + dev: false + /is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} @@ -4765,6 +4971,10 @@ packages: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} dev: false + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: false + /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true @@ -5763,6 +5973,29 @@ packages: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} dev: false + /object-is@1.1.5: + resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + dev: false + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: false + + /object.assign@4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: false + /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: @@ -6182,6 +6415,15 @@ packages: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} dev: false + /regexp.prototype.flags@1.5.0: + resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + functions-have-names: 1.2.3 + dev: false + /request@2.88.2: resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==} engines: {node: '>= 6'} @@ -6554,6 +6796,13 @@ packages: /std-env@3.3.3: resolution: {integrity: sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==} + /stop-iteration-iterator@1.0.0: + resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} + engines: {node: '>= 0.4'} + dependencies: + internal-slot: 1.0.5 + dev: false + /stream-transform@3.2.6: resolution: {integrity: sha512-/pyOvaCQFqYTmrFhmMbnAEVo3SsTx1H39eUVPOtYeAgbEUc+rDo7GoP8LbHJgU83mKtzJe/7Nq/ipaAnUOHgJQ==} dev: false @@ -8408,6 +8657,37 @@ packages: webidl-conversions: 3.0.1 dev: false + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: false + + /which-collection@1.0.1: + resolution: {integrity: sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==} + dependencies: + is-map: 2.0.2 + is-set: 2.0.2 + is-weakmap: 2.0.1 + is-weakset: 2.0.2 + dev: false + + /which-typed-array@1.1.9: + resolution: {integrity: sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + is-typed-array: 1.1.10 + dev: false + /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} diff --git a/src/lib/components/ComfyJourneyView.svelte b/src/lib/components/ComfyJourneyView.svelte index 74a0d39..69bdabd 100644 --- a/src/lib/components/ComfyJourneyView.svelte +++ b/src/lib/components/ComfyJourneyView.svelte @@ -7,11 +7,12 @@ import type ComfyApp from './ComfyApp'; import type { ComfyBoxWorkflow } from '$lib/stores/workflowState'; import workflowState from '$lib/stores/workflowState'; - import type { WritableJourneyStateStore } from '$lib/stores/journeyStates'; + import { calculateWorkflowParamsPatch, resolvePatch, type JourneyPatchNode, type WritableJourneyStateStore } from '$lib/stores/journeyStates'; import JourneyRenderer from './JourneyRenderer.svelte'; import { Plus } from "svelte-bootstrap-icons"; import { getWorkflowRestoreParams, getWorkflowRestoreParamsFromWorkflow } from '$lib/restoreParameters'; import notify from '$lib/notify'; + import selectionState from '$lib/stores/selectionState'; export let app: ComfyApp; @@ -27,20 +28,86 @@ return; } - const nodes = Array.from(journey.iterateBreadthFirst()); - let parent = null; - if (nodes.length > 0) - parent = nodes[nodes.length - 1] const workflowParams = getWorkflowRestoreParamsFromWorkflow(workflow) - journey.addNode(workflowParams, parent?.id); + const activeNode = journey.getActiveNode(); + + let journeyNode + + if (activeNode == null) { + // add root node + if ($journey.root != null) { + return; + } + journeyNode = journey.addNode(workflowParams, null); + notify("Pushed a new base workflow state.", { type: "info" }) + } + else { + // add patch node + const patch = calculateWorkflowParamsPatch(activeNode, workflowParams); + const patchedCount = Object.keys(patch).length; + if (patchedCount === 0) { + notify("No changes were made to active parameters yet.", { type: "warning" }) + return; + } + journeyNode = journey.addNode(patch, activeNode); + notify(`Pushed new state with ${patchedCount} changes.`, { type: "info" }) + } + + if (journeyNode != null) { + journey.selectNode(journeyNode); + } + } + + function onSelectNode(e: CustomEvent<{ cyto: cytoscape.Core, node: cytoscape.NodeSingular }>) { + const { node } = e.detail; + + const id = node.id(); + const journeyNode = $journey.nodesByID[id]; + if (journeyNode == null) { + console.error("[ComfyJourneyView] Missing journey node!", id) + return; + } + + const patch = resolvePatch(journeyNode); + + // ensure reactive state is updated + workflow.applyParamsPatch(patch); + $workflowState = $workflowState + } + + function onHoverNode(e: CustomEvent<{ cyto: cytoscape.Core, node: cytoscape.NodeSingular }>) { + const { node } = e.detail; + + const id = node.id(); + const journeyNode = $journey.nodesByID[id]; + if (journeyNode == null) { + console.error("[ComfyJourneyView] Missing journey node!", id) + return; + } + + if (journeyNode.type === "patch") { + $selectionState.currentPatchHoveredNodes = new Set(Object.keys((journeyNode as JourneyPatchNode).patch)) + } + else { + $selectionState.currentPatchHoveredNodes = new Set(); + } + } + + function onHoverNodeOut(e: CustomEvent<{ cyto: cytoscape.Core, node: cytoscape.NodeSingular }>) { + $selectionState.currentPatchHoveredNodes = new Set(); }
- +
diff --git a/src/lib/components/JourneyRenderer.svelte b/src/lib/components/JourneyRenderer.svelte index 1336076..1a3d054 100644 --- a/src/lib/components/JourneyRenderer.svelte +++ b/src/lib/components/JourneyRenderer.svelte @@ -4,34 +4,19 @@ import { get } from 'svelte/store'; import Graph from './graph/Graph.svelte' import type { NodeDataDefinition, EdgeDataDefinition } from 'cytoscape'; + import { createEventDispatcher } from "svelte"; + import selectionState from '$lib/stores/selectionState'; export let workflow: ComfyBoxWorkflow | null = null export let journey: WritableJourneyStateStore | null = null - // - // const nodes: NodeDataDefinition[] = [ - // //{ id: 'N1', label: 'Start' }, - // //{ id: 'N2', label: '4' }, - // //{ id: 'N4', label: '8' }, - // //{ id: 'N5', label: '15' }, - // //{ id: 'N3', label: '16' }, - // //{ id: 'N6', label: '23' }, - // //{ id: 'N7', label: '42' }, - // //{ id: 'N8', label: 'End' } - // ] - // - // const edges: EdgeDataDefinition[] = [ - // //{ id: 'E1', source: 'N1', target: 'N2' }, - // //{ id: 'E2', source: 'N2', target: 'N3' }, - // //{ id: 'E3', source: 'N3', target: 'N6' }, - // //{ id: 'E4', source: 'N2', target: 'N4' }, - // //{ id: 'E5', source: 'N4', target: 'N5' }, - // //{ id: 'E6', source: 'N5', target: 'N4', label: '2' }, - // //{ id: 'E7', source: 'N5', target: 'N6' }, - // //{ id: 'E8', source: 'N6', target: 'N7' }, - // //{ id: 'E9', source: 'N7', target: 'N7', label: '3' }, - // //{ id: 'E10', source: 'N7', target: 'N8' } - // ] + const dispatch = createEventDispatcher<{ + select_node: { cyto: cytoscape.Core, node: cytoscape.NodeSingular }; + hover_node: { cyto: cytoscape.Core, node: cytoscape.NodeSingular }; + hover_node_out: { cyto: cytoscape.Core, node: cytoscape.NodeSingular }; + }>(); + + let lastSelected = null; let lastVersion = -1; @@ -48,6 +33,7 @@ } const journeyState = get(journey); + lastSelected = journeyState.activeNodeID; const nodes: NodeDataDefinition[] = [] const edges: EdgeDataDefinition[] = [] @@ -86,17 +72,48 @@ function onNodeSelected(e: cytoscape.InputEventObject) { console.warn("SELECT", e) - const node = e.target; + const node = e.target as cytoscape.NodeSingular; journey.selectNode(node.id()); + + e.cy.animate({ + center: { eles: node } + }, { + duration: 400, + easing: "ease-in-out-quad" + }); + e.cy.center(node) + + dispatch("select_node", { cyto: e.cy, node }) + } + + function onNodeHovered(e: cytoscape.InputEventObject) { + const node = e.target as cytoscape.NodeSingular; + dispatch("hover_node", { cyto: e.cy, node }) + } + + function onNodeHoveredOut(e: cytoscape.InputEventObject) { + const node = e.target as cytoscape.NodeSingular; + dispatch("hover_node_out", { cyto: e.cy, node }) } function onRebuilt(e: CustomEvent<{cyto: cytoscape.Core}>) { const { cyto } = e.detail; + for (const node of cyto.nodes().components()) { + if (node.id() === lastSelected) { + // why doesn't passing `selected` work in the ctor? + node.select(); + } + } + + $selectionState.currentPatchHoveredNodes = new Set() + cyto.nodes() .lock() .on("select", onNodeSelected) + .on("mouseover", onNodeHovered) + .on("mouseout", onNodeHoveredOut) const nodes = Array.from(journey.iterateBreadthFirst()); if (nodes.length > 0) { diff --git a/src/lib/components/WidgetContainer.svelte b/src/lib/components/WidgetContainer.svelte index da6001d..07e9eac 100644 --- a/src/lib/components/WidgetContainer.svelte +++ b/src/lib/components/WidgetContainer.svelte @@ -84,6 +84,7 @@ class:edit={edit} class:hovered class:selected + class:patch-affected={$selectionState.currentPatchHoveredNodes.has(widget.node.id)} class:is-executing={$queueState.runningNodeID && $queueState.runningNodeID == widget.node.id} class:hidden={hidden} > @@ -112,6 +113,10 @@ &.selected { background: var(--comfy-widget-selected-background-fill); } + + &.patch-affected { + background: var(--secondary-500); + } } .is-executing { diff --git a/src/lib/components/graph/Graph.svelte b/src/lib/components/graph/Graph.svelte index 9c2f591..f7f0303 100644 --- a/src/lib/components/graph/Graph.svelte +++ b/src/lib/components/graph/Graph.svelte @@ -30,9 +30,6 @@ cyInstance = null; } - let _nodes: any[]; - let _edges: any[]; - function rebuildGraph() { cytoscape.use(dagre) @@ -41,6 +38,7 @@ style: GraphStyles, wheelSensitivity: 0.1, maxZoom: 1, + minZoom: 0.5, }) cyInstance.on("add", () => { @@ -74,8 +72,7 @@ let cyInstance: cytoscape.Core = null -
-
+
diff --git a/src/lib/components/ComfyPaneView.svelte b/src/lib/components/ComfyPaneView.svelte index fcff94a..46f3ff6 100644 --- a/src/lib/components/ComfyPaneView.svelte +++ b/src/lib/components/ComfyPaneView.svelte @@ -32,7 +32,6 @@ ] function switchMode(newMode: ComfyPaneMode) { - console.warn("switch", mode, newMode) mode = newMode; } diff --git a/src/lib/components/JourneyRenderer.svelte b/src/lib/components/JourneyRenderer.svelte index 67a5dc3..f50e418 100644 --- a/src/lib/components/JourneyRenderer.svelte +++ b/src/lib/components/JourneyRenderer.svelte @@ -1,67 +1,115 @@ + + diff --git a/src/lib/components/Modal.svelte b/src/lib/components/Modal.svelte index 63f82a5..41688f1 100644 --- a/src/lib/components/Modal.svelte +++ b/src/lib/components/Modal.svelte @@ -37,8 +37,11 @@ on:close={close} on:cancel={doClose} on:click|self={close} + on:contextmenu|preventDefault|stopPropagation > -
+
diff --git a/src/lib/components/PromptDisplay.svelte b/src/lib/components/PromptDisplay.svelte index 88cc5d8..ab5d3f7 100644 --- a/src/lib/components/PromptDisplay.svelte +++ b/src/lib/components/PromptDisplay.svelte @@ -10,19 +10,22 @@ import type { Styles } from "@gradio/utils"; import { comfyFileToComfyBoxMetadata, comfyURLToComfyFile, countNewLines } from "$lib/utils"; import ReceiveOutputTargets from "./modal/ReceiveOutputTargets.svelte"; + import RestoreParamsTable from "./modal/RestoreParamsTable.svelte"; import workflowState, { type ComfyBoxWorkflow, type WorkflowReceiveOutputTargets } from "$lib/stores/workflowState"; import type { ComfyReceiveOutputNode } from "$lib/nodes/actions"; import type ComfyApp from "./ComfyApp"; import { TabItem, Tabs } from "@gradio/tabs"; import { type ComfyBoxStdPrompt } from "$lib/ComfyBoxStdPrompt"; import ComfyBoxStdPromptSerializer from "$lib/ComfyBoxStdPromptSerializer"; - import JsonView from "./JsonView.svelte"; - import type { ZodError } from "zod"; + import JsonView from "./JsonView.svelte"; + import type { ZodError } from "zod"; + import { concatRestoreParams, getWorkflowRestoreParams, type RestoreParamTargets, type RestoreParamWorkflowNodeTargets } from "$lib/restoreParameters"; const splitLength = 50; export let prompt: SerializedPromptInputsAll; export let workflow: SerializedAppState | null; + export let restoreParams: RestoreParamTargets = {} export let images: string[] = []; // list of image URLs to ComfyUI's /view? endpoint export let isMobile: boolean = false; export let expandAll: boolean = false; @@ -33,6 +36,14 @@ let stdPromptError: ZodError | null; $: { + restoreParams = {} + + // TODO other sources than serialized workflow + if (workflow != null) { + const workflowParams = getWorkflowRestoreParams(workflow.workflow) + restoreParams = concatRestoreParams(restoreParams, workflowParams); + } + const [result, orig] = new ComfyBoxStdPromptSerializer().serialize(prompt, workflow); if (result.success === true) { stdPrompt = result.data; @@ -44,7 +55,9 @@ } } - let selectedTab: "restore-parameters" | "send-outputs" | "standard-prompt" | "prompt" = "standard-prompt"; + type PromptDisplayTabID = "restore-parameters" | "send-outputs" | "standard-prompt" | "prompt" + + let selectedTab: PromptDisplayTabID = "restore-parameters" let selected_image: number | null = null; @@ -146,15 +159,16 @@ closeModal(); } + + function doRestoreParams(e: CustomEvent) { + }
- - Parameters - + {#if comfyBoxImages.length > 0} diff --git a/src/lib/components/WidgetContainer.svelte b/src/lib/components/WidgetContainer.svelte index 07e9eac..b9443ee 100644 --- a/src/lib/components/WidgetContainer.svelte +++ b/src/lib/components/WidgetContainer.svelte @@ -70,40 +70,34 @@ {#if container} - {#key $attrsChanged} - - {/key} + {:else if widget && widget.node} {@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets"} {@const hidden = isHidden(widget)} {@const hovered = $uiState.uiUnlocked && $selectionState.currentHovered.has(widget.id)} {@const selected = $uiState.uiUnlocked && $selectionState.currentSelection.includes(widget.id)} - {#key $attrsChanged} - {#key $propsChanged} -
- -
- {#if hidden && edit} -
- {/if} - {#if showHandles || hovered} -
- {/if} - {/key} - {/key} +
+ +
+ {#if hidden && edit} +
+ {/if} + {#if showHandles || hovered} +
+ {/if} {/if} diff --git a/src/lib/nodes/ComfyGraphNode.ts b/src/lib/nodes/ComfyGraphNode.ts index e03204a..e3698e8 100644 --- a/src/lib/nodes/ComfyGraphNode.ts +++ b/src/lib/nodes/ComfyGraphNode.ts @@ -118,7 +118,7 @@ export default class ComfyGraphNode extends LGraphNode { } get dragItem(): WidgetLayout | null { - return layoutStates.getDragItemByNode(this); + return layoutStates.getDragItemByNode(this) as WidgetLayout; } get workflow(): ComfyBoxWorkflow | null { diff --git a/src/lib/restoreParameters.ts b/src/lib/restoreParameters.ts index 2274945..c350077 100644 --- a/src/lib/restoreParameters.ts +++ b/src/lib/restoreParameters.ts @@ -185,24 +185,23 @@ export function getWorkflowRestoreParamsFromWorkflow(workflow: ComfyBoxWorkflow, return result } -export function getWorkflowRestoreParams(workflow: ComfyBoxWorkflow, prompt: SerializedLGraph): RestoreParamWorkflowNodeTargets { +export function getWorkflowRestoreParams(serGraph: SerializedLGraph, noExclude: boolean = false): RestoreParamWorkflowNodeTargets { const result = {} - const graph = workflow.graph; + for (const node of serGraph.nodes) { + if (!isSerializedComfyWidgetNode(node)) + continue; - // 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.nodes) { - const foundNode = graph.getNodeByIdRecursive(serNode.id); - if (isComfyWidgetNode(foundNode) && foundNode.type === serNode.type) { - const finalValue = (serNode as SerializedComfyWidgetNode).comfyValue; - if (finalValue != null) { - const source: RestoreParamSourceWorkflowNode = { - type: "workflow", - finalValue, - } - result[foundNode.id] = source; + if (!noExclude && node.properties.excludeFromJourney) + continue; + + const finalValue = node.comfyValue + if (finalValue != null) { + const source: RestoreParamSourceWorkflowNode = { + type: "workflow", + finalValue, } + result[node.id] = source; } } @@ -250,10 +249,10 @@ export function getBackendRestoreParams(workflow: ComfyBoxWorkflow, prompt: Seri return result } -export default function restoreParameters(workflow: ComfyBoxWorkflow, prompt: SerializedPrompt): RestoreParamTargets { +export default function getRestoreParameters(workflow: ComfyBoxWorkflow, prompt: SerializedPrompt): RestoreParamTargets { const result = {} - const workflowParams = getWorkflowRestoreParams(workflow, prompt.workflow); + const workflowParams = getWorkflowRestoreParams(prompt.workflow); concatRestoreParams(result, workflowParams); const backendParams = getBackendRestoreParams(workflow, prompt); diff --git a/src/lib/stores/journeyStates.ts b/src/lib/stores/journeyStates.ts index 2719f83..0b7ec2e 100644 --- a/src/lib/stores/journeyStates.ts +++ b/src/lib/stores/journeyStates.ts @@ -185,7 +185,8 @@ function create() { if (activeNode == null) { // add root node if (get(store).root != null) { - return; + console.debug("[journeyStates] Root already exists") + return null; } journeyNode = addNode(workflowParams, null); if (showNotification) @@ -196,9 +197,10 @@ function create() { const patch = calculateWorkflowParamsPatch(activeNode, workflowParams); const patchedCount = Object.keys(patch).length; if (patchedCount === 0) { + console.debug("[journeyStates] Patch had no diff") if (showNotification) notify("No changes were made to active parameters yet.", { type: "warning" }) - return; + return null; } journeyNode = addNode(patch, activeNode); if (showNotification) @@ -209,6 +211,7 @@ function create() { selectNode(journeyNode); } + console.debug("[journeyStates] added node", journeyNode) return journeyNode; } @@ -268,6 +271,11 @@ function create() { return; // TODO + store.update(s => { + s.version += 1; + s.activeNodeID = journeyNode.id; + return s; + }) } return { diff --git a/src/lib/stores/uiQueueState.ts b/src/lib/stores/uiQueueState.ts index a361c22..a295478 100644 --- a/src/lib/stores/uiQueueState.ts +++ b/src/lib/stores/uiQueueState.ts @@ -89,6 +89,13 @@ function convertEntry(entry: QueueEntry, status: QueueUIEntryStatus): QueueUIEnt } } +export function getQueueEntryImages(queueEntry: QueueEntry): string[] { + return Object.values(queueEntry.outputs) + .filter(o => o.images) + .flatMap(o => o.images) + .map(convertComfyOutputToComfyURL); +} + function convertPendingEntry(entry: QueueEntry, status: QueueUIEntryStatus): QueueUIEntry { const result = convertEntry(entry, status); @@ -97,10 +104,7 @@ function convertPendingEntry(entry: QueueEntry, status: QueueUIEntryStatus): Que result.images = thumbnails.map(convertComfyOutputToComfyURL); } - const outputs = Object.values(entry.outputs) - .filter(o => o.images) - .flatMap(o => o.images) - .map(convertComfyOutputToComfyURL); + const outputs = getQueueEntryImages(entry); if (outputs) { result.images = result.images.concat(outputs) } @@ -111,11 +115,7 @@ function convertPendingEntry(entry: QueueEntry, status: QueueUIEntryStatus): Que function convertCompletedEntry(entry: CompletedQueueEntry): QueueUIEntry { const result = convertEntry(entry.entry, entry.status); - const images = Object.values(entry.entry.outputs) - .filter(o => o.images) - .flatMap(o => o.images) - .map(convertComfyOutputToComfyURL); - result.images = images + result.images = getQueueEntryImages(entry.entry) if (entry.message) result.submessage = entry.message diff --git a/src/lib/stores/uiState.ts b/src/lib/stores/uiState.ts index 5ee9a00..398616e 100644 --- a/src/lib/stores/uiState.ts +++ b/src/lib/stores/uiState.ts @@ -16,7 +16,7 @@ export type UIState = { activeError: PromptID | null - autoPushJourney: boolean + saveHistory: boolean } type UIStateOps = { @@ -38,7 +38,7 @@ const store: Writable = writable( activeError: null, - autoPushJourney: true + saveHistory: true }) function reconnecting() { From 9054d257afd1299c1b8c60227d2dbb0b99ee296e Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Sat, 3 Jun 2023 17:25:24 -0500 Subject: [PATCH 10/13] Linear history mode --- src/lib/components/ComfyJourneyView.svelte | 11 +- src/lib/components/JourneyRenderer.svelte | 143 +++++++++++++++------ src/lib/components/PromptDisplay.svelte | 4 +- src/lib/components/graph/GraphStyles.ts | 5 +- src/lib/restoreParameters.ts | 72 ++++++++++- src/lib/stores/journeyStates.ts | 51 +++++++- 6 files changed, 228 insertions(+), 58 deletions(-) diff --git a/src/lib/components/ComfyJourneyView.svelte b/src/lib/components/ComfyJourneyView.svelte index bbc227a..e4492eb 100644 --- a/src/lib/components/ComfyJourneyView.svelte +++ b/src/lib/components/ComfyJourneyView.svelte @@ -12,10 +12,10 @@ import type { ComfyBoxWorkflow } from '$lib/stores/workflowState'; import workflowState from '$lib/stores/workflowState'; import uiState from '$lib/stores/uiState'; - import { calculateWorkflowParamsPatch, resolvePatch, type JourneyPatchNode, type WritableJourneyStateStore, diffParams, JourneyNode } from '$lib/stores/journeyStates'; + import { resolvePatch, type JourneyPatchNode, type WritableJourneyStateStore, diffParams, type JourneyNode } from '$lib/stores/journeyStates'; import JourneyRenderer, { type JourneyNodeEvent } from './JourneyRenderer.svelte'; import { Trash, ClockHistory, Diagram3 } from "svelte-bootstrap-icons"; - import { getWorkflowRestoreParams, getWorkflowRestoreParamsFromWorkflow } from '$lib/restoreParameters'; + import { getWorkflowRestoreParamsFromWorkflow } from '$lib/restoreParameters'; import notify from '$lib/notify'; import selectionState from '$lib/stores/selectionState'; import { Checkbox } from '@gradio/form'; @@ -41,11 +41,6 @@ $: workflow = $workflowState.activeWorkflow $: { journey = workflow?.journey - activeNode = null; - updateActiveNode(); - } - - function updateActiveNode() { activeNode = journey?.getActiveNode() } @@ -151,7 +146,7 @@
{#key $journey.version} -
@@ -57,7 +67,7 @@ {:else if Object.keys(uiRestoreParams).length === 0}

No parameters to restore found in this workflow.

-

(TODO: Only parameters compatible with the currently active workflow can be restored right now)

+

(Either prompt is unchanged from active workflow, or the workflow the parameters were saved from was different)

{:else} @@ -69,11 +79,22 @@ {#each uiRestoreParams as { node, widget, sources }} - {widget.attrs.title || node.title} +
➤ {widget.attrs.title || node.title}
{#each sources as source} + {@const value = String(source.finalValue)}
-
➤ {source.type}
+ + {capitalize(source.type)} +
+ {#if isMultiline(value, 20)} + {@const lines = Math.max(countNewLines(value), value.length / 20)} + + {:else} + + {/if} +
+
{/each} @@ -95,20 +116,25 @@ } } + .target-name { + padding-bottom: 0.5rem; + } + .target { - display: flex; - flex-direction: row; - justify-content: center; - text-align: left; .target-name-and-desc { - margin: auto auto auto 0; - left: 0px; + :global(.block) { + background: var(--panel-background-fill); + } .target-desc { opacity: 65%; font-size: 11pt; } } + + pre { + @include json-view; + } } diff --git a/src/lib/restoreParameters.ts b/src/lib/restoreParameters.ts index a29be11..9a95ab4 100644 --- a/src/lib/restoreParameters.ts +++ b/src/lib/restoreParameters.ts @@ -1,4 +1,4 @@ -import type { INodeInputSlot, NodeID, SerializedLGraph } from "@litegraph-ts/core"; +import type { INodeInputSlot, NodeID, SerializedLGraph, SerializedLGraphNode } from "@litegraph-ts/core"; import type { SerializedPrompt } from "./components/ComfyApp"; import type { ComfyWidgetNode } from "./nodes/widgets"; import type { SerializedComfyWidgetNode } from "./nodes/widgets/ComfyWidgetNode"; @@ -242,15 +242,29 @@ export function getWorkflowRestoreParams(serGraph: SerializedLGraph, workflow?: return result } +function* iterateSerializedNodesRecursive(serGraph: SerializedLGraph): Iterable { + for (const serNode of serGraph.nodes) { + yield serNode; + + if (serNode.type === "graph/subgraph") { + for (const childNode of iterateSerializedNodesRecursive((serNode as any).subgraph)) { + yield childNode; + } + } + } +} + export function getWorkflowRestoreParamsUsingLayout(serGraph: SerializedLGraph, layout?: SerializedLayoutState, noExclude: boolean = false): RestoreParamWorkflowNodeTargets { const result = {} - for (const serNode of serGraph.nodes) { - if (!isSerializedComfyWidgetNode(serNode)) + for (const serNode of iterateSerializedNodesRecursive(serGraph)) { + if (!isSerializedComfyWidgetNode(serNode)) { continue; + } - if (!noExclude && serNode.properties.excludeFromJourney) + if (!noExclude && serNode.properties.excludeFromJourney) { continue; + } let name = null; const serWidget = Array.from(Object.values(layout?.allItems || {})).find(di => di.dragItem.type === "widget" && di.dragItem.nodeId === serNode.id) diff --git a/src/lib/stores/journeyStates.ts b/src/lib/stores/journeyStates.ts index 31fdddf..ae27bc4 100644 --- a/src/lib/stores/journeyStates.ts +++ b/src/lib/stores/journeyStates.ts @@ -20,7 +20,7 @@ export interface JourneyNode { id: JourneyNodeID, type: JourneyNodeType, children: JourneyPatchNode[], - promptID?: PromptID, + promptIDs: Set, images?: string[] } @@ -101,6 +101,7 @@ export function calculateWorkflowParamsPatch(parent: JourneyNode, newParams: Res export type JourneyState = { root: JourneyRootNode | null, nodesByID: Record, + nodesByPromptID: Record, activeNodeID: JourneyNodeID | null, /* @@ -117,6 +118,7 @@ type JourneyStateOps = { iterateBreadthFirst: (id?: JourneyNodeID | null) => Iterable, iterateLinearPath: (id: JourneyNodeID) => Iterable, pushPatchOntoActive: (workflow: ComfyBoxWorkflow, activeNode?: JourneyNode, showNotification?: boolean) => JourneyNode | null + afterQueued: (journeyNode: JourneyNode, promptID: PromptID) => void, onExecuted: (promptID: PromptID, nodeID: ComfyNodeID, output: SerializedPromptOutput, queueEntry: QueueEntry) => void } @@ -127,6 +129,7 @@ function create() { { root: null, nodesByID: {}, + nodesByPromptID: {}, activeNodeID: null, version: 0 }) @@ -135,6 +138,7 @@ function create() { store.set({ root: null, nodesByID: {}, + nodesByPromptID: {}, activeNodeID: null, version: 0 }) @@ -173,6 +177,7 @@ function create() { id: uuidv4(), type: "root", children: [], + promptIDs: new Set(), base: { ...params } } s.root = _node @@ -183,6 +188,7 @@ function create() { type: "patch", parent: parentNode, children: [], + promptIDs: new Set(), patch: params, } parentNode.children.push(_node); @@ -311,8 +317,16 @@ function create() { return path; } + function afterQueued(journeyNode: JourneyNode, promptID: PromptID) { + journeyNode.promptIDs.add(promptID); + store.update(s => { + s.nodesByPromptID[promptID] = journeyNode; + return s; + }) + } + function onExecuted(promptID: PromptID, nodeID: ComfyNodeID, output: SerializedPromptOutput, queueEntry: QueueEntry) { - const journeyNode = Array.from(iterateBreadthFirst()).find(j => j.promptID === promptID); + const journeyNode = get(store).nodesByPromptID[promptID]; if (journeyNode == null) return; @@ -333,7 +347,8 @@ function create() { selectNode, iterateBreadthFirst, iterateLinearPath, - onExecuted + afterQueued, + onExecuted, } } diff --git a/src/lib/stores/queueState.ts b/src/lib/stores/queueState.ts index 9e45774..f93982c 100644 --- a/src/lib/stores/queueState.ts +++ b/src/lib/stores/queueState.ts @@ -36,7 +36,7 @@ type QueueStateOps = { export type QueueEntry = { /*** Data preserved on page refresh ***/ - /** Priority of the prompt. -1 means to queue at the front. */ + /** Priority of the prompt. Lower/negative numbers get higher priority. */ number: number, queuedAt?: Date, finishedAt?: Date, diff --git a/src/lib/stores/uiQueueState.ts b/src/lib/stores/uiQueueState.ts index a295478..f712db0 100644 --- a/src/lib/stores/uiQueueState.ts +++ b/src/lib/stores/uiQueueState.ts @@ -132,6 +132,10 @@ function updateFromQueue(queuePending: QueueEntry[], queueRunning: QueueEntry[]) // newest entries appear at the top s.queuedEntries = queuePending.map((e) => convertPendingEntry(e, "pending")).reverse(); s.runningEntries = queueRunning.map((e) => convertPendingEntry(e, "running")).reverse(); + + s.queuedEntries.sort((a, b) => a.entry.number - b.entry.number) + s.runningEntries.sort((a, b) => a.entry.number - b.entry.number) + s.queueUIEntries = s.queuedEntries.concat(s.runningEntries); console.warn("[ComfyQueue] BUILDQUEUE", s.queuedEntries.length, s.runningEntries.length) return s; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 75808b0..1a4c924 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -745,3 +745,7 @@ const MOBILE_USER_AGENTS = ["iPhone", "iPad", "Android", "BlackBerry", "WebOs"]. export function isMobileBrowser(userAgent: string): boolean { return MOBILE_USER_AGENTS.some(a => userAgent.match(a)) } + +export function isMultiline(input: any, splitLength: number = 50): boolean { + return typeof input === "string" && (input.length > splitLength || countNewLines(input) > 1); +} From 7e4af6e22f8b36eab20c97f20d4afe806a804812 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Sat, 3 Jun 2023 21:56:28 -0500 Subject: [PATCH 12/13] Center on node button --- src/lib/components/ComfyJourneyView.svelte | 34 +++++++++++++++++----- src/lib/components/JourneyRenderer.svelte | 14 ++++----- src/lib/components/graph/Graph.svelte | 6 ++-- src/lib/restoreParameters.ts | 3 ++ src/lib/stores/journeyStates.ts | 1 + 5 files changed, 38 insertions(+), 20 deletions(-) diff --git a/src/lib/components/ComfyJourneyView.svelte b/src/lib/components/ComfyJourneyView.svelte index 8004451..7486293 100644 --- a/src/lib/components/ComfyJourneyView.svelte +++ b/src/lib/components/ComfyJourneyView.svelte @@ -14,7 +14,7 @@ import uiState from '$lib/stores/uiState'; import { resolvePatch, type JourneyPatchNode, type WritableJourneyStateStore, diffParams, type JourneyNode } from '$lib/stores/journeyStates'; import JourneyRenderer, { type JourneyNodeEvent } from './JourneyRenderer.svelte'; - import { Trash, ClockHistory, Diagram3 } from "svelte-bootstrap-icons"; + import { Trash, ClockHistory, Diagram3, GeoAlt } from "svelte-bootstrap-icons"; import { getWorkflowRestoreParamsFromWorkflow } from '$lib/restoreParameters'; import notify from '$lib/notify'; import selectionState from '$lib/stores/selectionState'; @@ -32,6 +32,7 @@ let journey: WritableJourneyStateStore | null = null; let activeNode: JourneyNode | null = null; let mode: JourneyMode = "linear"; + let cyto: cytoscape.Core | null = null; const MODES: [JourneyMode, typeof SvelteComponent][] = [ ["linear", ClockHistory], @@ -62,6 +63,21 @@ notify("History cleared.", { type: "info" }) } + function doCenter() { + if (cyto == null) + return; + + const activeNode = journey.getActiveNode(); + if (activeNode == null) + return; + + const node = cyto.$(`#${activeNode.id}`); + if (node.isNode()) { + cyto.zoom(1.25); + cyto.center(node) + } + } + function onSelectNode(e: CustomEvent) { const { node } = e.detail; @@ -141,7 +157,13 @@
+
{#key $journey.version} {#if workflow && journey} - diff --git a/src/lib/components/graph/Graph.svelte b/src/lib/components/graph/Graph.svelte index 9726679..4ecdc5e 100644 --- a/src/lib/components/graph/Graph.svelte +++ b/src/lib/components/graph/Graph.svelte @@ -18,6 +18,9 @@ export let style: string = "" + let refElement = null + export let cyInstance: cytoscape.Core | null = null + const dispatch = createEventDispatcher<{ rebuilt: { cyto: cytoscape.Core }; }>(); @@ -75,9 +78,6 @@ dispatch("rebuilt", { cyto: cyInstance }) } - - let refElement = null - let cyInstance: cytoscape.Core = null
diff --git a/src/lib/restoreParameters.ts b/src/lib/restoreParameters.ts index 9a95ab4..6d23fec 100644 --- a/src/lib/restoreParameters.ts +++ b/src/lib/restoreParameters.ts @@ -39,6 +39,8 @@ export interface RestoreParamSource { */ export interface RestoreParamSourceWorkflowNode extends RestoreParamSource<"workflow"> { type: "workflow", + + prevValue?: any } export type RestoreParamWorkflowNodeTargets = Record @@ -233,6 +235,7 @@ export function getWorkflowRestoreParams(serGraph: SerializedLGraph, workflow?: type: "workflow", nodeType: node.type, name, + prevValue: finalValue, finalValue, } result[node.id] = source; diff --git a/src/lib/stores/journeyStates.ts b/src/lib/stores/journeyStates.ts index ae27bc4..be7e446 100644 --- a/src/lib/stores/journeyStates.ts +++ b/src/lib/stores/journeyStates.ts @@ -81,6 +81,7 @@ export function diffParams(base: RestoreParamWorkflowNodeTargets, updated: Resto for (const [k, v] of Object.entries(updated)) { if (!(k in base) || !deepEqual(base[k].finalValue, v.finalValue, { strict: true })) { result[k] = v + v.prevValue = base[k].finalValue } } From 2dcf5de0747359ef9cd908b6279e24e7081ddab6 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Mon, 5 Jun 2023 13:05:46 -0500 Subject: [PATCH 13/13] Restore parameters from modal --- src/lib/components/PromptDisplay.svelte | 24 +++++++++++++++++++++++- src/lib/stores/uiQueueState.ts | 2 +- src/scss/global.scss | 4 ++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/lib/components/PromptDisplay.svelte b/src/lib/components/PromptDisplay.svelte index 8d9ef13..004b4bd 100644 --- a/src/lib/components/PromptDisplay.svelte +++ b/src/lib/components/PromptDisplay.svelte @@ -20,6 +20,7 @@ import JsonView from "./JsonView.svelte"; import type { ZodError } from "zod"; import { concatRestoreParams, getWorkflowRestoreParams, getWorkflowRestoreParamsUsingLayout, type RestoreParamTargets, type RestoreParamWorkflowNodeTargets } from "$lib/restoreParameters"; + import notify from "$lib/notify"; const splitLength = 50; @@ -38,9 +39,12 @@ $: { restoreParams = {} + // TODO exclude from both history and journey patch + const noExclude = true; + // TODO other sources than serialized workflow if (workflow != null) { - const workflowParams = getWorkflowRestoreParamsUsingLayout(workflow.workflow, workflow.layout) + const workflowParams = getWorkflowRestoreParamsUsingLayout(workflow.workflow, workflow.layout, noExclude) console.error("GETPARMS", workflowParams) restoreParams = concatRestoreParams(restoreParams, workflowParams); } @@ -158,6 +162,24 @@ } function doRestoreParams(e: CustomEvent) { + const activeWorkflow = workflowState.getActiveWorkflow(); + if (activeWorkflow == null) { + notify("No active workflow!", { type: "error" }) + } + + // TODO other param sources + const patch: RestoreParamWorkflowNodeTargets = {}; + + for (const [nodeID, sources] of Object.entries(restoreParams)) { + for (const source of sources) { + if (source.type === "workflow") { + patch[nodeID] = source; + } + } + } + + activeWorkflow.applyParamsPatch(patch); + closeModal(); } diff --git a/src/lib/stores/uiQueueState.ts b/src/lib/stores/uiQueueState.ts index f712db0..5517703 100644 --- a/src/lib/stores/uiQueueState.ts +++ b/src/lib/stores/uiQueueState.ts @@ -62,7 +62,7 @@ function convertEntry(entry: QueueEntry, status: QueueUIEntryStatus): QueueUIEnt const subgraphs: string[] | null = entry.extraData?.extra_pnginfo?.comfyBoxPrompt?.subgraphs; - let message = "Prompt"; + let message = `#${entry.number}: Prompt`; if (entry.extraData?.workflowTitle != null) { message = `${entry.extraData.workflowTitle}` } diff --git a/src/scss/global.scss b/src/scss/global.scss index 8e738b4..5df21c6 100644 --- a/src/scss/global.scss +++ b/src/scss/global.scss @@ -13,6 +13,10 @@ body { height: 100%; margin: 0px; font-family: Arial; + display: block; + position: absolute; + top: 0; + left: 0; } :root {