feat/drop-node-on-connection #18
@@ -1,19 +1,44 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { MeshLineGeometry, MeshLineMaterial } from '@threlte/extras';
|
import { MeshLineGeometry, MeshLineMaterial } from "@threlte/extras";
|
||||||
import { points, lines } from './store.js';
|
import { points, lines, rects } from "./store.js";
|
||||||
import { T } from '@threlte/core';
|
import { T } from "@threlte/core";
|
||||||
|
import { Color } from "three";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each $points as point}
|
{#each $points as point}
|
||||||
<T.Mesh position.x={point.x} position.y={point.y} position.z={point.z} rotation.x={-Math.PI / 2}>
|
<T.Mesh
|
||||||
|
position.x={point.x}
|
||||||
|
position.y={point.y}
|
||||||
|
position.z={point.z}
|
||||||
|
rotation.x={-Math.PI / 2}
|
||||||
|
>
|
||||||
<T.CircleGeometry args={[0.2, 32]} />
|
<T.CircleGeometry args={[0.2, 32]} />
|
||||||
<T.MeshBasicMaterial color="red" />
|
<T.MeshBasicMaterial color="red" />
|
||||||
</T.Mesh>
|
</T.Mesh>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#each $lines as line}
|
{#each $rects as rect, i}
|
||||||
<T.Mesh>
|
<T.Mesh
|
||||||
<MeshLineGeometry points={line} />
|
position.x={(rect.minX + rect.maxX) / 2}
|
||||||
<MeshLineMaterial color="red" linewidth={1} attenuate={false} />
|
position.y={0}
|
||||||
|
position.z={(rect.minY + rect.maxY) / 2}
|
||||||
|
rotation.x={-Math.PI / 2}
|
||||||
|
>
|
||||||
|
<T.PlaneGeometry args={[rect.maxX - rect.minX, rect.maxY - rect.minY]} />
|
||||||
|
<T.MeshBasicMaterial
|
||||||
|
color={new Color().setHSL((i * 1.77) % 1, 1, 0.5)}
|
||||||
|
opacity={0.9}
|
||||||
|
/>
|
||||||
|
</T.Mesh>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#each $lines as line}
|
||||||
|
<T.Mesh position.y={1}>
|
||||||
|
<MeshLineGeometry points={line.points} />
|
||||||
|
<MeshLineMaterial
|
||||||
|
color={line.color || "red"}
|
||||||
|
linewidth={1}
|
||||||
|
attenuate={false}
|
||||||
|
/>
|
||||||
</T.Mesh>
|
</T.Mesh>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Vector3 } from "three/src/math/Vector3.js";
|
import type { Box } from '@nodarium/types';
|
||||||
import { lines, points } from "./store";
|
import { Vector3 } from 'three/src/math/Vector3.js';
|
||||||
|
import Component from './Debug.svelte';
|
||||||
|
import { lines, points, rects } from './store';
|
||||||
|
|
||||||
export function debugPosition(x: number, y: number) {
|
export function debugPosition(x: number, y: number) {
|
||||||
points.update((p) => {
|
points.update((p) => {
|
||||||
@@ -8,18 +10,28 @@ export function debugPosition(x: number, y: number) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function debugRect(rect: Box) {
|
||||||
|
console.log(rect);
|
||||||
|
rects.update((r) => {
|
||||||
|
r.push(rect);
|
||||||
|
return r;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function clear() {
|
export function clear() {
|
||||||
points.set([]);
|
points.set([]);
|
||||||
lines.set([]);
|
lines.set([]);
|
||||||
|
rects.set([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function debugLine(line: Vector3[]) {
|
export function debugLine(points: Vector3[], color?: Color) {
|
||||||
lines.update((l) => {
|
lines.update((l) => {
|
||||||
l.push(line);
|
l.push({ points, color });
|
||||||
return l;
|
return l;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
import Component from "./Debug.svelte";
|
export default Component;
|
||||||
|
export function clearLines() {
|
||||||
export default Component
|
lines.set([]);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { writable } from "svelte/store";
|
import type { Box } from '@nodarium/types';
|
||||||
import { Vector3 } from "three/src/math/Vector3.js";
|
import { writable } from 'svelte/store';
|
||||||
|
import type { Color } from 'three';
|
||||||
|
import { Vector3 } from 'three/src/math/Vector3.js';
|
||||||
|
|
||||||
export const points = writable<Vector3[]>([]);
|
export const points = writable<Vector3[]>([]);
|
||||||
|
export const rects = writable<Box[]>([]);
|
||||||
export const lines = writable<Vector3[][]>([]);
|
export const lines = writable<{ points: Vector3[]; color?: Color }[]>([]);
|
||||||
|
|||||||
@@ -31,6 +31,10 @@
|
|||||||
import { CubicBezierCurve } from "three/src/extras/curves/CubicBezierCurve.js";
|
import { CubicBezierCurve } from "three/src/extras/curves/CubicBezierCurve.js";
|
||||||
import { Vector2 } from "three/src/math/Vector2.js";
|
import { Vector2 } from "three/src/math/Vector2.js";
|
||||||
import { appSettings } from "$lib/settings/app-settings.svelte";
|
import { appSettings } from "$lib/settings/app-settings.svelte";
|
||||||
|
import { getGraphState } from "../graph-state.svelte";
|
||||||
|
import { onDestroy } from "svelte";
|
||||||
|
|
||||||
|
const graphState = getGraphState();
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
x1: number;
|
x1: number;
|
||||||
@@ -38,20 +42,21 @@
|
|||||||
x2: number;
|
x2: number;
|
||||||
y2: number;
|
y2: number;
|
||||||
z: number;
|
z: number;
|
||||||
|
id?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { x1, y1, x2, y2, z }: Props = $props();
|
const { x1, y1, x2, y2, z, id }: Props = $props();
|
||||||
|
|
||||||
const thickness = $derived(Math.max(0.001, 0.00082 * Math.exp(0.055 * z)));
|
const thickness = $derived(Math.max(0.001, 0.00082 * Math.exp(0.055 * z)));
|
||||||
|
|
||||||
let points = $state<Vector3[]>([]);
|
let points = $state<Vector3[]>([]);
|
||||||
|
|
||||||
let lastId: string | null = null;
|
let lastId: string | null = null;
|
||||||
|
const curveId = $derived(`${x1}-${y1}-${x2}-${y2}`);
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
const new_x = x2 - x1;
|
const new_x = x2 - x1;
|
||||||
const new_y = y2 - y1;
|
const new_y = y2 - y1;
|
||||||
const curveId = `${x1}-${y1}-${x2}-${y2}`;
|
|
||||||
if (lastId === curveId) {
|
if (lastId === curveId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -72,6 +77,15 @@
|
|||||||
.getPoints(samples)
|
.getPoints(samples)
|
||||||
.map((p) => new Vector3(p.x, 0, p.y))
|
.map((p) => new Vector3(p.x, 0, p.y))
|
||||||
.flat();
|
.flat();
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
graphState.setEdgeGeometry(
|
||||||
|
id,
|
||||||
|
x1,
|
||||||
|
y1,
|
||||||
|
$state.snapshot(points) as unknown as Vector3[],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -79,6 +93,10 @@
|
|||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (id) graphState.removeEdgeGeometry(id);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<T.Mesh
|
<T.Mesh
|
||||||
@@ -101,6 +119,18 @@
|
|||||||
<T.CircleGeometry args={[0.5, 16]} />
|
<T.CircleGeometry args={[0.5, 16]} />
|
||||||
</T.Mesh>
|
</T.Mesh>
|
||||||
|
|
||||||
|
{#if graphState.hoveredEdgeId === id}
|
||||||
|
<T.Mesh position.x={x1} position.z={y1} position.y={0.1}>
|
||||||
|
<MeshLineGeometry {points} />
|
||||||
|
<MeshLineMaterial
|
||||||
|
width={thickness * 5}
|
||||||
|
color={lineColor}
|
||||||
|
opacity={0.5}
|
||||||
|
transparent
|
||||||
|
/>
|
||||||
|
</T.Mesh>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<T.Mesh position.x={x1} position.z={y1} position.y={0.1}>
|
<T.Mesh position.x={x1} position.z={y1} position.y={0.1}>
|
||||||
<MeshLineGeometry {points} />
|
<MeshLineGeometry {points} />
|
||||||
<MeshLineMaterial width={thickness} color={lineColor} />
|
<MeshLineMaterial width={thickness} color={lineColor} />
|
||||||
|
|||||||
@@ -1,31 +1,30 @@
|
|||||||
|
import throttle from '$lib/helpers/throttle';
|
||||||
import type {
|
import type {
|
||||||
Edge,
|
Edge,
|
||||||
Graph,
|
Graph,
|
||||||
NodeInstance,
|
|
||||||
NodeDefinition,
|
NodeDefinition,
|
||||||
NodeInput,
|
|
||||||
NodeRegistry,
|
|
||||||
NodeId,
|
NodeId,
|
||||||
Socket,
|
NodeInput,
|
||||||
} from "@nodarium/types";
|
NodeInstance,
|
||||||
import { fastHashString } from "@nodarium/utils";
|
NodeRegistry,
|
||||||
import { SvelteMap } from "svelte/reactivity";
|
Socket
|
||||||
import EventEmitter from "./helpers/EventEmitter";
|
} from '@nodarium/types';
|
||||||
import { createLogger } from "@nodarium/utils";
|
import { fastHashString } from '@nodarium/utils';
|
||||||
import throttle from "$lib/helpers/throttle";
|
import { createLogger } from '@nodarium/utils';
|
||||||
import { HistoryManager } from "./history-manager";
|
import { SvelteMap } from 'svelte/reactivity';
|
||||||
|
import EventEmitter from './helpers/EventEmitter';
|
||||||
|
import { HistoryManager } from './history-manager';
|
||||||
|
|
||||||
const logger = createLogger("graph-manager");
|
const logger = createLogger('graph-manager');
|
||||||
logger.mute();
|
logger.mute();
|
||||||
|
|
||||||
const clone =
|
const clone = 'structuredClone' in self
|
||||||
"structuredClone" in self
|
|
||||||
? self.structuredClone
|
? self.structuredClone
|
||||||
: (args: any) => JSON.parse(JSON.stringify(args));
|
: (args: any) => JSON.parse(JSON.stringify(args));
|
||||||
|
|
||||||
function areSocketsCompatible(
|
function areSocketsCompatible(
|
||||||
output: string | undefined,
|
output: string | undefined,
|
||||||
inputs: string | (string | undefined)[] | undefined,
|
inputs: string | (string | undefined)[] | undefined
|
||||||
) {
|
) {
|
||||||
if (Array.isArray(inputs) && output) {
|
if (Array.isArray(inputs) && output) {
|
||||||
return inputs.includes(output);
|
return inputs.includes(output);
|
||||||
@@ -34,24 +33,23 @@ function areSocketsCompatible(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function areEdgesEqual(firstEdge: Edge, secondEdge: Edge) {
|
function areEdgesEqual(firstEdge: Edge, secondEdge: Edge) {
|
||||||
|
|
||||||
if (firstEdge[0].id !== secondEdge[0].id) {
|
if (firstEdge[0].id !== secondEdge[0].id) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (firstEdge[1] !== secondEdge[1]) {
|
if (firstEdge[1] !== secondEdge[1]) {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (firstEdge[2].id !== secondEdge[2].id) {
|
if (firstEdge[2].id !== secondEdge[2].id) {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (firstEdge[3] !== secondEdge[3]) {
|
if (firstEdge[3] !== secondEdge[3]) {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GraphManager extends EventEmitter<{
|
export class GraphManager extends EventEmitter<{
|
||||||
@@ -62,7 +60,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
values: Record<string, unknown>;
|
values: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
}> {
|
}> {
|
||||||
status = $state<"loading" | "idle" | "error">();
|
status = $state<'loading' | 'idle' | 'error'>();
|
||||||
loaded = false;
|
loaded = false;
|
||||||
|
|
||||||
graph: Graph = { id: 0, nodes: [], edges: [] };
|
graph: Graph = { id: 0, nodes: [], edges: [] };
|
||||||
@@ -88,7 +86,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
history: HistoryManager = new HistoryManager();
|
history: HistoryManager = new HistoryManager();
|
||||||
execute = throttle(() => {
|
execute = throttle(() => {
|
||||||
if (this.loaded === false) return;
|
if (this.loaded === false) return;
|
||||||
this.emit("result", this.serialize());
|
this.emit('result', this.serialize());
|
||||||
}, 10);
|
}, 10);
|
||||||
|
|
||||||
constructor(public registry: NodeRegistry) {
|
constructor(public registry: NodeRegistry) {
|
||||||
@@ -100,21 +98,21 @@ export class GraphManager extends EventEmitter<{
|
|||||||
id: node.id,
|
id: node.id,
|
||||||
position: [...node.position],
|
position: [...node.position],
|
||||||
type: node.type,
|
type: node.type,
|
||||||
props: node.props,
|
props: node.props
|
||||||
})) as NodeInstance[];
|
})) as NodeInstance[];
|
||||||
const edges = this.edges.map((edge) => [
|
const edges = this.edges.map((edge) => [
|
||||||
edge[0].id,
|
edge[0].id,
|
||||||
edge[1],
|
edge[1],
|
||||||
edge[2].id,
|
edge[2].id,
|
||||||
edge[3],
|
edge[3]
|
||||||
]) as Graph["edges"];
|
]) as Graph['edges'];
|
||||||
const serialized = {
|
const serialized = {
|
||||||
id: this.graph.id,
|
id: this.graph.id,
|
||||||
settings: $state.snapshot(this.settings),
|
settings: $state.snapshot(this.settings),
|
||||||
nodes,
|
nodes,
|
||||||
edges,
|
edges
|
||||||
};
|
};
|
||||||
logger.log("serializing graph", serialized);
|
logger.log('serializing graph', serialized);
|
||||||
return clone($state.snapshot(serialized));
|
return clone($state.snapshot(serialized));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +146,95 @@ export class GraphManager extends EventEmitter<{
|
|||||||
return [...nodes.values()];
|
return [...nodes.values()];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getEdgeId(e: Edge) {
|
||||||
|
return `${e[0].id}-${e[1]}-${e[2].id}-${e[3]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEdgeById(id: string): Edge | undefined {
|
||||||
|
return this.edges.find((e) => this.getEdgeId(e) === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
dropNodeOnEdge(nodeId: number, edge: Edge) {
|
||||||
|
const draggedNode = this.getNode(nodeId);
|
||||||
|
if (!draggedNode || !draggedNode.state?.type) return;
|
||||||
|
|
||||||
|
const [fromNode, fromSocketIdx, toNode, toSocketKey] = edge;
|
||||||
|
|
||||||
|
const draggedInputs = Object.entries(draggedNode.state.type.inputs ?? {});
|
||||||
|
const draggedOutputs = draggedNode.state.type.outputs ?? [];
|
||||||
|
|
||||||
|
const edgeOutputSocketType = fromNode.state?.type?.outputs?.[fromSocketIdx];
|
||||||
|
const targetInput = toNode.state?.type?.inputs?.[toSocketKey];
|
||||||
|
const targetAcceptedTypes = [targetInput?.type, ...(targetInput?.accepts || [])];
|
||||||
|
|
||||||
|
const bestInputEntry = draggedInputs.find(([_, input]) => {
|
||||||
|
const accepted = [input.type, ...(input.accepts || [])];
|
||||||
|
return areSocketsCompatible(edgeOutputSocketType, accepted);
|
||||||
|
});
|
||||||
|
|
||||||
|
const bestOutputIdx = draggedOutputs.findIndex(outputType => areSocketsCompatible(outputType, targetAcceptedTypes));
|
||||||
|
|
||||||
|
if (!bestInputEntry || bestOutputIdx === -1) {
|
||||||
|
logger.error('Could not find compatible sockets for drop');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.startUndoGroup();
|
||||||
|
|
||||||
|
this.removeEdge(edge, { applyDeletion: false });
|
||||||
|
|
||||||
|
this.createEdge(fromNode, fromSocketIdx, draggedNode, bestInputEntry[0], {
|
||||||
|
applyUpdate: false
|
||||||
|
});
|
||||||
|
|
||||||
|
this.createEdge(draggedNode, bestOutputIdx, toNode, toSocketKey, {
|
||||||
|
applyUpdate: false
|
||||||
|
});
|
||||||
|
|
||||||
|
this.saveUndoGroup();
|
||||||
|
this.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
getPossibleDropOnEdges(nodeId: number): Edge[] {
|
||||||
|
const draggedNode = this.getNode(nodeId);
|
||||||
|
if (!draggedNode || !draggedNode.state?.type) return [];
|
||||||
|
|
||||||
|
const draggedInputs = Object.values(draggedNode.state.type.inputs ?? {});
|
||||||
|
const draggedOutputs = draggedNode.state.type.outputs ?? [];
|
||||||
|
|
||||||
|
// Optimization: Pre-calculate parents to avoid cycles
|
||||||
|
const parentIds = new Set(this.getParentsOfNode(draggedNode).map(n => n.id));
|
||||||
|
|
||||||
|
return this.edges.filter((edge) => {
|
||||||
|
const [fromNode, fromSocketIdx, toNode, toSocketKey] = edge;
|
||||||
|
|
||||||
|
// 1. Prevent cycles: If the target node is already a parent, we can't drop here
|
||||||
|
if (parentIds.has(toNode.id)) return false;
|
||||||
|
|
||||||
|
// 2. Prevent self-dropping: Don't drop on edges already connected to this node
|
||||||
|
if (fromNode.id === nodeId || toNode.id === nodeId) return false;
|
||||||
|
|
||||||
|
// 3. Check if edge.source can plug into ANY draggedNode.input
|
||||||
|
const edgeOutputSocketType = fromNode.state?.type?.outputs?.[fromSocketIdx];
|
||||||
|
const canPlugIntoDragged = draggedInputs.some(input => {
|
||||||
|
const acceptedTypes = [input.type, ...(input.accepts || [])];
|
||||||
|
return areSocketsCompatible(edgeOutputSocketType, acceptedTypes);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!canPlugIntoDragged) return false;
|
||||||
|
|
||||||
|
// 4. Check if ANY draggedNode.output can plug into edge.target
|
||||||
|
const targetInput = toNode.state?.type?.inputs?.[toSocketKey];
|
||||||
|
const targetAcceptedTypes = [targetInput?.type, ...(targetInput?.accepts || [])];
|
||||||
|
|
||||||
|
const draggedCanPlugIntoTarget = draggedOutputs.some(outputType =>
|
||||||
|
areSocketsCompatible(outputType, targetAcceptedTypes)
|
||||||
|
);
|
||||||
|
|
||||||
|
return draggedCanPlugIntoTarget;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getEdgesBetweenNodes(nodes: NodeInstance[]): [number, number, number, string][] {
|
getEdgesBetweenNodes(nodes: NodeInstance[]): [number, number, number, string][] {
|
||||||
const edges = [];
|
const edges = [];
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
@@ -155,14 +242,14 @@ export class GraphManager extends EventEmitter<{
|
|||||||
for (const child of children) {
|
for (const child of children) {
|
||||||
if (nodes.includes(child)) {
|
if (nodes.includes(child)) {
|
||||||
const edge = this.edges.find(
|
const edge = this.edges.find(
|
||||||
(e) => e[0].id === node.id && e[2].id === child.id,
|
(e) => e[0].id === node.id && e[2].id === child.id
|
||||||
);
|
);
|
||||||
if (edge) {
|
if (edge) {
|
||||||
edges.push([edge[0].id, edge[1], edge[2].id, edge[3]] as [
|
edges.push([edge[0].id, edge[1], edge[2].id, edge[3]] as [
|
||||||
number,
|
number,
|
||||||
number,
|
number,
|
||||||
number,
|
number,
|
||||||
string,
|
string
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,18 +266,18 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const n = node as NodeInstance;
|
const n = node as NodeInstance;
|
||||||
if (nodeType) {
|
if (nodeType) {
|
||||||
n.state = {
|
n.state = {
|
||||||
type: nodeType,
|
type: nodeType
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return [node.id, n];
|
return [node.id, n];
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const edges = graph.edges.map((edge) => {
|
const edges = graph.edges.map((edge) => {
|
||||||
const from = nodes.get(edge[0]);
|
const from = nodes.get(edge[0]);
|
||||||
const to = nodes.get(edge[2]);
|
const to = nodes.get(edge[2]);
|
||||||
if (!from || !to) {
|
if (!from || !to) {
|
||||||
throw new Error("Edge references non-existing node");
|
throw new Error('Edge references non-existing node');
|
||||||
}
|
}
|
||||||
from.state.children = from.state.children || [];
|
from.state.children = from.state.children || [];
|
||||||
from.state.children.push(to);
|
from.state.children.push(to);
|
||||||
@@ -214,21 +301,21 @@ export class GraphManager extends EventEmitter<{
|
|||||||
|
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.graph = graph;
|
this.graph = graph;
|
||||||
this.status = "loading";
|
this.status = 'loading';
|
||||||
this.id = graph.id;
|
this.id = graph.id;
|
||||||
|
|
||||||
logger.info("loading graph", $state.snapshot(graph));
|
logger.info('loading graph', $state.snapshot(graph));
|
||||||
|
|
||||||
const nodeIds = Array.from(new Set([...graph.nodes.map((n) => n.type)]));
|
const nodeIds = Array.from(new Set([...graph.nodes.map((n) => n.type)]));
|
||||||
await this.registry.load(nodeIds);
|
await this.registry.load(nodeIds);
|
||||||
|
|
||||||
logger.info("loaded node types", this.registry.getAllNodes());
|
logger.info('loaded node types', this.registry.getAllNodes());
|
||||||
|
|
||||||
for (const node of this.graph.nodes) {
|
for (const node of this.graph.nodes) {
|
||||||
const nodeType = this.registry.getNode(node.type);
|
const nodeType = this.registry.getNode(node.type);
|
||||||
if (!nodeType) {
|
if (!nodeType) {
|
||||||
logger.error(`Node type not found: ${node.type}`);
|
logger.error(`Node type not found: ${node.type}`);
|
||||||
this.status = "error";
|
this.status = 'error';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Turn into runtime node
|
// Turn into runtime node
|
||||||
@@ -253,11 +340,11 @@ export class GraphManager extends EventEmitter<{
|
|||||||
settingTypes[settingId] = {
|
settingTypes[settingId] = {
|
||||||
__node_type: type.id,
|
__node_type: type.id,
|
||||||
__node_input: key,
|
__node_input: key,
|
||||||
...type.inputs[key],
|
...type.inputs[key]
|
||||||
};
|
};
|
||||||
if (
|
if (
|
||||||
settingValues[settingId] === undefined &&
|
settingValues[settingId] === undefined
|
||||||
"value" in type.inputs[key]
|
&& 'value' in type.inputs[key]
|
||||||
) {
|
) {
|
||||||
settingValues[settingId] = type.inputs[key].value;
|
settingValues[settingId] = type.inputs[key].value;
|
||||||
}
|
}
|
||||||
@@ -267,14 +354,14 @@ export class GraphManager extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.settings = settingValues;
|
this.settings = settingValues;
|
||||||
this.emit("settings", { types: settingTypes, values: settingValues });
|
this.emit('settings', { types: settingTypes, values: settingValues });
|
||||||
|
|
||||||
this.history.reset();
|
this.history.reset();
|
||||||
this._init(this.graph);
|
this._init(this.graph);
|
||||||
|
|
||||||
this.save();
|
this.save();
|
||||||
|
|
||||||
this.status = "idle";
|
this.status = 'idle';
|
||||||
|
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
logger.log(`Graph loaded in ${performance.now() - a}ms`);
|
logger.log(`Graph loaded in ${performance.now() - a}ms`);
|
||||||
@@ -307,9 +394,9 @@ export class GraphManager extends EventEmitter<{
|
|||||||
if (settingId) {
|
if (settingId) {
|
||||||
settingTypes[settingId] = nodeType.inputs[key];
|
settingTypes[settingId] = nodeType.inputs[key];
|
||||||
if (
|
if (
|
||||||
settingValues &&
|
settingValues
|
||||||
settingValues?.[settingId] === undefined &&
|
&& settingValues?.[settingId] === undefined
|
||||||
"value" in nodeType.inputs[key]
|
&& 'value' in nodeType.inputs[key]
|
||||||
) {
|
) {
|
||||||
settingValues[settingId] = nodeType.inputs[key].value;
|
settingValues[settingId] = nodeType.inputs[key].value;
|
||||||
}
|
}
|
||||||
@@ -319,7 +406,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
|
|
||||||
this.settings = settingValues;
|
this.settings = settingValues;
|
||||||
this.settingTypes = settingTypes;
|
this.settingTypes = settingTypes;
|
||||||
this.emit("settings", { types: settingTypes, values: settingValues });
|
this.emit('settings', { types: settingTypes, values: settingValues });
|
||||||
}
|
}
|
||||||
|
|
||||||
getChildren(node: NodeInstance) {
|
getChildren(node: NodeInstance) {
|
||||||
@@ -368,7 +455,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const inputType = to?.state?.type?.inputs?.[toSocket]?.type;
|
const inputType = to?.state?.type?.inputs?.[toSocket]?.type;
|
||||||
if (outputType === inputType) {
|
if (outputType === inputType) {
|
||||||
this.createEdge(from, fromSocket, to, toSocket, {
|
this.createEdge(from, fromSocket, to, toSocket, {
|
||||||
applyUpdate: false,
|
applyUpdate: false
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -403,7 +490,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
// map old ids to new ids
|
// map old ids to new ids
|
||||||
const idMap = new Map<number, number>();
|
const idMap = new Map<number, number>();
|
||||||
|
|
||||||
let startId = this.createNodeId()
|
let startId = this.createNodeId();
|
||||||
|
|
||||||
nodes = nodes.map((node) => {
|
nodes = nodes.map((node) => {
|
||||||
const id = startId++;
|
const id = startId++;
|
||||||
@@ -420,7 +507,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const to = nodes.find((n) => n.id === idMap.get(edge[2]));
|
const to = nodes.find((n) => n.id === idMap.get(edge[2]));
|
||||||
|
|
||||||
if (!from || !to) {
|
if (!from || !to) {
|
||||||
throw new Error("Edge references non-existing node");
|
throw new Error('Edge references non-existing node');
|
||||||
}
|
}
|
||||||
|
|
||||||
to.state.parents = to.state.parents || [];
|
to.state.parents = to.state.parents || [];
|
||||||
@@ -445,11 +532,11 @@ export class GraphManager extends EventEmitter<{
|
|||||||
createNode({
|
createNode({
|
||||||
type,
|
type,
|
||||||
position,
|
position,
|
||||||
props = {},
|
props = {}
|
||||||
}: {
|
}: {
|
||||||
type: NodeInstance["type"];
|
type: NodeInstance['type'];
|
||||||
position: NodeInstance["position"];
|
position: NodeInstance['position'];
|
||||||
props: NodeInstance["props"];
|
props: NodeInstance['props'];
|
||||||
}) {
|
}) {
|
||||||
const nodeType = this.registry.getNode(type);
|
const nodeType = this.registry.getNode(type);
|
||||||
if (!nodeType) {
|
if (!nodeType) {
|
||||||
@@ -462,14 +549,14 @@ export class GraphManager extends EventEmitter<{
|
|||||||
type,
|
type,
|
||||||
position,
|
position,
|
||||||
state: { type: nodeType },
|
state: { type: nodeType },
|
||||||
props,
|
props
|
||||||
});
|
});
|
||||||
|
|
||||||
this.nodes.set(node.id, node);
|
this.nodes.set(node.id, node);
|
||||||
|
|
||||||
this.save();
|
this.save();
|
||||||
|
|
||||||
return node
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
createEdge(
|
createEdge(
|
||||||
@@ -477,17 +564,16 @@ export class GraphManager extends EventEmitter<{
|
|||||||
fromSocket: number,
|
fromSocket: number,
|
||||||
to: NodeInstance,
|
to: NodeInstance,
|
||||||
toSocket: string,
|
toSocket: string,
|
||||||
{ applyUpdate = true } = {},
|
{ applyUpdate = true } = {}
|
||||||
): Edge | undefined {
|
): Edge | undefined {
|
||||||
|
|
||||||
const existingEdges = this.getEdgesToNode(to);
|
const existingEdges = this.getEdgesToNode(to);
|
||||||
|
|
||||||
// check if this exact edge already exists
|
// check if this exact edge already exists
|
||||||
const existingEdge = existingEdges.find(
|
const existingEdge = existingEdges.find(
|
||||||
(e) => e[0].id === from.id && e[1] === fromSocket && e[3] === toSocket,
|
(e) => e[0].id === from.id && e[1] === fromSocket && e[3] === toSocket
|
||||||
);
|
);
|
||||||
if (existingEdge) {
|
if (existingEdge) {
|
||||||
logger.error("Edge already exists", existingEdge);
|
logger.error('Edge already exists', existingEdge);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -500,13 +586,13 @@ export class GraphManager extends EventEmitter<{
|
|||||||
|
|
||||||
if (!areSocketsCompatible(fromSocketType, toSocketType)) {
|
if (!areSocketsCompatible(fromSocketType, toSocketType)) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Socket types do not match: ${fromSocketType} !== ${toSocketType}`,
|
`Socket types do not match: ${fromSocketType} !== ${toSocketType}`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const edgeToBeReplaced = this.edges.find(
|
const edgeToBeReplaced = this.edges.find(
|
||||||
(e) => e[2].id === to.id && e[3] === toSocket,
|
(e) => e[2].id === to.id && e[3] === toSocket
|
||||||
);
|
);
|
||||||
if (edgeToBeReplaced) {
|
if (edgeToBeReplaced) {
|
||||||
this.removeEdge(edgeToBeReplaced, { applyDeletion: false });
|
this.removeEdge(edgeToBeReplaced, { applyDeletion: false });
|
||||||
@@ -533,7 +619,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const nextState = this.history.undo();
|
const nextState = this.history.undo();
|
||||||
if (nextState) {
|
if (nextState) {
|
||||||
this._init(nextState);
|
this._init(nextState);
|
||||||
this.emit("save", this.serialize());
|
this.emit('save', this.serialize());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,7 +627,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const nextState = this.history.redo();
|
const nextState = this.history.redo();
|
||||||
if (nextState) {
|
if (nextState) {
|
||||||
this._init(nextState);
|
this._init(nextState);
|
||||||
this.emit("save", this.serialize());
|
this.emit('save', this.serialize());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,8 +644,8 @@ export class GraphManager extends EventEmitter<{
|
|||||||
if (this.currentUndoGroup) return;
|
if (this.currentUndoGroup) return;
|
||||||
const state = this.serialize();
|
const state = this.serialize();
|
||||||
this.history.save(state);
|
this.history.save(state);
|
||||||
this.emit("save", state);
|
this.emit('save', state);
|
||||||
logger.log("saving graphs", state);
|
logger.log('saving graphs', state);
|
||||||
}
|
}
|
||||||
|
|
||||||
getParentsOfNode(node: NodeInstance) {
|
getParentsOfNode(node: NodeInstance) {
|
||||||
@@ -567,7 +653,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const stack = node.state?.parents?.slice(0);
|
const stack = node.state?.parents?.slice(0);
|
||||||
while (stack?.length) {
|
while (stack?.length) {
|
||||||
if (parents.length > 1000000) {
|
if (parents.length > 1000000) {
|
||||||
logger.warn("Infinite loop detected");
|
logger.warn('Infinite loop detected');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const parent = stack.pop();
|
const parent = stack.pop();
|
||||||
@@ -586,26 +672,28 @@ export class GraphManager extends EventEmitter<{
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const definitions = typeof socket.index === "string"
|
const definitions = typeof socket.index === 'string'
|
||||||
? allDefinitions.filter(s => {
|
? allDefinitions.filter(s => {
|
||||||
return s.outputs?.find(_s => Object
|
return s.outputs?.find(_s =>
|
||||||
|
Object
|
||||||
.values(nodeType?.inputs || {})
|
.values(nodeType?.inputs || {})
|
||||||
.map(s => s.type)
|
.map(s => s.type)
|
||||||
.includes(_s as NodeInput["type"])
|
.includes(_s as NodeInput['type'])
|
||||||
)
|
);
|
||||||
})
|
})
|
||||||
: allDefinitions.filter(s => Object
|
: allDefinitions.filter(s =>
|
||||||
|
Object
|
||||||
.values(s.inputs ?? {})
|
.values(s.inputs ?? {})
|
||||||
.find(s => {
|
.find(s => {
|
||||||
if (s.hidden) return false;
|
if (s.hidden) return false;
|
||||||
if (nodeType.outputs?.includes(s.type)) {
|
if (nodeType.outputs?.includes(s.type)) {
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
return s.accepts?.find(a => nodeType.outputs?.includes(a))
|
return s.accepts?.find(a => nodeType.outputs?.includes(a));
|
||||||
}))
|
})
|
||||||
|
);
|
||||||
return definitions
|
|
||||||
|
|
||||||
|
return definitions;
|
||||||
}
|
}
|
||||||
|
|
||||||
getPossibleSockets({ node, index }: Socket): [NodeInstance, string | number][] {
|
getPossibleSockets({ node, index }: Socket): [NodeInstance, string | number][] {
|
||||||
@@ -615,11 +703,11 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const sockets: [NodeInstance, string | number][] = [];
|
const sockets: [NodeInstance, string | number][] = [];
|
||||||
|
|
||||||
// if index is a string, we are an input looking for outputs
|
// if index is a string, we are an input looking for outputs
|
||||||
if (typeof index === "string") {
|
if (typeof index === 'string') {
|
||||||
// filter out self and child nodes
|
// filter out self and child nodes
|
||||||
const children = new Set(this.getChildren(node).map((n) => n.id));
|
const children = new Set(this.getChildren(node).map((n) => n.id));
|
||||||
const nodes = this.getAllNodes().filter(
|
const nodes = this.getAllNodes().filter(
|
||||||
(n) => n.id !== node.id && !children.has(n.id),
|
(n) => n.id !== node.id && !children.has(n.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
const ownType = nodeType?.inputs?.[index].type;
|
const ownType = nodeType?.inputs?.[index].type;
|
||||||
@@ -634,20 +722,20 @@ export class GraphManager extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (typeof index === "number") {
|
} else if (typeof index === 'number') {
|
||||||
// if index is a number, we are an output looking for inputs
|
// if index is a number, we are an output looking for inputs
|
||||||
|
|
||||||
// filter out self and parent nodes
|
// filter out self and parent nodes
|
||||||
const parents = new Set(this.getParentsOfNode(node).map((n) => n.id));
|
const parents = new Set(this.getParentsOfNode(node).map((n) => n.id));
|
||||||
const nodes = this.getAllNodes().filter(
|
const nodes = this.getAllNodes().filter(
|
||||||
(n) => n.id !== node.id && !parents.has(n.id),
|
(n) => n.id !== node.id && !parents.has(n.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
// get edges from this socket
|
// get edges from this socket
|
||||||
const edges = new Map(
|
const edges = new Map(
|
||||||
this.getEdgesFromNode(node)
|
this.getEdgesFromNode(node)
|
||||||
.filter((e) => e[1] === index)
|
.filter((e) => e[1] === index)
|
||||||
.map((e) => [e[2].id, e[3]]),
|
.map((e) => [e[2].id, e[3]])
|
||||||
);
|
);
|
||||||
|
|
||||||
const ownType = nodeType.outputs?.[index];
|
const ownType = nodeType.outputs?.[index];
|
||||||
@@ -660,8 +748,8 @@ export class GraphManager extends EventEmitter<{
|
|||||||
otherType.push(...(inputs[key].accepts || []));
|
otherType.push(...(inputs[key].accepts || []));
|
||||||
|
|
||||||
if (
|
if (
|
||||||
areSocketsCompatible(ownType, otherType) &&
|
areSocketsCompatible(ownType, otherType)
|
||||||
edges.get(node.id) !== key
|
&& edges.get(node.id) !== key
|
||||||
) {
|
) {
|
||||||
sockets.push([node, key]);
|
sockets.push([node, key]);
|
||||||
}
|
}
|
||||||
@@ -674,7 +762,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
|
|
||||||
removeEdge(
|
removeEdge(
|
||||||
edge: Edge,
|
edge: Edge,
|
||||||
{ applyDeletion = true }: { applyDeletion?: boolean } = {},
|
{ applyDeletion = true }: { applyDeletion?: boolean } = {}
|
||||||
) {
|
) {
|
||||||
const id0 = edge[0].id;
|
const id0 = edge[0].id;
|
||||||
const sid0 = edge[1];
|
const sid0 = edge[1];
|
||||||
@@ -682,21 +770,20 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const sid2 = edge[3];
|
const sid2 = edge[3];
|
||||||
|
|
||||||
const _edge = this.edges.find(
|
const _edge = this.edges.find(
|
||||||
(e) =>
|
(e) => e[0].id === id0 && e[1] === sid0 && e[2].id === id2 && e[3] === sid2
|
||||||
e[0].id === id0 && e[1] === sid0 && e[2].id === id2 && e[3] === sid2,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!_edge) return;
|
if (!_edge) return;
|
||||||
|
|
||||||
if (edge[0].state.children) {
|
if (edge[0].state.children) {
|
||||||
edge[0].state.children = edge[0].state.children.filter(
|
edge[0].state.children = edge[0].state.children.filter(
|
||||||
(n: NodeInstance) => n.id !== id2,
|
(n: NodeInstance) => n.id !== id2
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (edge[2].state.parents) {
|
if (edge[2].state.parents) {
|
||||||
edge[2].state.parents = edge[2].state.parents.filter(
|
edge[2].state.parents = edge[2].state.parents.filter(
|
||||||
(n: NodeInstance) => n.id !== id0,
|
(n: NodeInstance) => n.id !== id0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -705,7 +792,6 @@ export class GraphManager extends EventEmitter<{
|
|||||||
this.execute();
|
this.execute();
|
||||||
this.save();
|
this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getEdgesToNode(node: NodeInstance) {
|
getEdgesToNode(node: NodeInstance) {
|
||||||
|
|||||||
@@ -1,36 +1,43 @@
|
|||||||
import type { NodeInstance, Socket } from "@nodarium/types";
|
import type { NodeInstance, Socket } from '@nodarium/types';
|
||||||
import { getContext, setContext } from "svelte";
|
import { getContext, setContext } from 'svelte';
|
||||||
import { SvelteSet } from "svelte/reactivity";
|
import { SvelteSet } from 'svelte/reactivity';
|
||||||
import type { GraphManager } from "./graph-manager.svelte";
|
import type { OrthographicCamera, Vector3 } from 'three';
|
||||||
import type { OrthographicCamera } from "three";
|
import type { GraphManager } from './graph-manager.svelte';
|
||||||
|
|
||||||
|
const graphStateKey = Symbol('graph-state');
|
||||||
const graphStateKey = Symbol("graph-state");
|
|
||||||
export function getGraphState() {
|
export function getGraphState() {
|
||||||
return getContext<GraphState>(graphStateKey);
|
return getContext<GraphState>(graphStateKey);
|
||||||
}
|
}
|
||||||
export function setGraphState(graphState: GraphState) {
|
export function setGraphState(graphState: GraphState) {
|
||||||
return setContext(graphStateKey, graphState)
|
return setContext(graphStateKey, graphState);
|
||||||
}
|
}
|
||||||
|
|
||||||
const graphManagerKey = Symbol("graph-manager");
|
const graphManagerKey = Symbol('graph-manager');
|
||||||
export function getGraphManager() {
|
export function getGraphManager() {
|
||||||
return getContext<GraphManager>(graphManagerKey)
|
return getContext<GraphManager>(graphManagerKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setGraphManager(manager: GraphManager) {
|
export function setGraphManager(manager: GraphManager) {
|
||||||
return setContext(graphManagerKey, manager);
|
return setContext(graphManagerKey, manager);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GraphState {
|
type EdgeData = {
|
||||||
|
x1: number;
|
||||||
|
y1: number;
|
||||||
|
points: Vector3[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export class GraphState {
|
||||||
constructor(private graph: GraphManager) {
|
constructor(private graph: GraphManager) {
|
||||||
$effect.root(() => {
|
$effect.root(() => {
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
localStorage.setItem("cameraPosition", `[${this.cameraPosition[0]},${this.cameraPosition[1]},${this.cameraPosition[2]}]`)
|
localStorage.setItem(
|
||||||
})
|
'cameraPosition',
|
||||||
})
|
`[${this.cameraPosition[0]},${this.cameraPosition[1]},${this.cameraPosition[2]}]`
|
||||||
const storedPosition = localStorage.getItem("cameraPosition")
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const storedPosition = localStorage.getItem('cameraPosition');
|
||||||
if (storedPosition) {
|
if (storedPosition) {
|
||||||
try {
|
try {
|
||||||
const d = JSON.parse(storedPosition);
|
const d = JSON.parse(storedPosition);
|
||||||
@@ -38,7 +45,7 @@ export class GraphState {
|
|||||||
this.cameraPosition[1] = d[1];
|
this.cameraPosition[1] = d[1];
|
||||||
this.cameraPosition[2] = d[2];
|
this.cameraPosition[2] = d[2];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Failed to parsed stored camera position", e);
|
console.log('Failed to parsed stored camera position', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,13 +53,16 @@ export class GraphState {
|
|||||||
width = $state(100);
|
width = $state(100);
|
||||||
height = $state(100);
|
height = $state(100);
|
||||||
|
|
||||||
|
hoveredEdgeId = $state<string | null>(null);
|
||||||
|
edges = new Map<string, EdgeData>();
|
||||||
|
|
||||||
wrapper = $state<HTMLDivElement>(null!);
|
wrapper = $state<HTMLDivElement>(null!);
|
||||||
rect: DOMRect = $derived(
|
rect: DOMRect = $derived(
|
||||||
(this.wrapper && this.width && this.height) ? this.wrapper.getBoundingClientRect() : new DOMRect(0, 0, 0, 0),
|
(this.wrapper && this.width && this.height) ? this.wrapper.getBoundingClientRect() : new DOMRect(0, 0, 0, 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
camera = $state<OrthographicCamera>(null!);
|
camera = $state<OrthographicCamera>(null!);
|
||||||
cameraPosition: [number, number, number] = $state([0, 0, 4]);
|
cameraPosition: [number, number, number] = $state([0, 0, 100]);
|
||||||
|
|
||||||
clipboard: null | {
|
clipboard: null | {
|
||||||
nodes: NodeInstance[];
|
nodes: NodeInstance[];
|
||||||
@@ -63,7 +73,7 @@ export class GraphState {
|
|||||||
this.cameraPosition[0] - this.width / this.cameraPosition[2] / 2,
|
this.cameraPosition[0] - this.width / this.cameraPosition[2] / 2,
|
||||||
this.cameraPosition[0] + this.width / this.cameraPosition[2] / 2,
|
this.cameraPosition[0] + this.width / this.cameraPosition[2] / 2,
|
||||||
this.cameraPosition[1] - this.height / this.cameraPosition[2] / 2,
|
this.cameraPosition[1] - this.height / this.cameraPosition[2] / 2,
|
||||||
this.cameraPosition[1] + this.height / this.cameraPosition[2] / 2,
|
this.cameraPosition[1] + this.height / this.cameraPosition[2] / 2
|
||||||
]);
|
]);
|
||||||
|
|
||||||
boxSelection = $state(false);
|
boxSelection = $state(false);
|
||||||
@@ -71,8 +81,8 @@ export class GraphState {
|
|||||||
addMenuPosition = $state<[number, number] | null>(null);
|
addMenuPosition = $state<[number, number] | null>(null);
|
||||||
|
|
||||||
snapToGrid = $state(false);
|
snapToGrid = $state(false);
|
||||||
showGrid = $state(true)
|
showGrid = $state(true);
|
||||||
showHelp = $state(false)
|
showHelp = $state(false);
|
||||||
|
|
||||||
cameraDown = [0, 0];
|
cameraDown = [0, 0];
|
||||||
mouseDownNodeId = -1;
|
mouseDownNodeId = -1;
|
||||||
@@ -88,33 +98,49 @@ export class GraphState {
|
|||||||
hoveredSocket = $state<Socket | null>(null);
|
hoveredSocket = $state<Socket | null>(null);
|
||||||
possibleSockets = $state<Socket[]>([]);
|
possibleSockets = $state<Socket[]>([]);
|
||||||
possibleSocketIds = $derived(
|
possibleSocketIds = $derived(
|
||||||
new Set(this.possibleSockets.map((s) => `${s.node.id}-${s.index}`)),
|
new Set(this.possibleSockets.map((s) => `${s.node.id}-${s.index}`))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
getEdges() {
|
||||||
|
return $state.snapshot(this.edges);
|
||||||
|
}
|
||||||
|
|
||||||
clearSelection() {
|
clearSelection() {
|
||||||
this.selectedNodes.clear();
|
this.selectedNodes.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
isBodyFocused = () => document?.activeElement?.nodeName !== "INPUT";
|
isBodyFocused = () => document?.activeElement?.nodeName !== 'INPUT';
|
||||||
|
|
||||||
|
setEdgeGeometry(edgeId: string, x1: number, y1: number, points: Vector3[]) {
|
||||||
|
this.edges.set(edgeId, { x1, y1, points });
|
||||||
|
}
|
||||||
|
|
||||||
|
removeEdgeGeometry(edgeId: string) {
|
||||||
|
this.edges.delete(edgeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
getEdgeData() {
|
||||||
|
return this.edges;
|
||||||
|
}
|
||||||
|
|
||||||
updateNodePosition(node: NodeInstance) {
|
updateNodePosition(node: NodeInstance) {
|
||||||
if (
|
if (
|
||||||
node.state.x === node.position[0] &&
|
node.state.x === node.position[0]
|
||||||
node.state.y === node.position[1]
|
&& node.state.y === node.position[1]
|
||||||
) {
|
) {
|
||||||
delete node.state.x;
|
delete node.state.x;
|
||||||
delete node.state.y;
|
delete node.state.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.state["x"] !== undefined && node.state["y"] !== undefined) {
|
if (node.state['x'] !== undefined && node.state['y'] !== undefined) {
|
||||||
if (node.state.ref) {
|
if (node.state.ref) {
|
||||||
node.state.ref.style.setProperty("--nx", `${node.state.x * 10}px`);
|
node.state.ref.style.setProperty('--nx', `${node.state.x * 10}px`);
|
||||||
node.state.ref.style.setProperty("--ny", `${node.state.y * 10}px`);
|
node.state.ref.style.setProperty('--ny', `${node.state.y * 10}px`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (node.state.ref) {
|
if (node.state.ref) {
|
||||||
node.state.ref.style.setProperty("--nx", `${node.position[0] * 10}px`);
|
node.state.ref.style.setProperty('--nx', `${node.position[0] * 10}px`);
|
||||||
node.state.ref.style.setProperty("--ny", `${node.position[1] * 10}px`);
|
node.state.ref.style.setProperty('--ny', `${node.position[1] * 10}px`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,18 +160,18 @@ export class GraphState {
|
|||||||
|
|
||||||
getSocketPosition(
|
getSocketPosition(
|
||||||
node: NodeInstance,
|
node: NodeInstance,
|
||||||
index: string | number,
|
index: string | number
|
||||||
): [number, number] {
|
): [number, number] {
|
||||||
if (typeof index === "number") {
|
if (typeof index === 'number') {
|
||||||
return [
|
return [
|
||||||
(node?.state?.x ?? node.position[0]) + 20,
|
(node?.state?.x ?? node.position[0]) + 20,
|
||||||
(node?.state?.y ?? node.position[1]) + 2.5 + 10 * index,
|
(node?.state?.y ?? node.position[1]) + 2.5 + 10 * index
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
const _index = Object.keys(node.state?.type?.inputs || {}).indexOf(index);
|
const _index = Object.keys(node.state?.type?.inputs || {}).indexOf(index);
|
||||||
return [
|
return [
|
||||||
node?.state?.x ?? node.position[0],
|
node?.state?.x ?? node.position[0],
|
||||||
(node?.state?.y ?? node.position[1]) + 10 + 10 * _index,
|
(node?.state?.y ?? node.position[1]) + 10 + 10 * _index
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,26 +185,26 @@ export class GraphState {
|
|||||||
if (!node?.inputs) {
|
if (!node?.inputs) {
|
||||||
return 5;
|
return 5;
|
||||||
}
|
}
|
||||||
const height =
|
const height = 5
|
||||||
5 +
|
+ 10
|
||||||
10 *
|
* Object.keys(node.inputs).filter(
|
||||||
Object.keys(node.inputs).filter(
|
|
||||||
(p) =>
|
(p) =>
|
||||||
p !== "seed" &&
|
p !== 'seed'
|
||||||
node?.inputs &&
|
&& node?.inputs
|
||||||
!("setting" in node?.inputs?.[p]) &&
|
&& !('setting' in node?.inputs?.[p])
|
||||||
node.inputs[p].hidden !== true,
|
&& node.inputs[p].hidden !== true
|
||||||
).length;
|
).length;
|
||||||
this.nodeHeightCache[nodeTypeId] = height;
|
this.nodeHeightCache[nodeTypeId] = height;
|
||||||
return height;
|
return height;
|
||||||
}
|
}
|
||||||
|
|
||||||
copyNodes() {
|
copyNodes() {
|
||||||
if (this.activeNodeId === -1 && !this.selectedNodes?.size)
|
if (this.activeNodeId === -1 && !this.selectedNodes?.size) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
let nodes = [
|
let nodes = [
|
||||||
this.activeNodeId,
|
this.activeNodeId,
|
||||||
...(this.selectedNodes?.values() || []),
|
...(this.selectedNodes?.values() || [])
|
||||||
]
|
]
|
||||||
.map((id) => this.graph.getNode(id))
|
.map((id) => this.graph.getNode(id))
|
||||||
.filter(b => !!b);
|
.filter(b => !!b);
|
||||||
@@ -188,14 +214,14 @@ export class GraphState {
|
|||||||
...node,
|
...node,
|
||||||
position: [
|
position: [
|
||||||
this.mousePosition[0] - node.position[0],
|
this.mousePosition[0] - node.position[0],
|
||||||
this.mousePosition[1] - node.position[1],
|
this.mousePosition[1] - node.position[1]
|
||||||
],
|
],
|
||||||
tmp: undefined,
|
tmp: undefined
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.clipboard = {
|
this.clipboard = {
|
||||||
nodes: nodes,
|
nodes: nodes,
|
||||||
edges: edges,
|
edges: edges
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,14 +243,13 @@ export class GraphState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
setDownSocket(socket: Socket) {
|
setDownSocket(socket: Socket) {
|
||||||
this.activeSocket = socket;
|
this.activeSocket = socket;
|
||||||
|
|
||||||
let { node, index, position } = socket;
|
let { node, index, position } = socket;
|
||||||
|
|
||||||
// remove existing edge
|
// remove existing edge
|
||||||
if (typeof index === "string") {
|
if (typeof index === 'string') {
|
||||||
const edges = this.graph.getEdgesToNode(node);
|
const edges = this.graph.getEdgesToNode(node);
|
||||||
for (const edge of edges) {
|
for (const edge of edges) {
|
||||||
if (edge[3] === index) {
|
if (edge[3] === index) {
|
||||||
@@ -241,7 +266,7 @@ export class GraphState {
|
|||||||
this.activeSocket = {
|
this.activeSocket = {
|
||||||
node,
|
node,
|
||||||
index,
|
index,
|
||||||
position,
|
position
|
||||||
};
|
};
|
||||||
|
|
||||||
this.possibleSockets = this.graph
|
this.possibleSockets = this.graph
|
||||||
@@ -250,18 +275,17 @@ export class GraphState {
|
|||||||
return {
|
return {
|
||||||
node,
|
node,
|
||||||
index,
|
index,
|
||||||
position: this.getSocketPosition(node, index),
|
position: this.getSocketPosition(node, index)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
projectScreenToWorld(x: number, y: number): [number, number] {
|
projectScreenToWorld(x: number, y: number): [number, number] {
|
||||||
return [
|
return [
|
||||||
this.cameraPosition[0] +
|
this.cameraPosition[0]
|
||||||
(x - this.width / 2) / this.cameraPosition[2],
|
+ (x - this.width / 2) / this.cameraPosition[2],
|
||||||
this.cameraPosition[1] +
|
this.cameraPosition[1]
|
||||||
(y - this.height / 2) / this.cameraPosition[2],
|
+ (y - this.height / 2) / this.cameraPosition[2]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,8 +298,8 @@ export class GraphState {
|
|||||||
if (event.button === 0) {
|
if (event.button === 0) {
|
||||||
// check if the clicked element is a node
|
// check if the clicked element is a node
|
||||||
if (event.target instanceof HTMLElement) {
|
if (event.target instanceof HTMLElement) {
|
||||||
const nodeElement = event.target.closest(".node");
|
const nodeElement = event.target.closest('.node');
|
||||||
const nodeId = nodeElement?.getAttribute?.("data-node-id");
|
const nodeId = nodeElement?.getAttribute?.('data-node-id');
|
||||||
if (nodeId) {
|
if (nodeId) {
|
||||||
clickedNodeId = parseInt(nodeId, 10);
|
clickedNodeId = parseInt(nodeId, 10);
|
||||||
}
|
}
|
||||||
@@ -303,10 +327,10 @@ export class GraphState {
|
|||||||
const height = this.getNodeHeight(node.type);
|
const height = this.getNodeHeight(node.type);
|
||||||
const width = 20;
|
const width = 20;
|
||||||
return (
|
return (
|
||||||
node.position[0] > this.cameraBounds[0] - width &&
|
node.position[0] > this.cameraBounds[0] - width
|
||||||
node.position[0] < this.cameraBounds[1] &&
|
&& node.position[0] < this.cameraBounds[1]
|
||||||
node.position[1] > this.cameraBounds[2] - height &&
|
&& node.position[1] > this.cameraBounds[2] - height
|
||||||
node.position[1] < this.cameraBounds[3]
|
&& node.position[1] < this.cameraBounds[3]
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,10 @@
|
|||||||
import HelpView from "../components/HelpView.svelte";
|
import HelpView from "../components/HelpView.svelte";
|
||||||
import { getGraphManager, getGraphState } from "../graph-state.svelte";
|
import { getGraphManager, getGraphState } from "../graph-state.svelte";
|
||||||
import { HTML } from "@threlte/extras";
|
import { HTML } from "@threlte/extras";
|
||||||
import { FileDropEventManager, MouseEventManager } from "./events";
|
|
||||||
import { maxZoom, minZoom } from "./constants";
|
import { maxZoom, minZoom } from "./constants";
|
||||||
|
import Debug from "../debug/Debug.svelte";
|
||||||
|
import { FileDropEventManager } from "./drop.events";
|
||||||
|
import { MouseEventManager } from "./mouse.events";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
keymap,
|
keymap,
|
||||||
@@ -174,9 +176,18 @@
|
|||||||
|
|
||||||
{#each graph.edges as edge}
|
{#each graph.edges as edge}
|
||||||
{@const [x1, y1, x2, y2] = getEdgePosition(edge)}
|
{@const [x1, y1, x2, y2] = getEdgePosition(edge)}
|
||||||
<EdgeEl z={graphState.cameraPosition[2]} {x1} {y1} {x2} {y2} />
|
<EdgeEl
|
||||||
|
id={graph.getEdgeId(edge)}
|
||||||
|
z={graphState.cameraPosition[2]}
|
||||||
|
{x1}
|
||||||
|
{y1}
|
||||||
|
{x2}
|
||||||
|
{y2}
|
||||||
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
|
<Debug />
|
||||||
|
|
||||||
<HTML transform={false}>
|
<HTML transform={false}>
|
||||||
<div
|
<div
|
||||||
role="tree"
|
role="tree"
|
||||||
|
|||||||
107
app/src/lib/graph-interface/graph/drop.events.ts
Normal file
107
app/src/lib/graph-interface/graph/drop.events.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { GraphSchema, type NodeId } from '@nodarium/types';
|
||||||
|
import type { GraphManager } from '../graph-manager.svelte';
|
||||||
|
import type { GraphState } from '../graph-state.svelte';
|
||||||
|
|
||||||
|
export class FileDropEventManager {
|
||||||
|
constructor(
|
||||||
|
private graph: GraphManager,
|
||||||
|
private state: GraphState
|
||||||
|
) { }
|
||||||
|
|
||||||
|
handleFileDrop(event: DragEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.state.isDragging = false;
|
||||||
|
if (!event.dataTransfer) return;
|
||||||
|
const nodeId = event.dataTransfer.getData('data/node-id') as NodeId;
|
||||||
|
let mx = event.clientX - this.state.rect.x;
|
||||||
|
let my = event.clientY - this.state.rect.y;
|
||||||
|
|
||||||
|
if (nodeId) {
|
||||||
|
let nodeOffsetX = event.dataTransfer.getData('data/node-offset-x');
|
||||||
|
let nodeOffsetY = event.dataTransfer.getData('data/node-offset-y');
|
||||||
|
if (nodeOffsetX && nodeOffsetY) {
|
||||||
|
mx += parseInt(nodeOffsetX);
|
||||||
|
my += parseInt(nodeOffsetY);
|
||||||
|
}
|
||||||
|
|
||||||
|
let props = {};
|
||||||
|
let rawNodeProps = event.dataTransfer.getData('data/node-props');
|
||||||
|
if (rawNodeProps) {
|
||||||
|
try {
|
||||||
|
props = JSON.parse(rawNodeProps);
|
||||||
|
} catch (e) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
const pos = this.state.projectScreenToWorld(mx, my);
|
||||||
|
this.graph.registry.load([nodeId]).then(() => {
|
||||||
|
this.graph.createNode({
|
||||||
|
type: nodeId,
|
||||||
|
props,
|
||||||
|
position: pos
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (event.dataTransfer.files.length) {
|
||||||
|
const file = event.dataTransfer.files[0];
|
||||||
|
|
||||||
|
if (file.type === 'application/wasm') {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async (e) => {
|
||||||
|
const buffer = e.target?.result;
|
||||||
|
if (buffer?.constructor === ArrayBuffer) {
|
||||||
|
const nodeType = await this.graph.registry.register(buffer);
|
||||||
|
|
||||||
|
this.graph.createNode({
|
||||||
|
type: nodeType.id,
|
||||||
|
props: {},
|
||||||
|
position: this.state.projectScreenToWorld(mx, my)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
} else if (file.type === 'application/json') {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const buffer = e.target?.result as ArrayBuffer;
|
||||||
|
if (buffer) {
|
||||||
|
const state = GraphSchema.parse(JSON.parse(buffer.toString()));
|
||||||
|
this.graph.load(state);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseLeave() {
|
||||||
|
this.state.isDragging = false;
|
||||||
|
this.state.isPanning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragEnter(e: DragEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.state.isDragging = true;
|
||||||
|
this.state.isPanning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragOver(e: DragEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.state.isDragging = true;
|
||||||
|
this.state.isPanning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragEnd(e: DragEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.state.isDragging = true;
|
||||||
|
this.state.isPanning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEventListenerProps() {
|
||||||
|
return {
|
||||||
|
ondragenter: (ev: DragEvent) => this.handleDragEnter(ev),
|
||||||
|
ondragover: (ev: DragEvent) => this.handleDragOver(ev),
|
||||||
|
ondragexit: (ev: DragEvent) => this.handleDragEnd(ev),
|
||||||
|
ondrop: (ev: DragEvent) => this.handleFileDrop(ev),
|
||||||
|
onmouseleave: () => this.handleMouseLeave()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
110
app/src/lib/graph-interface/graph/edge.events.ts
Normal file
110
app/src/lib/graph-interface/graph/edge.events.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import type { Box } from '@nodarium/types';
|
||||||
|
import type { GraphManager } from '../graph-manager.svelte';
|
||||||
|
import type { GraphState } from '../graph-state.svelte';
|
||||||
|
import { distanceFromPointToSegment } from '../helpers';
|
||||||
|
|
||||||
|
export class EdgeInteractionManager {
|
||||||
|
constructor(
|
||||||
|
private graph: GraphManager,
|
||||||
|
private state: GraphState
|
||||||
|
) { }
|
||||||
|
|
||||||
|
private MIN_DISTANCE = 3;
|
||||||
|
|
||||||
|
private _boundingBoxes = new Map<string, Box>();
|
||||||
|
|
||||||
|
handleMouseDown() {
|
||||||
|
this._boundingBoxes.clear();
|
||||||
|
|
||||||
|
const possibleEdges = this.graph
|
||||||
|
.getPossibleDropOnEdges(this.state.activeNodeId)
|
||||||
|
.map(e => this.graph.getEdgeId(e));
|
||||||
|
|
||||||
|
const edges = this.state.getEdges();
|
||||||
|
for (const edge of edges) {
|
||||||
|
const edgeId = edge[0];
|
||||||
|
if (!possibleEdges.includes(edgeId)) {
|
||||||
|
edges.delete(edgeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [edgeId, data] of edges) {
|
||||||
|
const points = data.points;
|
||||||
|
let minX = points[0].x + data.x1;
|
||||||
|
let maxX = points[0].x + data.x1;
|
||||||
|
let minY = points[0].z + data.y1;
|
||||||
|
let maxY = points[0].z + data.y1;
|
||||||
|
|
||||||
|
// Iterate through all points to find the true bounds
|
||||||
|
for (let i = 0; i < points.length; i++) {
|
||||||
|
const x = data.x1 + points[i].x;
|
||||||
|
const y = data.y1 + points[i].z;
|
||||||
|
if (x < minX) minX = x;
|
||||||
|
if (x > maxX) maxX = x;
|
||||||
|
if (y < minY) minY = y;
|
||||||
|
if (y > maxY) maxY = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
const boundingBox = {
|
||||||
|
minX: minX - this.MIN_DISTANCE,
|
||||||
|
maxX: maxX + this.MIN_DISTANCE,
|
||||||
|
minY: minY - this.MIN_DISTANCE,
|
||||||
|
maxY: maxY + this.MIN_DISTANCE
|
||||||
|
};
|
||||||
|
|
||||||
|
this._boundingBoxes.set(edgeId, boundingBox);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseMove() {
|
||||||
|
const [mouseX, mouseY] = this.state.mousePosition;
|
||||||
|
const hoveredEdgeIds: string[] = [];
|
||||||
|
|
||||||
|
const edges = this.state.getEdges();
|
||||||
|
|
||||||
|
// Check if mouse is inside any bounding box
|
||||||
|
for (const [edgeId, box] of this._boundingBoxes) {
|
||||||
|
const isInside = mouseX >= box.minX
|
||||||
|
&& mouseX <= box.maxX
|
||||||
|
&& mouseY >= box.minY
|
||||||
|
&& mouseY <= box.maxY;
|
||||||
|
|
||||||
|
if (isInside) {
|
||||||
|
hoveredEdgeIds.push(edgeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let hoveredEdgeId: string | null = null;
|
||||||
|
let hoveredEdgeDistance = Infinity;
|
||||||
|
|
||||||
|
const DENSITY = 10; // higher DENSITY = less points checked (yes density might not be the best name :-)
|
||||||
|
for (const edgeId of hoveredEdgeIds) {
|
||||||
|
const edge = edges.get(edgeId)!;
|
||||||
|
for (let i = 0; i < edge.points.length - DENSITY; i += DENSITY) {
|
||||||
|
const pointAx = edge.points[i].x + edge.x1;
|
||||||
|
const pointAy = edge.points[i].z + edge.y1;
|
||||||
|
const pointBx = edge.points[i + DENSITY].x + edge.x1;
|
||||||
|
const pointBy = edge.points[i + DENSITY].z + edge.y1;
|
||||||
|
const distance = distanceFromPointToSegment(pointAx, pointAy, pointBx, pointBy, mouseX, mouseY);
|
||||||
|
if (distance < this.MIN_DISTANCE) {
|
||||||
|
if (distance < hoveredEdgeDistance) {
|
||||||
|
hoveredEdgeDistance = distance;
|
||||||
|
hoveredEdgeId = edgeId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.hoveredEdgeId = hoveredEdgeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseUp() {
|
||||||
|
if (this.state.hoveredEdgeId) {
|
||||||
|
const edge = this.graph.getEdgeById(this.state.hoveredEdgeId);
|
||||||
|
if (edge) {
|
||||||
|
this.graph.dropNodeOnEdge(this.state.activeNodeId, edge);
|
||||||
|
}
|
||||||
|
this.state.hoveredEdgeId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,127 +1,23 @@
|
|||||||
import { GraphSchema, type NodeId, type NodeInstance } from "@nodarium/types";
|
import { animate, lerp } from '$lib/helpers';
|
||||||
import type { GraphManager } from "../graph-manager.svelte";
|
import { type NodeInstance } from '@nodarium/types';
|
||||||
import type { GraphState } from "../graph-state.svelte";
|
import type { GraphManager } from '../graph-manager.svelte';
|
||||||
import { animate, lerp } from "$lib/helpers";
|
import type { GraphState } from '../graph-state.svelte';
|
||||||
import { snapToGrid as snapPointToGrid } from "../helpers";
|
import { snapToGrid as snapPointToGrid } from '../helpers';
|
||||||
import { maxZoom, minZoom, zoomSpeed } from "./constants";
|
import { maxZoom, minZoom, zoomSpeed } from './constants';
|
||||||
|
import { EdgeInteractionManager } from './edge.events';
|
||||||
|
|
||||||
export class FileDropEventManager {
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private graph: GraphManager,
|
|
||||||
private state: GraphState
|
|
||||||
) { }
|
|
||||||
|
|
||||||
handleFileDrop(event: DragEvent) {
|
|
||||||
event.preventDefault();
|
|
||||||
this.state.isDragging = false;
|
|
||||||
if (!event.dataTransfer) return;
|
|
||||||
const nodeId = event.dataTransfer.getData("data/node-id") as NodeId;
|
|
||||||
let mx = event.clientX - this.state.rect.x;
|
|
||||||
let my = event.clientY - this.state.rect.y;
|
|
||||||
|
|
||||||
if (nodeId) {
|
|
||||||
let nodeOffsetX = event.dataTransfer.getData("data/node-offset-x");
|
|
||||||
let nodeOffsetY = event.dataTransfer.getData("data/node-offset-y");
|
|
||||||
if (nodeOffsetX && nodeOffsetY) {
|
|
||||||
mx += parseInt(nodeOffsetX);
|
|
||||||
my += parseInt(nodeOffsetY);
|
|
||||||
}
|
|
||||||
|
|
||||||
let props = {};
|
|
||||||
let rawNodeProps = event.dataTransfer.getData("data/node-props");
|
|
||||||
if (rawNodeProps) {
|
|
||||||
try {
|
|
||||||
props = JSON.parse(rawNodeProps);
|
|
||||||
} catch (e) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
const pos = this.state.projectScreenToWorld(mx, my);
|
|
||||||
this.graph.registry.load([nodeId]).then(() => {
|
|
||||||
this.graph.createNode({
|
|
||||||
type: nodeId,
|
|
||||||
props,
|
|
||||||
position: pos,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else if (event.dataTransfer.files.length) {
|
|
||||||
const file = event.dataTransfer.files[0];
|
|
||||||
|
|
||||||
if (file.type === "application/wasm") {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = async (e) => {
|
|
||||||
const buffer = e.target?.result;
|
|
||||||
if (buffer?.constructor === ArrayBuffer) {
|
|
||||||
const nodeType = await this.graph.registry.register(buffer);
|
|
||||||
|
|
||||||
this.graph.createNode({
|
|
||||||
type: nodeType.id,
|
|
||||||
props: {},
|
|
||||||
position: this.state.projectScreenToWorld(mx, my),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsArrayBuffer(file);
|
|
||||||
} else if (file.type === "application/json") {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e) => {
|
|
||||||
const buffer = e.target?.result as ArrayBuffer;
|
|
||||||
if (buffer) {
|
|
||||||
const state = GraphSchema.parse(JSON.parse(buffer.toString()));
|
|
||||||
this.graph.load(state);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMouseLeave() {
|
|
||||||
this.state.isDragging = false;
|
|
||||||
this.state.isPanning = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDragEnter(e: DragEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.state.isDragging = true;
|
|
||||||
this.state.isPanning = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDragOver(e: DragEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.state.isDragging = true;
|
|
||||||
this.state.isPanning = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDragEnd(e: DragEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.state.isDragging = true;
|
|
||||||
this.state.isPanning = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
getEventListenerProps() {
|
|
||||||
return {
|
|
||||||
ondragenter: (ev: DragEvent) => this.handleDragEnter(ev),
|
|
||||||
ondragover: (ev: DragEvent) => this.handleDragOver(ev),
|
|
||||||
ondragexit: (ev: DragEvent) => this.handleDragEnd(ev),
|
|
||||||
ondrop: (ev: DragEvent) => this.handleFileDrop(ev),
|
|
||||||
onmouseleave: () => this.handleMouseLeave(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export class MouseEventManager {
|
export class MouseEventManager {
|
||||||
|
edgeInteractionManager: EdgeInteractionManager;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private graph: GraphManager,
|
private graph: GraphManager,
|
||||||
private state: GraphState
|
private state: GraphState
|
||||||
) { }
|
) {
|
||||||
|
this.edgeInteractionManager = new EdgeInteractionManager(graph, state);
|
||||||
|
}
|
||||||
|
|
||||||
handleMouseUp(event: MouseEvent) {
|
handleMouseUp(event: MouseEvent) {
|
||||||
|
this.edgeInteractionManager.handleMouseUp();
|
||||||
this.state.isPanning = false;
|
this.state.isPanning = false;
|
||||||
if (!this.state.mouseDown) return;
|
if (!this.state.mouseDown) return;
|
||||||
|
|
||||||
@@ -145,25 +41,23 @@ export class MouseEventManager {
|
|||||||
const snapLevel = this.state.getSnapLevel();
|
const snapLevel = this.state.getSnapLevel();
|
||||||
activeNode.position[0] = snapPointToGrid(
|
activeNode.position[0] = snapPointToGrid(
|
||||||
activeNode?.state?.x ?? activeNode.position[0],
|
activeNode?.state?.x ?? activeNode.position[0],
|
||||||
5 / snapLevel,
|
5 / snapLevel
|
||||||
);
|
);
|
||||||
activeNode.position[1] = snapPointToGrid(
|
activeNode.position[1] = snapPointToGrid(
|
||||||
activeNode?.state?.y ?? activeNode.position[1],
|
activeNode?.state?.y ?? activeNode.position[1],
|
||||||
5 / snapLevel,
|
5 / snapLevel
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
activeNode.position[0] = activeNode?.state?.x ?? activeNode.position[0];
|
activeNode.position[0] = activeNode?.state?.x ?? activeNode.position[0];
|
||||||
activeNode.position[1] = activeNode?.state?.y ?? activeNode.position[1];
|
activeNode.position[1] = activeNode?.state?.y ?? activeNode.position[1];
|
||||||
}
|
}
|
||||||
const nodes = [
|
const nodes = [
|
||||||
...[...(this.state.selectedNodes?.values() || [])].map((id) =>
|
...[...(this.state.selectedNodes?.values() || [])].map((id) => this.graph.getNode(id))
|
||||||
this.graph.getNode(id),
|
|
||||||
),
|
|
||||||
] as NodeInstance[];
|
] as NodeInstance[];
|
||||||
|
|
||||||
const vec = [
|
const vec = [
|
||||||
activeNode.position[0] - (activeNode?.state.x || 0),
|
activeNode.position[0] - (activeNode?.state.x || 0),
|
||||||
activeNode.position[1] - (activeNode?.state.y || 0),
|
activeNode.position[1] - (activeNode?.state.y || 0)
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
@@ -179,9 +73,9 @@ export class MouseEventManager {
|
|||||||
animate(500, (a: number) => {
|
animate(500, (a: number) => {
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
if (
|
if (
|
||||||
node?.state &&
|
node?.state
|
||||||
node.state["x"] !== undefined &&
|
&& node.state['x'] !== undefined
|
||||||
node.state["y"] !== undefined
|
&& node.state['y'] !== undefined
|
||||||
) {
|
) {
|
||||||
node.state.x = lerp(node.state.x, node.position[0], a);
|
node.state.x = lerp(node.state.x, node.position[0], a);
|
||||||
node.state.y = lerp(node.state.y, node.position[1], a);
|
node.state.y = lerp(node.state.y, node.position[1], a);
|
||||||
@@ -195,24 +89,24 @@ export class MouseEventManager {
|
|||||||
this.graph.save();
|
this.graph.save();
|
||||||
} else if (this.state.hoveredSocket && this.state.activeSocket) {
|
} else if (this.state.hoveredSocket && this.state.activeSocket) {
|
||||||
if (
|
if (
|
||||||
typeof this.state.hoveredSocket.index === "number" &&
|
typeof this.state.hoveredSocket.index === 'number'
|
||||||
typeof this.state.activeSocket.index === "string"
|
&& typeof this.state.activeSocket.index === 'string'
|
||||||
) {
|
) {
|
||||||
this.graph.createEdge(
|
this.graph.createEdge(
|
||||||
this.state.hoveredSocket.node,
|
this.state.hoveredSocket.node,
|
||||||
this.state.hoveredSocket.index || 0,
|
this.state.hoveredSocket.index || 0,
|
||||||
this.state.activeSocket.node,
|
this.state.activeSocket.node,
|
||||||
this.state.activeSocket.index,
|
this.state.activeSocket.index
|
||||||
);
|
);
|
||||||
} else if (
|
} else if (
|
||||||
typeof this.state.activeSocket.index == "number" &&
|
typeof this.state.activeSocket.index == 'number'
|
||||||
typeof this.state.hoveredSocket.index === "string"
|
&& typeof this.state.hoveredSocket.index === 'string'
|
||||||
) {
|
) {
|
||||||
this.graph.createEdge(
|
this.graph.createEdge(
|
||||||
this.state.activeSocket.node,
|
this.state.activeSocket.node,
|
||||||
this.state.activeSocket.index || 0,
|
this.state.activeSocket.index || 0,
|
||||||
this.state.hoveredSocket.node,
|
this.state.hoveredSocket.node,
|
||||||
this.state.hoveredSocket.index,
|
this.state.hoveredSocket.index
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.graph.save();
|
this.graph.save();
|
||||||
@@ -220,18 +114,18 @@ export class MouseEventManager {
|
|||||||
// Handle automatic adding of nodes on ctrl+mouseUp
|
// Handle automatic adding of nodes on ctrl+mouseUp
|
||||||
this.state.edgeEndPosition = [
|
this.state.edgeEndPosition = [
|
||||||
this.state.mousePosition[0],
|
this.state.mousePosition[0],
|
||||||
this.state.mousePosition[1],
|
this.state.mousePosition[1]
|
||||||
];
|
];
|
||||||
|
|
||||||
if (typeof this.state.activeSocket.index === "number") {
|
if (typeof this.state.activeSocket.index === 'number') {
|
||||||
this.state.addMenuPosition = [
|
this.state.addMenuPosition = [
|
||||||
this.state.mousePosition[0],
|
this.state.mousePosition[0],
|
||||||
this.state.mousePosition[1] - 25 / this.state.cameraPosition[2],
|
this.state.mousePosition[1] - 25 / this.state.cameraPosition[2]
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
this.state.addMenuPosition = [
|
this.state.addMenuPosition = [
|
||||||
this.state.mousePosition[0] - 155 / this.state.cameraPosition[2],
|
this.state.mousePosition[0] - 155 / this.state.cameraPosition[2],
|
||||||
this.state.mousePosition[1] - 25 / this.state.cameraPosition[2],
|
this.state.mousePosition[1] - 25 / this.state.cameraPosition[2]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -239,11 +133,11 @@ export class MouseEventManager {
|
|||||||
|
|
||||||
// check if camera moved
|
// check if camera moved
|
||||||
if (
|
if (
|
||||||
clickedNodeId === -1 &&
|
clickedNodeId === -1
|
||||||
!this.state.boxSelection &&
|
&& !this.state.boxSelection
|
||||||
this.state.cameraDown[0] === this.state.cameraPosition[0] &&
|
&& this.state.cameraDown[0] === this.state.cameraPosition[0]
|
||||||
this.state.cameraDown[1] === this.state.cameraPosition[1] &&
|
&& this.state.cameraDown[1] === this.state.cameraPosition[1]
|
||||||
this.state.isBodyFocused()
|
&& this.state.isBodyFocused()
|
||||||
) {
|
) {
|
||||||
this.state.activeNodeId = -1;
|
this.state.activeNodeId = -1;
|
||||||
this.state.clearSelection();
|
this.state.clearSelection();
|
||||||
@@ -257,16 +151,15 @@ export class MouseEventManager {
|
|||||||
this.state.addMenuPosition = null;
|
this.state.addMenuPosition = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
handleMouseDown(event: MouseEvent) {
|
handleMouseDown(event: MouseEvent) {
|
||||||
if (this.state.mouseDown) return;
|
if (this.state.mouseDown) return;
|
||||||
this.state.edgeEndPosition = null;
|
this.state.edgeEndPosition = null;
|
||||||
|
|
||||||
if (event.target instanceof HTMLElement) {
|
if (event.target instanceof HTMLElement) {
|
||||||
if (
|
if (
|
||||||
event.target.nodeName !== "CANVAS" &&
|
event.target.nodeName !== 'CANVAS'
|
||||||
!event.target.classList.contains("node") &&
|
&& !event.target.classList.contains('node')
|
||||||
!event.target.classList.contains("content")
|
&& !event.target.classList.contains('content')
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -288,7 +181,7 @@ export class MouseEventManager {
|
|||||||
this.state.activeNodeId = clickedNodeId;
|
this.state.activeNodeId = clickedNodeId;
|
||||||
// if the selected node is the same as the clicked node
|
// if the selected node is the same as the clicked node
|
||||||
} else if (this.state.activeNodeId === clickedNodeId) {
|
} else if (this.state.activeNodeId === clickedNodeId) {
|
||||||
//$activeNodeId = -1;
|
// $activeNodeId = -1;
|
||||||
// if the clicked node is different from the selected node and secondary
|
// if the clicked node is different from the selected node and secondary
|
||||||
} else if (event.ctrlKey) {
|
} else if (event.ctrlKey) {
|
||||||
this.state.selectedNodes.add(this.state.activeNodeId);
|
this.state.selectedNodes.add(this.state.activeNodeId);
|
||||||
@@ -312,6 +205,7 @@ export class MouseEventManager {
|
|||||||
this.state.activeNodeId = clickedNodeId;
|
this.state.activeNodeId = clickedNodeId;
|
||||||
this.state.clearSelection();
|
this.state.clearSelection();
|
||||||
}
|
}
|
||||||
|
this.edgeInteractionManager.handleMouseDown();
|
||||||
} else if (event.ctrlKey) {
|
} else if (event.ctrlKey) {
|
||||||
this.state.boxSelection = true;
|
this.state.boxSelection = true;
|
||||||
}
|
}
|
||||||
@@ -335,7 +229,6 @@ export class MouseEventManager {
|
|||||||
this.state.edgeEndPosition = null;
|
this.state.edgeEndPosition = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
handleMouseMove(event: MouseEvent) {
|
handleMouseMove(event: MouseEvent) {
|
||||||
let mx = event.clientX - this.state.rect.x;
|
let mx = event.clientX - this.state.rect.x;
|
||||||
let my = event.clientY - this.state.rect.y;
|
let my = event.clientY - this.state.rect.y;
|
||||||
@@ -351,8 +244,8 @@ export class MouseEventManager {
|
|||||||
let _socket;
|
let _socket;
|
||||||
for (const socket of this.state.possibleSockets) {
|
for (const socket of this.state.possibleSockets) {
|
||||||
const dist = Math.sqrt(
|
const dist = Math.sqrt(
|
||||||
(socket.position[0] - this.state.mousePosition[0]) ** 2 +
|
(socket.position[0] - this.state.mousePosition[0]) ** 2
|
||||||
(socket.position[1] - this.state.mousePosition[1]) ** 2,
|
+ (socket.position[1] - this.state.mousePosition[1]) ** 2
|
||||||
);
|
);
|
||||||
if (dist < smallestDist) {
|
if (dist < smallestDist) {
|
||||||
smallestDist = dist;
|
smallestDist = dist;
|
||||||
@@ -375,7 +268,7 @@ export class MouseEventManager {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const mouseD = this.state.projectScreenToWorld(
|
const mouseD = this.state.projectScreenToWorld(
|
||||||
this.state.mouseDown[0],
|
this.state.mouseDown[0],
|
||||||
this.state.mouseDown[1],
|
this.state.mouseDown[1]
|
||||||
);
|
);
|
||||||
const x1 = Math.min(mouseD[0], this.state.mousePosition[0]);
|
const x1 = Math.min(mouseD[0], this.state.mousePosition[0]);
|
||||||
const x2 = Math.max(mouseD[0], this.state.mousePosition[0]);
|
const x2 = Math.max(mouseD[0], this.state.mousePosition[0]);
|
||||||
@@ -397,6 +290,7 @@ export class MouseEventManager {
|
|||||||
|
|
||||||
// here we are handling dragging of nodes
|
// here we are handling dragging of nodes
|
||||||
if (this.state.activeNodeId !== -1 && this.state.mouseDownNodeId !== -1) {
|
if (this.state.activeNodeId !== -1 && this.state.mouseDownNodeId !== -1) {
|
||||||
|
this.edgeInteractionManager.handleMouseMove();
|
||||||
const node = this.graph.getNode(this.state.activeNodeId);
|
const node = this.graph.getNode(this.state.activeNodeId);
|
||||||
if (!node || event.buttons !== 1) return;
|
if (!node || event.buttons !== 1) return;
|
||||||
|
|
||||||
@@ -405,10 +299,8 @@ export class MouseEventManager {
|
|||||||
const oldX = node.state.downX || 0;
|
const oldX = node.state.downX || 0;
|
||||||
const oldY = node.state.downY || 0;
|
const oldY = node.state.downY || 0;
|
||||||
|
|
||||||
let newX =
|
let newX = oldX + (mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
|
||||||
oldX + (mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
|
let newY = oldY + (my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
|
||||||
let newY =
|
|
||||||
oldY + (my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
|
|
||||||
|
|
||||||
if (event.ctrlKey) {
|
if (event.ctrlKey) {
|
||||||
const snapLevel = this.state.getSnapLevel();
|
const snapLevel = this.state.getSnapLevel();
|
||||||
@@ -448,23 +340,19 @@ export class MouseEventManager {
|
|||||||
|
|
||||||
// here we are handling panning of camera
|
// here we are handling panning of camera
|
||||||
this.state.isPanning = true;
|
this.state.isPanning = true;
|
||||||
let newX =
|
let newX = this.state.cameraDown[0]
|
||||||
this.state.cameraDown[0] -
|
- (mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
|
||||||
(mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
|
let newY = this.state.cameraDown[1]
|
||||||
let newY =
|
- (my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
|
||||||
this.state.cameraDown[1] -
|
|
||||||
(my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
|
|
||||||
|
|
||||||
this.state.cameraPosition[0] = newX;
|
this.state.cameraPosition[0] = newX;
|
||||||
this.state.cameraPosition[1] = newY;
|
this.state.cameraPosition[1] = newY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
handleMouseScroll(event: WheelEvent) {
|
handleMouseScroll(event: WheelEvent) {
|
||||||
const bodyIsFocused =
|
const bodyIsFocused = document.activeElement === document.body
|
||||||
document.activeElement === document.body ||
|
|| document.activeElement === this.state.wrapper
|
||||||
document.activeElement === this.state.wrapper ||
|
|| document?.activeElement?.id === 'graph';
|
||||||
document?.activeElement?.id === "graph";
|
|
||||||
if (!bodyIsFocused) return;
|
if (!bodyIsFocused) return;
|
||||||
|
|
||||||
// Define zoom speed and clamp it between -1 and 1
|
// Define zoom speed and clamp it between -1 and 1
|
||||||
@@ -479,21 +367,19 @@ export class MouseEventManager {
|
|||||||
maxZoom,
|
maxZoom,
|
||||||
isNegative
|
isNegative
|
||||||
? this.state.cameraPosition[2] / delta
|
? this.state.cameraPosition[2] / delta
|
||||||
: this.state.cameraPosition[2] * delta,
|
: this.state.cameraPosition[2] * delta
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Calculate the ratio of the new zoom to the original zoom
|
// Calculate the ratio of the new zoom to the original zoom
|
||||||
const zoomRatio = newZoom / this.state.cameraPosition[2];
|
const zoomRatio = newZoom / this.state.cameraPosition[2];
|
||||||
|
|
||||||
// Update camera position and zoom level
|
// Update camera position and zoom level
|
||||||
this.state.cameraPosition[0] = this.state.mousePosition[0] -
|
this.state.cameraPosition[0] = this.state.mousePosition[0]
|
||||||
(this.state.mousePosition[0] - this.state.cameraPosition[0]) /
|
- (this.state.mousePosition[0] - this.state.cameraPosition[0])
|
||||||
zoomRatio;
|
/ zoomRatio;
|
||||||
this.state.cameraPosition[1] = this.state.mousePosition[1] -
|
this.state.cameraPosition[1] = this.state.mousePosition[1]
|
||||||
(this.state.mousePosition[1] - this.state.cameraPosition[1]) /
|
- (this.state.mousePosition[1] - this.state.cameraPosition[1])
|
||||||
zoomRatio,
|
/ zoomRatio, this.state.cameraPosition[2] = newZoom;
|
||||||
this.state.cameraPosition[2] = newZoom;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ export function lerp(a: number, b: number, t: number) {
|
|||||||
|
|
||||||
export function animate(
|
export function animate(
|
||||||
duration: number,
|
duration: number,
|
||||||
callback: (progress: number) => void | false,
|
callback: (progress: number) => void | false
|
||||||
) {
|
) {
|
||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
const loop = (time: number) => {
|
const loop = (time: number) => {
|
||||||
@@ -33,11 +33,10 @@ export function createNodePath({
|
|||||||
cornerBottom = 0,
|
cornerBottom = 0,
|
||||||
leftBump = false,
|
leftBump = false,
|
||||||
rightBump = false,
|
rightBump = false,
|
||||||
aspectRatio = 1,
|
aspectRatio = 1
|
||||||
} = {}) {
|
} = {}) {
|
||||||
return `M0,${cornerTop}
|
return `M0,${cornerTop}
|
||||||
${
|
${cornerTop
|
||||||
cornerTop
|
|
||||||
? ` V${cornerTop}
|
? ` V${cornerTop}
|
||||||
Q0,0 ${cornerTop * aspectRatio},0
|
Q0,0 ${cornerTop * aspectRatio},0
|
||||||
H${100 - cornerTop * aspectRatio}
|
H${100 - cornerTop * aspectRatio}
|
||||||
@@ -48,13 +47,11 @@ export function createNodePath({
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
V${y - height / 2}
|
V${y - height / 2}
|
||||||
${
|
${rightBump
|
||||||
rightBump
|
|
||||||
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
|
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
|
||||||
: ` H100`
|
: ` H100`
|
||||||
}
|
}
|
||||||
${
|
${cornerBottom
|
||||||
cornerBottom
|
|
||||||
? ` V${100 - cornerBottom}
|
? ` V${100 - cornerBottom}
|
||||||
Q100,100 ${100 - cornerBottom * aspectRatio},100
|
Q100,100 ${100 - cornerBottom * aspectRatio},100
|
||||||
H${cornerBottom * aspectRatio}
|
H${cornerBottom * aspectRatio}
|
||||||
@@ -62,12 +59,11 @@ export function createNodePath({
|
|||||||
`
|
`
|
||||||
: `${leftBump ? `V100 H0` : `V100`}`
|
: `${leftBump ? `V100 H0` : `V100`}`
|
||||||
}
|
}
|
||||||
${
|
${leftBump
|
||||||
leftBump
|
|
||||||
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${y - height / 2}`
|
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${y - height / 2}`
|
||||||
: ` H0`
|
: ` H0`
|
||||||
}
|
}
|
||||||
Z`.replace(/\s+/g, " ");
|
Z`.replace(/\s+/g, ' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
export const debounce = (fn: Function, ms = 300) => {
|
export const debounce = (fn: Function, ms = 300) => {
|
||||||
@@ -78,14 +74,13 @@ export const debounce = (fn: Function, ms = 300) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const clone: <T>(v: T) => T =
|
export const clone: <T>(v: T) => T = 'structedClone' in globalThis
|
||||||
"structedClone" in globalThis
|
|
||||||
? globalThis.structuredClone
|
? globalThis.structuredClone
|
||||||
: (obj) => JSON.parse(JSON.stringify(obj));
|
: (obj) => JSON.parse(JSON.stringify(obj));
|
||||||
|
|
||||||
export function withSubComponents<A, B extends Record<string, any>>(
|
export function withSubComponents<A, B extends Record<string, any>>(
|
||||||
component: A,
|
component: A,
|
||||||
subcomponents: B,
|
subcomponents: B
|
||||||
): A & B {
|
): A & B {
|
||||||
Object.keys(subcomponents).forEach((key) => {
|
Object.keys(subcomponents).forEach((key) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -93,3 +88,27 @@ export function withSubComponents<A, B extends Record<string, any>>(
|
|||||||
});
|
});
|
||||||
return component as A & B;
|
return component as A & B;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function distanceFromPointToSegment(
|
||||||
|
x1: number,
|
||||||
|
y1: number,
|
||||||
|
x2: number,
|
||||||
|
y2: number,
|
||||||
|
x0: number,
|
||||||
|
y0: number
|
||||||
|
): number {
|
||||||
|
const dx = x2 - x1;
|
||||||
|
const dy = y2 - y1;
|
||||||
|
|
||||||
|
if (dx === 0 && dy === 0) {
|
||||||
|
return Math.hypot(x0 - x1, y0 - y1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = ((x0 - x1) * dx + (y0 - y1) * dy) / (dx * dx + dy * dy);
|
||||||
|
const clampedT = Math.max(0, Math.min(1, t));
|
||||||
|
|
||||||
|
const px = x1 + clampedT * dx;
|
||||||
|
const py = y1 + clampedT * dy;
|
||||||
|
|
||||||
|
return Math.hypot(x0 - px, y0 - py);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,18 +1,5 @@
|
|||||||
export type { NodeInput } from "./inputs";
|
export type { AsyncCache, NodeRegistry, RuntimeExecutor, SyncCache } from './components';
|
||||||
export type {
|
export type { NodeInput } from './inputs';
|
||||||
NodeRegistry,
|
export type { Box, Edge, Graph, NodeDefinition, NodeId, NodeInstance, SerializedNode, Socket } from './types';
|
||||||
RuntimeExecutor,
|
export { GraphSchema, NodeSchema } from './types';
|
||||||
SyncCache,
|
export { NodeDefinitionSchema } from './types';
|
||||||
AsyncCache,
|
|
||||||
} from "./components";
|
|
||||||
export type {
|
|
||||||
SerializedNode,
|
|
||||||
NodeInstance,
|
|
||||||
NodeDefinition,
|
|
||||||
Socket,
|
|
||||||
NodeId,
|
|
||||||
Edge,
|
|
||||||
Graph,
|
|
||||||
} from "./types";
|
|
||||||
export { NodeSchema, GraphSchema } from "./types";
|
|
||||||
export { NodeDefinitionSchema } from "./types";
|
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import { z } from "zod";
|
import { z } from 'zod';
|
||||||
import { NodeInputSchema } from "./inputs";
|
import { NodeInputSchema } from './inputs';
|
||||||
|
|
||||||
|
export type Box = {
|
||||||
|
minX: number;
|
||||||
|
maxX: number;
|
||||||
|
minY: number;
|
||||||
|
maxY: number;
|
||||||
|
};
|
||||||
|
|
||||||
export const NodeIdSchema = z
|
export const NodeIdSchema = z
|
||||||
.string()
|
.string()
|
||||||
.regex(/^[^/]+\/[^/]+\/[^/]+$/, "Invalid NodeId format")
|
.regex(/^[^/]+\/[^/]+\/[^/]+$/, 'Invalid NodeId format')
|
||||||
.transform((value) => value as `${string}/${string}/${string}`);
|
.transform((value) => value as `${string}/${string}/${string}`);
|
||||||
|
|
||||||
export type NodeId = z.infer<typeof NodeIdSchema>;
|
export type NodeId = z.infer<typeof NodeIdSchema>;
|
||||||
@@ -35,9 +42,9 @@ export const NodeDefinitionSchema = z.object({
|
|||||||
meta: z
|
meta: z
|
||||||
.object({
|
.object({
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
title: z.string().optional(),
|
title: z.string().optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const NodeSchema = z.object({
|
export const NodeSchema = z.object({
|
||||||
@@ -49,13 +56,12 @@ export const NodeSchema = z.object({
|
|||||||
meta: z
|
meta: z
|
||||||
.object({
|
.object({
|
||||||
title: z.string().optional(),
|
title: z.string().optional(),
|
||||||
lastModified: z.string().optional(),
|
lastModified: z.string().optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
position: z.tuple([z.number(), z.number()]),
|
position: z.tuple([z.number(), z.number()])
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export type SerializedNode = z.infer<typeof NodeSchema>;
|
export type SerializedNode = z.infer<typeof NodeSchema>;
|
||||||
|
|
||||||
export type NodeDefinition = z.infer<typeof NodeDefinitionSchema> & {
|
export type NodeDefinition = z.infer<typeof NodeDefinitionSchema> & {
|
||||||
@@ -75,12 +81,12 @@ export const GraphSchema = z.object({
|
|||||||
meta: z
|
meta: z
|
||||||
.object({
|
.object({
|
||||||
title: z.string().optional(),
|
title: z.string().optional(),
|
||||||
lastModified: z.string().optional(),
|
lastModified: z.string().optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
settings: z.record(z.string(), z.any()).optional(),
|
settings: z.record(z.string(), z.any()).optional(),
|
||||||
nodes: z.array(NodeSchema),
|
nodes: z.array(NodeSchema),
|
||||||
edges: z.array(z.tuple([z.number(), z.number(), z.number(), z.string()])),
|
edges: z.array(z.tuple([z.number(), z.number(), z.number(), z.string()]))
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Graph = z.infer<typeof GraphSchema>;
|
export type Graph = z.infer<typeof GraphSchema>;
|
||||||
|
|||||||
Reference in New Issue
Block a user