feat: implement selection

This commit is contained in:
max_richter 2024-03-13 14:30:30 +01:00
parent 9241700ada
commit 305341fdf0
16 changed files with 521 additions and 202 deletions

View File

@ -28,6 +28,7 @@
"@sveltejs/vite-plugin-svelte": "^3.0.1",
"@tauri-apps/cli": "2.0.0-beta.3",
"@tsconfig/svelte": "^5.0.2",
"@zerodevx/svelte-json-view": "^1.0.9",
"histoire": "^0.17.9",
"internal-ip": "^7.0.0",
"svelte": "^4.2.8",

View File

@ -2,12 +2,11 @@
import type { Node } from "$lib/types";
import NodeHeader from "./NodeHeader.svelte";
import NodeParameter from "./NodeParameter.svelte";
import { activeNodeId, selectedNodes } from "./graph/stores";
export let node: Node;
export let inView = true;
export let possibleSocketIds: null | Set<string> = null;
const type = node?.tmp?.type;
const parameters = Object.entries(type?.inputs || {});
@ -15,17 +14,17 @@
<div
class="node"
class:active={$activeNodeId === node.id}
class:selected={!!$selectedNodes?.has(node.id)}
class:in-view={inView}
data-node-id={node.id}
style={`--nx:${node.position.x * 10}px;
--ny: ${node.position.y * 10}px`}
bind:this={node.tmp.ref}
>
<NodeHeader {node} />
{#each parameters as [key, value], i}
<NodeParameter
{node}
{possibleSocketIds}
id={key}
index={i}
input={value}
@ -47,6 +46,18 @@
font-weight: 300;
font-size: 0.5em;
display: none;
--stroke: #777;
--stroke-width: 0.1px;
}
.node.active {
--stroke: white;
--stroke-width: 0.3px;
}
.node.selected {
--stroke: #f2be90;
--stroke-width: 0.2px;
}
.node.in-view {

View File

@ -1,41 +1,11 @@
<script lang="ts">
import type { Node } from "$lib/types";
import { createNodePath } from "$lib/helpers";
import type { Node, Socket } from "$lib/types";
import { getContext } from "svelte";
import { getGraphManager, getGraphState } from "./graph/context";
export let node: Node;
const graph = getGraphManager();
const state = getGraphState();
function createPath({ depth = 8, height = 20, y = 50 } = {}) {
let corner = 10;
let right_bump = node.tmp.type.outputs.length > 0;
return `M0,100
${
corner
? ` V${corner}
Q0,0 ${corner / 4},0
H${100 - corner / 4}
Q100,0 100,${corner}
`
: ` V0
H100
`
}
V${y - height / 2}
${
right_bump
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
: ` H100`
}
V100
Z`.replace(/\s+/g, " ");
}
const setDownSocket = getContext("setDownSocket");
const setDownSocket = getContext<(socket: Socket) => void>("setDownSocket");
function handleMouseDown(event: MouseEvent) {
event.stopPropagation();
@ -46,6 +16,35 @@
position: [node.position.x + 5, node.position.y + 0.625],
});
}
const cornerTop = 10;
const rightBump = !!node?.tmp?.type?.outputs?.length;
const aspectRatio = 0.25;
const path = createNodePath({
depth: 4.5,
height: 24,
y: 50,
cornerTop,
rightBump,
aspectRatio,
});
const pathDisabled = createNodePath({
depth: 0,
height: 15,
y: 50,
cornerTop,
rightBump,
aspectRatio,
});
const pathHover = createNodePath({
depth: 6,
height: 30,
y: 50,
cornerTop,
rightBump,
aspectRatio,
});
</script>
<div class="wrapper" data-node-id={node.id}>
@ -66,8 +65,8 @@
height="100"
preserveAspectRatio="none"
style={`
--path: path("${createPath({ depth: 5, height: 27, y: 50 })}");
--hover-path: path("${createPath({ depth: 6, height: 33, y: 50 })}");
--path: path("${path}");
--hover-path: path("${pathHover}");
`}
>
<path
@ -116,8 +115,8 @@
stroke-width: 0.2px;
transition: 0.2s;
fill: #131313;
stroke: #777;
stroke-width: 0.1;
stroke: var(--stroke);
stroke-width: var(--stroke-width);
d: var(--path);
}

View File

@ -1,51 +1,17 @@
<script lang="ts">
import type { NodeInput } from "$lib/types";
import type { NodeInput, Socket } from "$lib/types";
import type { Node } from "$lib/types";
import { getContext } from "svelte";
import { createNodePath } from "$lib/helpers";
import { possibleSocketIds } from "./graph/stores";
export let node: Node;
export let input: NodeInput;
export let id: string;
export let index: number;
export let possibleSocketIds: null | Set<string> = null;
export let isLast = false;
function createPath({ depth = 8, height = 20, y = 50 } = {}) {
let corner = isLast ? 5 : 0;
let right_bump = false;
let left_bump = node.tmp?.type?.inputs?.[id].internal !== true;
return `M0,0
H100
V${y - height / 2}
${
right_bump
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
: ` H100`
}
${
corner
? ` V${100 - corner}
Q100,100 ${100 - corner / 2},100
H${corner / 2}
Q0,100 0,${100 - corner}
`
: ` V100
H0
`
}
${
left_bump
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${y - height / 2}`
: ` H0`
}
Z`.replace(/\s+/g, " ");
}
const setDownSocket = getContext("setDownSocket");
const setDownSocket = getContext<(socket: Socket) => void>("setDownSocket");
function handleMouseDown(ev: MouseEvent) {
ev.preventDefault();
@ -56,12 +22,41 @@
position: [node.position.x, node.position.y + 2.5 + index * 2.5],
});
}
const leftBump = node.tmp?.type?.inputs?.[id].internal !== true;
const cornerBottom = isLast ? 5 : 0;
const aspectRatio = 0.5;
const path = createNodePath({
depth: 4,
height: 12,
y: 50,
cornerBottom,
leftBump,
aspectRatio,
});
const pathDisabled = createNodePath({
depth: 0,
height: 15,
y: 50,
cornerBottom,
leftBump,
aspectRatio,
});
const pathHover = createNodePath({
depth: 8,
height: 24,
y: 50,
cornerBottom,
leftBump,
aspectRatio,
});
</script>
<div
class="wrapper"
class:disabled={possibleSocketIds &&
!possibleSocketIds.has(`${node.id}-${id}`)}
class:disabled={$possibleSocketIds &&
!$possibleSocketIds.has(`${node.id}-${id}`)}
>
<div class="content">
<label>{id}</label>
@ -91,9 +86,9 @@
height="100"
preserveAspectRatio="none"
style={`
--path: path("${createPath({ depth: 5, height: 15, y: 50 })}");
--hover-path-disabled: path("${createPath({ depth: 0, height: 15, y: 50 })}");
--hover-path: path("${createPath({ depth: 8, height: 24, y: 50 })}");
--path: path("${path}");
--hover-path: path("${pathHover}");
--hover-path-disabled: path("${pathDisabled}");
`}
>
<path vector-effect="non-scaling-stroke"></path>
@ -111,6 +106,8 @@
.target {
position: absolute;
border-radius: 50%;
/* background: red; */
/* opacity: 0.1; */
}
.small.target {
@ -126,6 +123,11 @@
top: 5px;
left: -7.5px;
cursor: unset;
pointer-events: none;
}
:global(.hovering-sockets) .large.target {
pointer-events: all;
}
.content {
@ -169,19 +171,24 @@
}
svg path {
stroke-width: 0.2px;
transition: 0.2s;
fill: #060606;
stroke: #777;
stroke-width: 0.1;
stroke: var(--stroke);
stroke-width: var(--stroke-width);
d: var(--path);
}
:global(.hovering-sockets) .large:hover ~ svg path {
d: var(--hover-path);
fill: #131313;
}
:global(.hovering-sockets) .small:hover ~ svg path {
fill: #161616;
}
.disabled svg path {
d: var(--hover-path-disabled) !important;
fill: #060606 !important;
}
</style>

View File

@ -22,3 +22,12 @@
<MeshLineMaterial color="red" linewidth={1} attenuate={false} />
</T.Mesh>
{/each}
<style>
.wrapper {
position: fixed;
top: 10px;
left: 10px;
background: white;
}
</style>

View File

@ -22,7 +22,7 @@
let mesh: Mesh;
function update(force = false) {
export const update = function (force = false) {
if (!force) {
const new_x = from.x + to.x;
const new_y = from.y + to.y;
@ -50,7 +50,7 @@
points = curve.getPoints(samples).map((p) => new Vector3(p.x, 0, p.y));
// mesh.setGeometry(points);
// mesh.needsUpdate = true;
}
};
update();
$: if (from || to) {

View File

@ -9,8 +9,15 @@
import GraphView from "./GraphView.svelte";
import type { Node as NodeType } from "$lib/types";
import FloatingEdge from "../edges/FloatingEdge.svelte";
import * as debug from "../debug";
import type { Socket } from "$lib/types";
import {
activeNodeId,
activeSocket,
hoveredSocket,
possibleSockets,
possibleSocketIds,
selectedNodes,
} from "./stores";
export let graph: GraphManager;
setContext("graphManager", graph);
@ -27,39 +34,68 @@
let width = 100;
let height = 100;
let activeNodeId = -1;
let downSocket: null | Socket = null;
let possibleSockets: Socket[] = [];
$: possibleSocketIds = possibleSockets?.length
? new Set(possibleSockets.map((s) => `${s.node.id}-${s.index}`))
: null;
let hoveredSocket: Socket | null = null;
$: cameraBounds = [
cameraPosition[0] - width / cameraPosition[2],
cameraPosition[0] + width / cameraPosition[2],
cameraPosition[1] - height / cameraPosition[2],
cameraPosition[1] + height / cameraPosition[2],
cameraPosition[0] - width / cameraPosition[2] / 2,
cameraPosition[0] + width / cameraPosition[2] / 2,
cameraPosition[1] - height / cameraPosition[2] / 2,
cameraPosition[1] + height / cameraPosition[2] / 2,
];
export let debug = {};
$: debug = {
activeNodeId: $activeNodeId,
activeSocket: $activeSocket
? `${$activeSocket?.node.id}-${$activeSocket?.index}`
: null,
hoveredSocket: $hoveredSocket
? `${$hoveredSocket?.node.id}-${$hoveredSocket?.index}`
: null,
selectedNodes: [...($selectedNodes?.values() || [])],
};
function updateNodePosition(node: NodeType) {
node.tmp = node.tmp || {};
if (node?.tmp?.ref) {
node.tmp.ref.style.setProperty("--nx", `${node.position.x * 10}px`);
node.tmp.ref.style.setProperty("--ny", `${node.position.y * 10}px`);
}
}
const nodeHeightCache: Record<string, number> = {};
function getNodeHeight(nodeTypeId: string) {
if (nodeTypeId in nodeHeightCache) {
return nodeHeightCache[nodeTypeId];
}
const node = graph.getNodeType(nodeTypeId);
if (!node?.inputs) {
return 1.25;
}
const height = 1.25 + 2.5 * Object.keys(node.inputs).length;
nodeHeightCache[nodeTypeId] = height;
return height;
}
setContext("isNodeInView", (node: NodeType) => {
const height = getNodeHeight(node.type);
const width = 5;
return (
node.position.x > cameraBounds[0] &&
// check x-axis
node.position.x > cameraBounds[0] - width &&
node.position.x < cameraBounds[1] &&
node.position.y > cameraBounds[2] &&
// check y-axis
node.position.y > cameraBounds[2] - height &&
node.position.y < cameraBounds[3]
);
});
setContext("setDownSocket", (socket: Socket) => {
downSocket = socket;
$activeSocket = socket;
let { node, index, position } = socket;
// remove existing edge
if (typeof index === "string") {
const edges = graph.getEdgesToNode(node);
console.log({ edges });
for (const edge of edges) {
if (edge[3] === index) {
node = edge[0];
@ -72,14 +108,14 @@
}
mouseDown = position;
downSocket = {
$activeSocket = {
node,
index,
position,
};
possibleSockets = graph
.getPossibleSockets(downSocket)
$possibleSockets = graph
.getPossibleSockets($activeSocket)
.map(([node, index]) => {
return {
node,
@ -87,6 +123,9 @@
position: getSocketPosition({ node, index }),
};
});
$possibleSocketIds = new Set(
$possibleSockets.map((s) => `${s.node.id}-${s.index}`),
);
});
function getSnapLevel() {
@ -136,10 +175,11 @@
if (!mouseDown) return;
if (possibleSockets?.length) {
// we are creating a new edge here
if ($possibleSockets?.length) {
let smallestDist = 1000;
let _socket;
for (const socket of possibleSockets) {
for (const socket of $possibleSockets) {
const dist = Math.sqrt(
(socket.position[0] - mousePosition[0]) ** 2 +
(socket.position[1] - mousePosition[1]) ** 2,
@ -152,119 +192,236 @@
if (_socket && smallestDist < 0.3) {
mousePosition = _socket.position;
hoveredSocket = _socket;
$hoveredSocket = _socket;
} else {
hoveredSocket = null;
$hoveredSocket = null;
}
}
if (activeNodeId === -1) return;
if ($activeNodeId === -1) return;
const node = graph.getNode(activeNodeId);
if (!node) return;
const node = graph.getNode($activeNodeId);
if (!node || event.buttons !== 1) return;
node.tmp = node.tmp || {};
node.tmp.isMoving = true;
let newX =
(node?.tmp?.downX || 0) +
(event.clientX - mouseDown[0]) / cameraPosition[2];
let newY =
(node?.tmp?.downY || 0) +
(event.clientY - mouseDown[1]) / cameraPosition[2];
const oldX = node.tmp.downX || 0;
const oldY = node.tmp.downY || 0;
let newX = oldX + (event.clientX - mouseDown[0]) / cameraPosition[2];
let newY = oldY + (event.clientY - mouseDown[1]) / cameraPosition[2];
if (event.ctrlKey) {
const snapLevel = getSnapLevel();
newX = snapToGrid(newX, 5 / snapLevel);
newY = snapToGrid(newY, 5 / snapLevel);
}
if (!node.tmp.isMoving) {
const dist = Math.sqrt((oldX - newX) ** 2 + (oldY - newY) ** 2);
if (dist > 0.2) {
node.tmp.isMoving = true;
}
}
const vecX = oldX - newX;
const vecY = oldY - newY;
if ($selectedNodes?.size) {
for (const nodeId of $selectedNodes) {
const n = graph.getNode(nodeId);
if (!n) continue;
n.position.x = (n?.tmp?.downX || 0) - vecX;
n.position.y = (n?.tmp?.downY || 0) - vecY;
updateNodePosition(n);
}
}
node.position.x = newX;
node.position.y = newY;
node.position = node.position;
nodes.set($nodes);
edges.set($edges);
updateNodePosition(node);
$edges = $edges;
}
function handleMouseDown(event: MouseEvent) {
if (mouseDown) return;
for (const node of event.composedPath()) {
let _activeNodeId = (node as unknown as HTMLElement)?.getAttribute?.(
"data-node-id",
)!;
if (_activeNodeId) {
activeNodeId = parseInt(_activeNodeId, 10);
break;
}
}
if (activeNodeId < 0) return;
mouseDown = [event.clientX, event.clientY];
const node = graph.getNode(activeNodeId);
if (event.target instanceof HTMLElement && event.buttons === 1) {
const nodeElement = event.target.closest(".node");
const _activeNodeId = nodeElement?.getAttribute?.("data-node-id");
if (_activeNodeId) {
const nodeId = parseInt(_activeNodeId, 10);
if ($activeNodeId !== -1) {
// if the selected node is the same as the clicked node
if ($activeNodeId === nodeId) {
//$activeNodeId = -1;
// if the clicked node is different from the selected node and secondary
} else if (event.ctrlKey) {
$selectedNodes = $selectedNodes || new Set();
$selectedNodes.add($activeNodeId);
$selectedNodes.delete(nodeId);
$activeNodeId = nodeId;
// select the node
} else if (event.shiftKey) {
const activeNode = graph.getNode($activeNodeId);
const newNode = graph.getNode(nodeId);
if (activeNode && newNode) {
const edge = graph.getNodesBetween(activeNode, newNode);
if (edge) {
$selectedNodes = new Set(edge.map((n) => n.id));
}
$activeNodeId = nodeId;
}
} else if (!$selectedNodes?.has(nodeId)) {
$activeNodeId = nodeId;
}
} else {
$activeNodeId = nodeId;
}
} else {
$activeNodeId = -1;
$selectedNodes?.clear();
$selectedNodes = $selectedNodes;
}
}
const node = graph.getNode($activeNodeId);
if (!node) return;
node.tmp = node.tmp || {};
node.tmp.downX = node.position.x;
node.tmp.downY = node.position.y;
if ($selectedNodes) {
for (const nodeId of $selectedNodes) {
const n = graph.getNode(nodeId);
if (!n) continue;
n.tmp = n.tmp || {};
n.tmp.downX = n.position.x;
n.tmp.downY = n.position.y;
}
}
}
function handleKeyDown(event: KeyboardEvent) {
if (event.key === "Delete") {
if ($activeNodeId !== -1) {
const node = graph.getNode($activeNodeId);
if (node) {
graph.removeNode(node);
$activeNodeId = -1;
}
}
if ($selectedNodes) {
for (const nodeId of $selectedNodes) {
const node = graph.getNode(nodeId);
if (node) {
graph.removeNode(node);
}
}
$selectedNodes.clear();
$selectedNodes = $selectedNodes;
}
}
}
function handleMouseUp(event: MouseEvent) {
if (event.button !== 0) return;
const activeNode = graph.getNode($activeNodeId);
const node = graph.getNode(activeNodeId);
if (node) {
node.tmp = node.tmp || {};
node.tmp.isMoving = false;
if (event.target instanceof HTMLElement && event.button === 0) {
const nodeElement = event.target.closest(".node");
const _activeNodeId = nodeElement?.getAttribute?.("data-node-id");
if (_activeNodeId) {
const nodeId = parseInt(_activeNodeId, 10);
if (activeNode) {
if (!activeNode?.tmp?.isMoving && !event.ctrlKey && !event.shiftKey) {
$selectedNodes?.clear();
$selectedNodes = $selectedNodes;
$activeNodeId = nodeId;
}
}
}
}
if (activeNode?.tmp?.isMoving) {
activeNode.tmp = activeNode.tmp || {};
activeNode.tmp.isMoving = false;
const snapLevel = getSnapLevel();
const fx = snapToGrid(node.position.x, 5 / snapLevel);
const fy = snapToGrid(node.position.y, 5 / snapLevel);
const fx = snapToGrid(activeNode.position.x, 5 / snapLevel);
const fy = snapToGrid(activeNode.position.y, 5 / snapLevel);
if ($selectedNodes) {
for (const nodeId of $selectedNodes) {
const node = graph.getNode(nodeId);
if (!node) continue;
node.tmp = node.tmp || {};
node.tmp.snapX = node.position.x - (activeNode.position.x - fx);
node.tmp.snapY = node.position.y - (activeNode.position.y - fy);
}
}
animate(500, (a: number) => {
node.position.x = lerp(node.position.x, fx, a);
node.position.y = lerp(node.position.y, fy, a);
nodes.set($nodes);
edges.set($edges);
if (node?.tmp?.isMoving) {
activeNode.position.x = lerp(activeNode.position.x, fx, a);
activeNode.position.y = lerp(activeNode.position.y, fy, a);
updateNodePosition(activeNode);
if ($selectedNodes) {
for (const nodeId of $selectedNodes) {
const node = graph.getNode(nodeId);
if (!node) continue;
node.position.x = lerp(node.position.x, node?.tmp?.snapX || 0, a);
node.position.y = lerp(node.position.y, node?.tmp?.snapY || 0, a);
updateNodePosition(node);
}
}
if (activeNode?.tmp?.isMoving) {
return false;
}
$edges = $edges;
});
} else if (hoveredSocket && downSocket) {
console.log({ hoveredSocket, downSocket });
} else if ($hoveredSocket && $activeSocket) {
if (
typeof hoveredSocket.index === "number" &&
typeof downSocket.index === "string"
typeof $hoveredSocket.index === "number" &&
typeof $activeSocket.index === "string"
) {
graph.createEdge(
hoveredSocket.node,
hoveredSocket.index || 0,
downSocket.node,
downSocket.index,
$hoveredSocket.node,
$hoveredSocket.index || 0,
$activeSocket.node,
$activeSocket.index,
);
} else {
} else if (
typeof $activeSocket.index == "number" &&
typeof $hoveredSocket.index === "string"
) {
graph.createEdge(
downSocket.node,
downSocket.index || 0,
hoveredSocket.node,
hoveredSocket.index,
$activeSocket.node,
$activeSocket.index || 0,
$hoveredSocket.node,
$hoveredSocket.index,
);
}
}
mouseDown = null;
downSocket = null;
possibleSockets = [];
hoveredSocket = null;
activeNodeId = -1;
$activeSocket = null;
$possibleSockets = [];
$possibleSocketIds = null;
$hoveredSocket = null;
}
</script>
<svelte:document
<svelte:window
on:mousemove={handleMouseMove}
on:mouseup={handleMouseUp}
on:mousedown={handleMouseDown}
on:keydown={handleKeyDown}
bind:innerWidth={width}
bind:innerHeight={height}
/>
<svelte:window bind:innerWidth={width} bind:innerHeight={height} />
<Debug />
<Camera bind:camera {maxZoom} {minZoom} bind:position={cameraPosition} />
@ -272,19 +429,13 @@
<Background {cameraPosition} {maxZoom} {minZoom} {width} {height} />
{#if $status === "idle"}
{#if downSocket}
{#if $activeSocket}
<FloatingEdge
from={{ x: downSocket.position[0], y: downSocket.position[1] }}
from={{ x: $activeSocket.position[0], y: $activeSocket.position[1] }}
to={{ x: mousePosition[0], y: mousePosition[1] }}
/>
{/if}
<GraphView
{nodes}
{edges}
{cameraPosition}
{possibleSocketIds}
{downSocket}
/>
<GraphView {nodes} {edges} {cameraPosition} />
{:else if $status === "loading"}
<span>Loading</span>
{:else if $status === "error"}

View File

@ -1,18 +1,16 @@
<script lang="ts">
import type { Edge as EdgeType, Node as NodeType } from "$lib/types";
import type { Edge as EdgeType, Node as NodeType, Socket } from "$lib/types";
import { HTML } from "@threlte/extras";
import Edge from "../edges/Edge.svelte";
import Node from "../Node.svelte";
import { getContext } from "svelte";
import { getContext, onMount } from "svelte";
import type { Writable } from "svelte/store";
import { activeSocket } from "./stores";
export let nodes: Writable<Map<number, NodeType>>;
export let edges: Writable<EdgeType[]>;
export let cameraPosition = [0, 1, 0];
export let downSocket: null | { node: NodeType; index: number | string } =
null;
export let possibleSocketIds: null | Set<string> = null;
const isNodeInView = getContext<(n: NodeType) => boolean>("isNodeInView");
@ -25,9 +23,18 @@
edge[2].position.y + 2.5 + index * 2.5,
];
}
onMount(() => {
for (const node of $nodes.values()) {
if (node?.tmp?.ref) {
node.tmp.ref.style.setProperty("--nx", `${node.position.x * 10}px`);
node.tmp.ref.style.setProperty("--ny", `${node.position.y * 10}px`);
}
}
});
</script>
{#each $edges as edge}
{#each $edges as edge (edge[0].id + edge[2].id + edge[3])}
{@const pos = getEdgePosition(edge)}
{@const [x1, y1, x2, y2] = pos}
<Edge
@ -48,15 +55,11 @@
tabindex="0"
class="wrapper"
class:zoom-small={cameraPosition[2] < 10}
class:hovering-sockets={downSocket}
class:hovering-sockets={activeSocket}
style={`--cz: ${cameraPosition[2]}`}
>
{#each $nodes.values() as node}
<Node
{node}
inView={cameraPosition && isNodeInView(node)}
{possibleSocketIds}
/>
{#each $nodes.values() as node (node.id)}
<Node {node} inView={cameraPosition && isNodeInView(node)} />
{/each}
</div>
</HTML>

View File

@ -1,11 +1,6 @@
import type { GraphManager } from "$lib/graph-manager";
import { getContext } from "svelte";
import type { GraphView } from "./view";
export function getGraphManager(): GraphManager {
return getContext("graphManager");
}
export function getGraphState(): GraphView {
return getContext("graphState");
}

View File

@ -0,0 +1,10 @@
import type { Node, Socket } from "$lib/types";
import { writable, type Writable } from "svelte/store";
export const activeNodeId: Writable<number> = writable(-1);
export const selectedNodes: Writable<Set<number> | null> = writable(null);
export const activeSocket: Writable<Socket | null> = writable(null);
export const hoveredSocket: Writable<Socket | null> = writable(null);
export const possibleSockets: Writable<Socket[]> = writable([]);
export const possibleSocketIds: Writable<Set<string> | null> = writable(null);

View File

@ -0,0 +1,20 @@
<script lang="ts">
export let title = "Details";
</script>
<details>
<summary>{title}</summary>
<slot />
</details>
<style>
details {
padding: 1em;
color: white;
outline: solid 0.1px white;
border-radius: 2px;
font-weight: 300;
font-size: 0.9em;
}
</style>

View File

@ -50,6 +50,19 @@ export class GraphManager {
this.edges.subscribe((edges) => {
this._edges = edges;
});
globalThis["serialize"] = () => this.serialize();
}
serialize() {
const nodes = Array.from(this._nodes.values()).map(node => ({
id: node.id,
position: node.position,
type: node.type,
props: node.props,
}));
const edges = this._edges.map(edge => [edge[0].id, edge[1], edge[2].id, edge[3]]);
return { nodes, edges };
}
async load() {
@ -87,8 +100,6 @@ export class GraphManager {
);
this.nodes.set(nodes);
console.log(this._nodes);
this.status.set("idle");
}
@ -102,6 +113,10 @@ export class GraphManager {
return this._nodes.get(id);
}
getNodeType(id: string) {
return this.nodeRegistry.getNode(id);
}
getChildrenOfNode(node: Node) {
const children = [];
const stack = node.tmp?.children?.slice(0);
@ -114,8 +129,35 @@ export class GraphManager {
return children;
}
createEdge(from: Node, fromSocket: number, to: Node, toSocket: string) {
getNodesBetween(from: Node, to: Node): Node[] | undefined {
// < - - - - from
const toParents = this.getParentsOfNode(to);
// < - - - - from - - - - to
const fromParents = this.getParentsOfNode(from);
if (toParents.includes(from)) {
return toParents.splice(toParents.indexOf(from));
} else if (fromParents.includes(to)) {
return fromParents.splice(fromParents.indexOf(to));
} else {
// these two nodes are not connected
return;
}
}
private updateNodeParents(node: Node) {
}
removeNode(node: Node) {
const edges = this._edges.filter((edge) => edge[0].id !== node.id && edge[2].id !== node.id);
this.edges.set(edges);
this.nodes.update((nodes) => {
nodes.delete(node.id);
return nodes;
});
}
createEdge(from: Node, fromSocket: number, to: Node, toSocket: string) {
const existingEdges = this.getEdgesToNode(to);
@ -150,7 +192,7 @@ export class GraphManager {
parents.push(parent);
stack.push(...parent.tmp?.parents || []);
}
return parents;
return parents.reverse();
}
getPossibleSockets({ node, index }: Socket): [Node, string | number][] {

View File

@ -21,3 +21,44 @@ export function animate(duration: number, callback: (progress: number) => void |
}
requestAnimationFrame(loop);
}
export function createNodePath({
depth = 8,
height = 20,
y = 50,
cornerTop = 0,
cornerBottom = 0,
leftBump = false,
rightBump = false,
aspectRatio = 1,
} = {}) {
return `M0,${cornerTop}
${cornerTop
? ` V${cornerTop}
Q0,0 ${cornerTop * aspectRatio},0
H${100 - cornerTop * aspectRatio}
Q100,0 100,${cornerTop}
`
: ` V0
H100
`
}
V${y - height / 2}
${rightBump
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
: ` H100`
}
${cornerBottom
? ` V${100 - cornerBottom}
Q100,100 ${100 - cornerBottom * aspectRatio},100
H${cornerBottom * aspectRatio}
Q0,100 0,${100 - cornerBottom}
`
: `${leftBump ? `V100 H0` : `V100`}`
}
${leftBump
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${y - height / 2}`
: ` H0`
}
Z`.replace(/\s+/g, " ");
}

View File

@ -11,6 +11,9 @@ export type Node = {
type?: NodeType;
downX?: number;
downY?: number;
snapX?: number;
snapY?: number;
ref?: HTMLElement;
visible?: boolean;
isMoving?: boolean;
},

View File

@ -2,14 +2,17 @@
import { invoke } from "@tauri-apps/api/core";
import { onMount } from "svelte";
import { PerfMonitor } from "@threlte/extras";
import { Canvas } from "@threlte/core";
import { GraphManager } from "$lib/graph-manager";
import Graph from "$lib/components/graph/Graph.svelte";
import Details from "$lib/elements/Details.svelte";
import { JsonView } from "@zerodevx/svelte-json-view";
const graph = GraphManager.createEmptyGraph({ width: 12, height: 12 });
graph.load();
let debug: undefined;
// onMount(async () => {
// try {
// const res = await invoke("greet", { name: "Dude" });
@ -27,15 +30,28 @@
// });
</script>
<div>
<div class="wrapper">
<Details>
<JsonView json={debug} />
</Details>
</div>
<div class="canvas-wrapper">
<Canvas shadows={false} renderMode="on-demand" autoRender={true}>
<!-- <PerfMonitor /> -->
<Graph {graph} />
<Graph {graph} bind:debug />
</Canvas>
</div>
<style>
div {
.wrapper {
position: absolute;
z-index: 100;
top: 10px;
left: 10px;
}
.canvas-wrapper {
height: 100vh;
}

View File

@ -54,6 +54,9 @@ importers:
'@tsconfig/svelte':
specifier: ^5.0.2
version: 5.0.2
'@zerodevx/svelte-json-view':
specifier: ^1.0.9
version: 1.0.9(svelte@4.2.12)
histoire:
specifier: ^0.17.9
version: 0.17.9(vite@5.1.4)
@ -1228,6 +1231,14 @@ packages:
- supports-color
dev: false
/@zerodevx/svelte-json-view@1.0.9(svelte@4.2.12):
resolution: {integrity: sha512-2KKxBfDxEo7lM/kJSy+m1PdLAp5Q9c5nB6OYVBg7oWPdCLXB9JVH1Ytxn2hkqTn77m9MobqGI1fz9FFOTPONfA==}
peerDependencies:
svelte: ^3.57.0 || ^4.0.0
dependencies:
svelte: 4.2.12
dev: true
/abab@2.0.6:
resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==}
deprecated: Use your platform's native atob() and btoa() methods instead