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