Very basic graph parameters

This commit is contained in:
space-nuko
2023-06-02 21:01:30 -05:00
parent 59aad9183d
commit 4f237d01a5
6 changed files with 323 additions and 71 deletions

View File

@@ -10,6 +10,8 @@
import type { WritableJourneyStateStore } from '$lib/stores/journeyStates'; import type { WritableJourneyStateStore } from '$lib/stores/journeyStates';
import JourneyRenderer from './JourneyRenderer.svelte'; import JourneyRenderer from './JourneyRenderer.svelte';
import { Plus } from "svelte-bootstrap-icons"; import { Plus } from "svelte-bootstrap-icons";
import { getWorkflowRestoreParams, getWorkflowRestoreParamsFromWorkflow } from '$lib/restoreParameters';
import notify from '$lib/notify';
export let app: ComfyApp; export let app: ComfyApp;
@@ -18,6 +20,20 @@
$: workflow = $workflowState.activeWorkflow $: workflow = $workflowState.activeWorkflow
$: journey = workflow?.journey $: 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);
}
</script> </script>
<div class="journey-view"> <div class="journey-view">
@@ -25,7 +41,7 @@
<div class="bottom"> <div class="bottom">
<button class="mode-button ternary" <button class="mode-button ternary"
title={"Add new"} title={"Add new"}
on:click={() => {}}> on:click={doAdd}>
<Plus width="100%" height="100%" /> <Plus width="100%" height="100%" />
</button> </button>
</div> </div>

View File

@@ -1,38 +1,115 @@
<script lang="ts"> <script lang="ts">
import type { WritableJourneyStateStore } from '$lib/stores/journeyStates'; import type { JourneyPatchNode, WritableJourneyStateStore } from '$lib/stores/journeyStates';
import { ComfyBoxWorkflow } from '$lib/stores/workflowState'; import { ComfyBoxWorkflow } from '$lib/stores/workflowState';
import { get } from 'svelte/store';
import Graph from './graph/Graph.svelte' import Graph from './graph/Graph.svelte'
import type { NodeDataDefinition, EdgeDataDefinition } from 'cytoscape'; import type { NodeDataDefinition, EdgeDataDefinition } from 'cytoscape';
import { NodeDataDefinition } from 'cytoscape';
export let workflow: ComfyBoxWorkflow | null = null export let workflow: ComfyBoxWorkflow | null = null
export let journey: WritableJourneyStateStore | null = ull 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 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[] = [ let lastVersion = -1;
{ id: 'E1', source: 'N1', target: 'N2' },
{ id: 'E2', source: 'N2', target: 'N3' }, let nodes = []
{ id: 'E3', source: 'N3', target: 'N6' }, let edges = []
{ id: 'E4', source: 'N2', target: 'N4' }, $: if ($journey.version !== lastVersion){
{ id: 'E5', source: 'N4', target: 'N5' }, [nodes, edges] = buildGraph(journey)
{ id: 'E6', source: 'N5', target: 'N4', label: '2' }, lastVersion = $journey.version
{ id: 'E7', source: 'N5', target: 'N6' }, }
{ id: 'E8', source: 'N6', target: 'N7' },
{ id: 'E9', source: 'N7', target: 'N7', label: '3' }, function buildGraph(journey: WritableJourneyStateStore | null): [NodeDataDefinition[], EdgeDataDefinition[]] {
{ id: 'E10', source: 'N7', target: 'N8' } if (!journey) {
] return [[], []]
}
const journeyState = get(journey);
const nodes: NodeDataDefinition[] = []
const edges: EdgeDataDefinition[] = []
for (const node of journey.iterateBreadthFirst()) {
if (node.type === "root") {
nodes.push({
id: node.id,
label: "Start",
selected: node.id === journeyState.activeNodeID,
locked: true
})
continue;
}
else {
const patchNode = node as JourneyPatchNode;
nodes.push({
id: patchNode.id,
label: "N",
selected: node.id === journeyState.activeNodeID,
locked: true
})
edges.push({
id: `${patchNode.parent.id}_${patchNode.id}`,
source: patchNode.parent.id,
target: patchNode.id,
selectable: false,
locked: true
})
}
}
return [nodes, edges]
}
function onNodeSelected(e: cytoscape.InputEventObject) {
console.warn("SELECT", e)
const node = e.target;
journey.selectNode(node.id());
e.cy.center(node)
}
function onRebuilt(e: CustomEvent<{cyto: cytoscape.Core}>) {
const { cyto } = e.detail;
cyto.nodes()
.lock()
.on("select", onNodeSelected)
const nodes = Array.from(journey.iterateBreadthFirst());
if (nodes.length > 0) {
const lastNode = nodes[nodes.length - 1]
const start = cyto.$(`#${lastNode.id}`)
cyto.center(start)
}
}
</script> </script>
{#if workflow && journey} {#if workflow && journey}
<Graph {nodes} {edges} style="background: var(--neutral-900)" /> <Graph {nodes} {edges}
style="background: var(--neutral-900)"
on:rebuilt={onRebuilt}
/>
{/if} {/if}

View File

@@ -12,12 +12,17 @@
import GraphStyles from "./GraphStyles" import GraphStyles from "./GraphStyles"
import type { EdgeDataDefinition } from "cytoscape"; import type { EdgeDataDefinition } from "cytoscape";
import type { NodeDataDefinition } from "cytoscape"; import type { NodeDataDefinition } from "cytoscape";
import { createEventDispatcher } from "svelte";
export let nodes: ReadonlyArray<NodeDataDefinition>; export let nodes: ReadonlyArray<NodeDataDefinition>;
export let edges: ReadonlyArray<EdgeDataDefinition>; export let edges: ReadonlyArray<EdgeDataDefinition>;
export let style: string = "" export let style: string = ""
const dispatch = createEventDispatcher<{
rebuilt: { cyto: cytoscape.Core };
}>();
$: if (nodes != null && edges != null && refElement != null) { $: if (nodes != null && edges != null && refElement != null) {
rebuildGraph() rebuildGraph()
} }
@@ -35,6 +40,7 @@
container: refElement, container: refElement,
style: GraphStyles, style: GraphStyles,
wheelSensitivity: 0.1, wheelSensitivity: 0.1,
maxZoom: 1,
}) })
cyInstance.on("add", () => { cyInstance.on("add", () => {
@@ -60,6 +66,8 @@
data: { ...edge } data: { ...edge }
}) })
} }
dispatch("rebuilt", { cyto: cyInstance })
} }
let refElement = null let refElement = null

View File

@@ -2,59 +2,73 @@ import type { Stylesheet } from "cytoscape";
const styles: Stylesheet[] = [ const styles: Stylesheet[] = [
{ {
selector: 'node', selector: "core",
style: { style: {
'width': '50', "selection-box-color": "#ddd",
'height': '50', "selection-box-opacity": 0.65,
'font-family': 'Arial', "selection-box-border-color": "#aaa",
'font-size': '18', "selection-box-border-width": 1,
'font-weight': 'normal', "active-bg-color": "#4b5563",
'content': `data(label)`, "active-bg-opacity": 0.35,
'text-valign': 'center', "active-bg-size": 30,
'text-wrap': 'wrap', "outside-texture-bg-color": "#000",
'text-max-width': '140', "outside-texture-bg-opacity": 0.125,
'background-color': '#60a5fa',
'border-color': '#2563eb',
'border-width': '3',
'color': '#1d3660'
} }
}, },
{ {
selector: 'node:selected', selector: "node",
style: { style: {
'background-color': '#f97316', "width": "50",
color: 'white', "height": "50",
'border-color': '#ea580c', "font-family": "Arial",
'line-color': '#0e76ba', "font-size": "18",
'target-arrow-color': '#0e76ba' "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: { style: {
'curve-style': 'bezier', "background-color": "#f97316",
'color': 'darkred', "color": "white",
'text-background-color': '#ffffff', "border-color": "#ea580c",
'text-background-opacity': '1', "line-color": "#0e76ba",
'text-background-padding': '3', "target-arrow-color": "#0e76ba"
'width': '3',
'target-arrow-shape': 'triangle',
'line-color': '#1d4ed8',
'target-arrow-color': '#1d4ed8',
'font-weight': 'bold'
} }
}, },
{ {
selector: 'edge[label]', selector: "edge",
style: { 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: { style: {
'line-color': 'orange', "content": `data(label)`,
'target-arrow-color': 'orange' }
},
{
selector: "edge.label",
style: {
"line-color": "orange",
"target-arrow-color": "orange"
} }
} }
] ]

View File

@@ -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 { SerializedPrompt } from "./components/ComfyApp";
import type { ComfyWidgetNode } from "./nodes/widgets"; import type { ComfyWidgetNode } from "./nodes/widgets";
import type { SerializedComfyWidgetNode } from "./nodes/widgets/ComfyWidgetNode"; import type { SerializedComfyWidgetNode } from "./nodes/widgets/ComfyWidgetNode";
@@ -159,14 +159,37 @@ export function concatRestoreParams2(a: RestoreParamTargets, b: RestoreParamTarg
return a; 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 result = {}
const graph = workflow.graph; const graph = workflow.graph;
// Find nodes that correspond to *this* workflow exactly, since we can // Find nodes that correspond to *this* workflow exactly, since we can
// easily match up the nodes between each (their IDs will be the same) // 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); const foundNode = graph.getNodeByIdRecursive(serNode.id);
if (isComfyWidgetNode(foundNode) && foundNode.type === serNode.type) { if (isComfyWidgetNode(foundNode) && foundNode.type === serNode.type) {
const finalValue = (serNode as SerializedComfyWidgetNode).comfyValue; 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 { export default function restoreParameters(workflow: ComfyBoxWorkflow, prompt: SerializedPrompt): RestoreParamTargets {
const result = {} const result = {}
const workflowParams = getWorkflowRestoreParams(workflow, prompt); const workflowParams = getWorkflowRestoreParams(workflow, prompt.workflow);
concatRestoreParams(result, workflowParams); concatRestoreParams(result, workflowParams);
const backendParams = getBackendRestoreParams(workflow, prompt); const backendParams = getBackendRestoreParams(workflow, prompt);

View File

@@ -1,9 +1,10 @@
import { writable } from 'svelte/store'; import { get, writable } from 'svelte/store';
import type { Readable, Writable } from 'svelte/store'; import type { Readable, Writable } from 'svelte/store';
import type { DragItemID, IDragItem } from './layoutStates'; import type { DragItemID, IDragItem } from './layoutStates';
import type { LGraphNode, NodeID, UUID } from '@litegraph-ts/core'; import type { LGraphNode, NodeID, UUID } from '@litegraph-ts/core';
import type { SerializedAppState } from '$lib/components/ComfyApp'; import type { SerializedAppState } from '$lib/components/ComfyApp';
import type { RestoreParamTargets, RestoreParamWorkflowNodeTargets } from '$lib/restoreParameters'; import type { RestoreParamTargets, RestoreParamWorkflowNodeTargets } from '$lib/restoreParameters';
import { v4 as uuidv4 } from "uuid";
export type JourneyNodeType = "root" | "patch"; export type JourneyNodeType = "root" | "patch";
@@ -62,18 +63,33 @@ function diffParams(base: RestoreParamWorkflowNodeTargets, updated: RestoreParam
return result; 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 * 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 * tree-like graph. It lets you save incremental changes to your workflow and
* jump between past and present sets of parameters. * jump between past and present sets of parameters.
*/ */
export type JourneyState = { export type JourneyState = {
tree: JourneyNode, root: JourneyRootNode | null,
nodesByID: Record<JourneyNodeID, JourneyNode> nodesByID: Record<JourneyNodeID, JourneyNode>,
activeNodeID: JourneyNodeID | null,
/*
* Incremented when graph structure is updated
*/
version: number
} }
type JourneyStateOps = { type JourneyStateOps = {
clear: () => void, clear: () => void,
addNode: (params: RestoreParamWorkflowNodeTargets, parent?: JourneyNodeID) => JourneyNode,
selectNode: (id?: JourneyNodeID) => void,
iterateBreadthFirst: (id?: JourneyNodeID | null) => Iterable<JourneyNode>
} }
export type WritableJourneyStateStore = Writable<JourneyState> & JourneyStateOps; export type WritableJourneyStateStore = Writable<JourneyState> & JourneyStateOps;
@@ -81,16 +97,114 @@ export type WritableJourneyStateStore = Writable<JourneyState> & JourneyStateOps
function create() { function create() {
const store: Writable<JourneyState> = writable( const store: Writable<JourneyState> = writable(
{ {
root: null,
nodesByID: {},
activeNodeID: null,
version: 0
}) })
function clear() { function clear() {
store.set({ 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<JourneyNode> {
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 { return {
...store, ...store,
clear clear,
addNode,
selectNode,
iterateBreadthFirst
} }
} }