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 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);
}
</script>
<div class="journey-view">
@@ -25,7 +41,7 @@
<div class="bottom">
<button class="mode-button ternary"
title={"Add new"}
on:click={() => {}}>
on:click={doAdd}>
<Plus width="100%" height="100%" />
</button>
</div>

View File

@@ -1,38 +1,115 @@
<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 { get } from 'svelte/store';
import Graph from './graph/Graph.svelte'
import type { NodeDataDefinition, EdgeDataDefinition } from 'cytoscape';
import { NodeDataDefinition } from 'cytoscape';
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[] = [
{ 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' }
]
let lastVersion = -1;
let nodes = []
let edges = []
$: if ($journey.version !== lastVersion){
[nodes, edges] = buildGraph(journey)
lastVersion = $journey.version
}
function buildGraph(journey: WritableJourneyStateStore | null): [NodeDataDefinition[], EdgeDataDefinition[]] {
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>
{#if workflow && journey}
<Graph {nodes} {edges} style="background: var(--neutral-900)" />
<Graph {nodes} {edges}
style="background: var(--neutral-900)"
on:rebuilt={onRebuilt}
/>
{/if}

View File

@@ -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<NodeDataDefinition>;
export let edges: ReadonlyArray<EdgeDataDefinition>;
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

View File

@@ -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"
}
}
]

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 { 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);

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 { 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<JourneyNodeID, JourneyNode>
root: JourneyRootNode | null,
nodesByID: Record<JourneyNodeID, JourneyNode>,
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<JourneyNode>
}
export type WritableJourneyStateStore = Writable<JourneyState> & JourneyStateOps;
@@ -81,16 +97,114 @@ export type WritableJourneyStateStore = Writable<JourneyState> & JourneyStateOps
function create() {
const store: Writable<JourneyState> = 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<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 {
...store,
clear
clear,
addNode,
selectNode,
iterateBreadthFirst
}
}