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