Very basic graph parameters
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user