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
}
}