@@ -1,19 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { MeshLineGeometry, MeshLineMaterial } from '@threlte/extras';
|
||||
import { points, lines } from './store.js';
|
||||
import { T } from '@threlte/core';
|
||||
import { MeshLineGeometry, MeshLineMaterial } from "@threlte/extras";
|
||||
import { points, lines, rects } from "./store.js";
|
||||
import { T } from "@threlte/core";
|
||||
import { Color } from "three";
|
||||
</script>
|
||||
|
||||
{#each $points as point}
|
||||
<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.MeshBasicMaterial color="red" />
|
||||
</T.Mesh>
|
||||
<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.MeshBasicMaterial color="red" />
|
||||
</T.Mesh>
|
||||
{/each}
|
||||
|
||||
{#each $rects as rect, i}
|
||||
<T.Mesh
|
||||
position.x={(rect.minX + rect.maxX) / 2}
|
||||
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>
|
||||
<MeshLineGeometry points={line} />
|
||||
<MeshLineMaterial color="red" linewidth={1} attenuate={false} />
|
||||
</T.Mesh>
|
||||
<T.Mesh position.y={1}>
|
||||
<MeshLineGeometry points={line.points} />
|
||||
<MeshLineMaterial
|
||||
color={line.color || "red"}
|
||||
linewidth={1}
|
||||
attenuate={false}
|
||||
/>
|
||||
</T.Mesh>
|
||||
{/each}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Vector3 } from "three/src/math/Vector3.js";
|
||||
import { lines, points } from "./store";
|
||||
import type { Box } from '@nodarium/types';
|
||||
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) {
|
||||
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() {
|
||||
points.set([]);
|
||||
lines.set([]);
|
||||
rects.set([]);
|
||||
}
|
||||
|
||||
export function debugLine(line: Vector3[]) {
|
||||
export function debugLine(points: Vector3[], color?: Color) {
|
||||
lines.update((l) => {
|
||||
l.push(line);
|
||||
l.push({ points, color });
|
||||
return l;
|
||||
});
|
||||
}
|
||||
|
||||
import Component from "./Debug.svelte";
|
||||
|
||||
export default Component
|
||||
export default Component;
|
||||
export function clearLines() {
|
||||
lines.set([]);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { writable } from "svelte/store";
|
||||
import { Vector3 } from "three/src/math/Vector3.js";
|
||||
import type { Box } from '@nodarium/types';
|
||||
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 lines = writable<Vector3[][]>([]);
|
||||
export const rects = writable<Box[]>([]);
|
||||
export const lines = writable<{ points: Vector3[]; color?: Color }[]>([]);
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
import { CubicBezierCurve } from "three/src/extras/curves/CubicBezierCurve.js";
|
||||
import { Vector2 } from "three/src/math/Vector2.js";
|
||||
import { appSettings } from "$lib/settings/app-settings.svelte";
|
||||
import { getGraphState } from "../graph-state.svelte";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
const graphState = getGraphState();
|
||||
|
||||
type Props = {
|
||||
x1: number;
|
||||
@@ -38,20 +42,21 @@
|
||||
x2: number;
|
||||
y2: 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)));
|
||||
|
||||
let points = $state<Vector3[]>([]);
|
||||
|
||||
let lastId: string | null = null;
|
||||
const curveId = $derived(`${x1}-${y1}-${x2}-${y2}`);
|
||||
|
||||
function update() {
|
||||
const new_x = x2 - x1;
|
||||
const new_y = y2 - y1;
|
||||
const curveId = `${x1}-${y1}-${x2}-${y2}`;
|
||||
if (lastId === curveId) {
|
||||
return;
|
||||
}
|
||||
@@ -72,6 +77,15 @@
|
||||
.getPoints(samples)
|
||||
.map((p) => new Vector3(p.x, 0, p.y))
|
||||
.flat();
|
||||
|
||||
if (id) {
|
||||
graphState.setEdgeGeometry(
|
||||
id,
|
||||
x1,
|
||||
y1,
|
||||
$state.snapshot(points) as unknown as Vector3[],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
@@ -79,6 +93,10 @@
|
||||
update();
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (id) graphState.removeEdgeGeometry(id);
|
||||
});
|
||||
</script>
|
||||
|
||||
<T.Mesh
|
||||
@@ -101,6 +119,18 @@
|
||||
<T.CircleGeometry args={[0.5, 16]} />
|
||||
</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}>
|
||||
<MeshLineGeometry {points} />
|
||||
<MeshLineMaterial width={thickness} color={lineColor} />
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
import throttle from '$lib/helpers/throttle';
|
||||
import type {
|
||||
Edge,
|
||||
Graph,
|
||||
NodeInstance,
|
||||
NodeDefinition,
|
||||
NodeInput,
|
||||
NodeRegistry,
|
||||
NodeId,
|
||||
Socket,
|
||||
} from "@nodarium/types";
|
||||
import { fastHashString } from "@nodarium/utils";
|
||||
import { SvelteMap } from "svelte/reactivity";
|
||||
import EventEmitter from "./helpers/EventEmitter";
|
||||
import { createLogger } from "@nodarium/utils";
|
||||
import throttle from "$lib/helpers/throttle";
|
||||
import { HistoryManager } from "./history-manager";
|
||||
NodeInput,
|
||||
NodeInstance,
|
||||
NodeRegistry,
|
||||
Socket
|
||||
} from '@nodarium/types';
|
||||
import { fastHashString } from '@nodarium/utils';
|
||||
import { createLogger } from '@nodarium/utils';
|
||||
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();
|
||||
|
||||
const clone =
|
||||
"structuredClone" in self
|
||||
? self.structuredClone
|
||||
: (args: any) => JSON.parse(JSON.stringify(args));
|
||||
const clone = 'structuredClone' in self
|
||||
? self.structuredClone
|
||||
: (args: any) => JSON.parse(JSON.stringify(args));
|
||||
|
||||
function areSocketsCompatible(
|
||||
output: string | undefined,
|
||||
inputs: string | (string | undefined)[] | undefined,
|
||||
inputs: string | (string | undefined)[] | undefined
|
||||
) {
|
||||
if (Array.isArray(inputs) && output) {
|
||||
return inputs.includes(output);
|
||||
@@ -34,24 +33,23 @@ function areSocketsCompatible(
|
||||
}
|
||||
|
||||
function areEdgesEqual(firstEdge: Edge, secondEdge: Edge) {
|
||||
|
||||
if (firstEdge[0].id !== secondEdge[0].id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (firstEdge[1] !== secondEdge[1]) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
if (firstEdge[2].id !== secondEdge[2].id) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
if (firstEdge[3] !== secondEdge[3]) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
|
||||
export class GraphManager extends EventEmitter<{
|
||||
@@ -62,7 +60,7 @@ export class GraphManager extends EventEmitter<{
|
||||
values: Record<string, unknown>;
|
||||
};
|
||||
}> {
|
||||
status = $state<"loading" | "idle" | "error">();
|
||||
status = $state<'loading' | 'idle' | 'error'>();
|
||||
loaded = false;
|
||||
|
||||
graph: Graph = { id: 0, nodes: [], edges: [] };
|
||||
@@ -88,7 +86,7 @@ export class GraphManager extends EventEmitter<{
|
||||
history: HistoryManager = new HistoryManager();
|
||||
execute = throttle(() => {
|
||||
if (this.loaded === false) return;
|
||||
this.emit("result", this.serialize());
|
||||
this.emit('result', this.serialize());
|
||||
}, 10);
|
||||
|
||||
constructor(public registry: NodeRegistry) {
|
||||
@@ -100,21 +98,21 @@ export class GraphManager extends EventEmitter<{
|
||||
id: node.id,
|
||||
position: [...node.position],
|
||||
type: node.type,
|
||||
props: node.props,
|
||||
props: node.props
|
||||
})) as NodeInstance[];
|
||||
const edges = this.edges.map((edge) => [
|
||||
edge[0].id,
|
||||
edge[1],
|
||||
edge[2].id,
|
||||
edge[3],
|
||||
]) as Graph["edges"];
|
||||
edge[3]
|
||||
]) as Graph['edges'];
|
||||
const serialized = {
|
||||
id: this.graph.id,
|
||||
settings: $state.snapshot(this.settings),
|
||||
nodes,
|
||||
edges,
|
||||
edges
|
||||
};
|
||||
logger.log("serializing graph", serialized);
|
||||
logger.log('serializing graph', serialized);
|
||||
return clone($state.snapshot(serialized));
|
||||
}
|
||||
|
||||
@@ -148,6 +146,95 @@ export class GraphManager extends EventEmitter<{
|
||||
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][] {
|
||||
const edges = [];
|
||||
for (const node of nodes) {
|
||||
@@ -155,14 +242,14 @@ export class GraphManager extends EventEmitter<{
|
||||
for (const child of children) {
|
||||
if (nodes.includes(child)) {
|
||||
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) {
|
||||
edges.push([edge[0].id, edge[1], edge[2].id, edge[3]] as [
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
string,
|
||||
string
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -179,18 +266,18 @@ export class GraphManager extends EventEmitter<{
|
||||
const n = node as NodeInstance;
|
||||
if (nodeType) {
|
||||
n.state = {
|
||||
type: nodeType,
|
||||
type: nodeType
|
||||
};
|
||||
}
|
||||
return [node.id, n];
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
const edges = graph.edges.map((edge) => {
|
||||
const from = nodes.get(edge[0]);
|
||||
const to = nodes.get(edge[2]);
|
||||
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.push(to);
|
||||
@@ -214,21 +301,21 @@ export class GraphManager extends EventEmitter<{
|
||||
|
||||
this.loaded = false;
|
||||
this.graph = graph;
|
||||
this.status = "loading";
|
||||
this.status = 'loading';
|
||||
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)]));
|
||||
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) {
|
||||
const nodeType = this.registry.getNode(node.type);
|
||||
if (!nodeType) {
|
||||
logger.error(`Node type not found: ${node.type}`);
|
||||
this.status = "error";
|
||||
this.status = 'error';
|
||||
return;
|
||||
}
|
||||
// Turn into runtime node
|
||||
@@ -253,11 +340,11 @@ export class GraphManager extends EventEmitter<{
|
||||
settingTypes[settingId] = {
|
||||
__node_type: type.id,
|
||||
__node_input: key,
|
||||
...type.inputs[key],
|
||||
...type.inputs[key]
|
||||
};
|
||||
if (
|
||||
settingValues[settingId] === undefined &&
|
||||
"value" in type.inputs[key]
|
||||
settingValues[settingId] === undefined
|
||||
&& 'value' in type.inputs[key]
|
||||
) {
|
||||
settingValues[settingId] = type.inputs[key].value;
|
||||
}
|
||||
@@ -267,14 +354,14 @@ export class GraphManager extends EventEmitter<{
|
||||
}
|
||||
|
||||
this.settings = settingValues;
|
||||
this.emit("settings", { types: settingTypes, values: settingValues });
|
||||
this.emit('settings', { types: settingTypes, values: settingValues });
|
||||
|
||||
this.history.reset();
|
||||
this._init(this.graph);
|
||||
|
||||
this.save();
|
||||
|
||||
this.status = "idle";
|
||||
this.status = 'idle';
|
||||
|
||||
this.loaded = true;
|
||||
logger.log(`Graph loaded in ${performance.now() - a}ms`);
|
||||
@@ -307,9 +394,9 @@ export class GraphManager extends EventEmitter<{
|
||||
if (settingId) {
|
||||
settingTypes[settingId] = nodeType.inputs[key];
|
||||
if (
|
||||
settingValues &&
|
||||
settingValues?.[settingId] === undefined &&
|
||||
"value" in nodeType.inputs[key]
|
||||
settingValues
|
||||
&& settingValues?.[settingId] === undefined
|
||||
&& 'value' in nodeType.inputs[key]
|
||||
) {
|
||||
settingValues[settingId] = nodeType.inputs[key].value;
|
||||
}
|
||||
@@ -319,7 +406,7 @@ export class GraphManager extends EventEmitter<{
|
||||
|
||||
this.settings = settingValues;
|
||||
this.settingTypes = settingTypes;
|
||||
this.emit("settings", { types: settingTypes, values: settingValues });
|
||||
this.emit('settings', { types: settingTypes, values: settingValues });
|
||||
}
|
||||
|
||||
getChildren(node: NodeInstance) {
|
||||
@@ -368,7 +455,7 @@ export class GraphManager extends EventEmitter<{
|
||||
const inputType = to?.state?.type?.inputs?.[toSocket]?.type;
|
||||
if (outputType === inputType) {
|
||||
this.createEdge(from, fromSocket, to, toSocket, {
|
||||
applyUpdate: false,
|
||||
applyUpdate: false
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -403,7 +490,7 @@ export class GraphManager extends EventEmitter<{
|
||||
// map old ids to new ids
|
||||
const idMap = new Map<number, number>();
|
||||
|
||||
let startId = this.createNodeId()
|
||||
let startId = this.createNodeId();
|
||||
|
||||
nodes = nodes.map((node) => {
|
||||
const id = startId++;
|
||||
@@ -420,7 +507,7 @@ export class GraphManager extends EventEmitter<{
|
||||
const to = nodes.find((n) => n.id === idMap.get(edge[2]));
|
||||
|
||||
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 || [];
|
||||
@@ -445,11 +532,11 @@ export class GraphManager extends EventEmitter<{
|
||||
createNode({
|
||||
type,
|
||||
position,
|
||||
props = {},
|
||||
props = {}
|
||||
}: {
|
||||
type: NodeInstance["type"];
|
||||
position: NodeInstance["position"];
|
||||
props: NodeInstance["props"];
|
||||
type: NodeInstance['type'];
|
||||
position: NodeInstance['position'];
|
||||
props: NodeInstance['props'];
|
||||
}) {
|
||||
const nodeType = this.registry.getNode(type);
|
||||
if (!nodeType) {
|
||||
@@ -462,14 +549,14 @@ export class GraphManager extends EventEmitter<{
|
||||
type,
|
||||
position,
|
||||
state: { type: nodeType },
|
||||
props,
|
||||
props
|
||||
});
|
||||
|
||||
this.nodes.set(node.id, node);
|
||||
|
||||
this.save();
|
||||
|
||||
return node
|
||||
return node;
|
||||
}
|
||||
|
||||
createEdge(
|
||||
@@ -477,17 +564,16 @@ export class GraphManager extends EventEmitter<{
|
||||
fromSocket: number,
|
||||
to: NodeInstance,
|
||||
toSocket: string,
|
||||
{ applyUpdate = true } = {},
|
||||
{ applyUpdate = true } = {}
|
||||
): Edge | undefined {
|
||||
|
||||
const existingEdges = this.getEdgesToNode(to);
|
||||
|
||||
// check if this exact edge already exists
|
||||
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) {
|
||||
logger.error("Edge already exists", existingEdge);
|
||||
logger.error('Edge already exists', existingEdge);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -500,13 +586,13 @@ export class GraphManager extends EventEmitter<{
|
||||
|
||||
if (!areSocketsCompatible(fromSocketType, toSocketType)) {
|
||||
logger.error(
|
||||
`Socket types do not match: ${fromSocketType} !== ${toSocketType}`,
|
||||
`Socket types do not match: ${fromSocketType} !== ${toSocketType}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
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) {
|
||||
this.removeEdge(edgeToBeReplaced, { applyDeletion: false });
|
||||
@@ -533,7 +619,7 @@ export class GraphManager extends EventEmitter<{
|
||||
const nextState = this.history.undo();
|
||||
if (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();
|
||||
if (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;
|
||||
const state = this.serialize();
|
||||
this.history.save(state);
|
||||
this.emit("save", state);
|
||||
logger.log("saving graphs", state);
|
||||
this.emit('save', state);
|
||||
logger.log('saving graphs', state);
|
||||
}
|
||||
|
||||
getParentsOfNode(node: NodeInstance) {
|
||||
@@ -567,7 +653,7 @@ export class GraphManager extends EventEmitter<{
|
||||
const stack = node.state?.parents?.slice(0);
|
||||
while (stack?.length) {
|
||||
if (parents.length > 1000000) {
|
||||
logger.warn("Infinite loop detected");
|
||||
logger.warn('Infinite loop detected');
|
||||
break;
|
||||
}
|
||||
const parent = stack.pop();
|
||||
@@ -586,26 +672,28 @@ export class GraphManager extends EventEmitter<{
|
||||
return [];
|
||||
}
|
||||
|
||||
const definitions = typeof socket.index === "string"
|
||||
const definitions = typeof socket.index === 'string'
|
||||
? allDefinitions.filter(s => {
|
||||
return s.outputs?.find(_s => Object
|
||||
.values(nodeType?.inputs || {})
|
||||
.map(s => s.type)
|
||||
.includes(_s as NodeInput["type"])
|
||||
)
|
||||
return s.outputs?.find(_s =>
|
||||
Object
|
||||
.values(nodeType?.inputs || {})
|
||||
.map(s => s.type)
|
||||
.includes(_s as NodeInput['type'])
|
||||
);
|
||||
})
|
||||
: allDefinitions.filter(s => Object
|
||||
.values(s.inputs ?? {})
|
||||
.find(s => {
|
||||
if (s.hidden) return false;
|
||||
if (nodeType.outputs?.includes(s.type)) {
|
||||
return true
|
||||
}
|
||||
return s.accepts?.find(a => nodeType.outputs?.includes(a))
|
||||
}))
|
||||
|
||||
return definitions
|
||||
: allDefinitions.filter(s =>
|
||||
Object
|
||||
.values(s.inputs ?? {})
|
||||
.find(s => {
|
||||
if (s.hidden) return false;
|
||||
if (nodeType.outputs?.includes(s.type)) {
|
||||
return true;
|
||||
}
|
||||
return s.accepts?.find(a => nodeType.outputs?.includes(a));
|
||||
})
|
||||
);
|
||||
|
||||
return definitions;
|
||||
}
|
||||
|
||||
getPossibleSockets({ node, index }: Socket): [NodeInstance, string | number][] {
|
||||
@@ -615,11 +703,11 @@ export class GraphManager extends EventEmitter<{
|
||||
const sockets: [NodeInstance, string | number][] = [];
|
||||
|
||||
// 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
|
||||
const children = new Set(this.getChildren(node).map((n) => n.id));
|
||||
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;
|
||||
@@ -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
|
||||
|
||||
// filter out self and parent nodes
|
||||
const parents = new Set(this.getParentsOfNode(node).map((n) => n.id));
|
||||
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
|
||||
const edges = new Map(
|
||||
this.getEdgesFromNode(node)
|
||||
.filter((e) => e[1] === index)
|
||||
.map((e) => [e[2].id, e[3]]),
|
||||
.map((e) => [e[2].id, e[3]])
|
||||
);
|
||||
|
||||
const ownType = nodeType.outputs?.[index];
|
||||
@@ -660,8 +748,8 @@ export class GraphManager extends EventEmitter<{
|
||||
otherType.push(...(inputs[key].accepts || []));
|
||||
|
||||
if (
|
||||
areSocketsCompatible(ownType, otherType) &&
|
||||
edges.get(node.id) !== key
|
||||
areSocketsCompatible(ownType, otherType)
|
||||
&& edges.get(node.id) !== key
|
||||
) {
|
||||
sockets.push([node, key]);
|
||||
}
|
||||
@@ -674,7 +762,7 @@ export class GraphManager extends EventEmitter<{
|
||||
|
||||
removeEdge(
|
||||
edge: Edge,
|
||||
{ applyDeletion = true }: { applyDeletion?: boolean } = {},
|
||||
{ applyDeletion = true }: { applyDeletion?: boolean } = {}
|
||||
) {
|
||||
const id0 = edge[0].id;
|
||||
const sid0 = edge[1];
|
||||
@@ -682,21 +770,20 @@ export class GraphManager extends EventEmitter<{
|
||||
const sid2 = edge[3];
|
||||
|
||||
const _edge = this.edges.find(
|
||||
(e) =>
|
||||
e[0].id === id0 && e[1] === sid0 && e[2].id === id2 && e[3] === sid2,
|
||||
(e) => e[0].id === id0 && e[1] === sid0 && e[2].id === id2 && e[3] === sid2
|
||||
);
|
||||
|
||||
if (!_edge) return;
|
||||
|
||||
if (edge[0].state.children) {
|
||||
edge[0].state.children = edge[0].state.children.filter(
|
||||
(n: NodeInstance) => n.id !== id2,
|
||||
(n: NodeInstance) => n.id !== id2
|
||||
);
|
||||
}
|
||||
|
||||
if (edge[2].state.parents) {
|
||||
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.save();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
getEdgesToNode(node: NodeInstance) {
|
||||
|
||||
@@ -1,36 +1,43 @@
|
||||
import type { NodeInstance, Socket } from "@nodarium/types";
|
||||
import { getContext, setContext } from "svelte";
|
||||
import { SvelteMap, SvelteSet } from "svelte/reactivity";
|
||||
import type { GraphManager } from "./graph-manager.svelte";
|
||||
import type { Mesh, OrthographicCamera, Vector3 } from "three";
|
||||
import type { NodeInstance, Socket } from '@nodarium/types';
|
||||
import { getContext, setContext } from 'svelte';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
import type { OrthographicCamera, Vector3 } from 'three';
|
||||
import type { GraphManager } from './graph-manager.svelte';
|
||||
|
||||
|
||||
const graphStateKey = Symbol("graph-state");
|
||||
const graphStateKey = Symbol('graph-state');
|
||||
export function getGraphState() {
|
||||
return getContext<GraphState>(graphStateKey);
|
||||
}
|
||||
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() {
|
||||
return getContext<GraphManager>(graphManagerKey)
|
||||
return getContext<GraphManager>(graphManagerKey);
|
||||
}
|
||||
|
||||
export function setGraphManager(manager: GraphManager) {
|
||||
return setContext(graphManagerKey, manager);
|
||||
}
|
||||
|
||||
export class GraphState {
|
||||
type EdgeData = {
|
||||
x1: number;
|
||||
y1: number;
|
||||
points: Vector3[];
|
||||
};
|
||||
|
||||
export class GraphState {
|
||||
constructor(private graph: GraphManager) {
|
||||
$effect.root(() => {
|
||||
$effect(() => {
|
||||
localStorage.setItem("cameraPosition", `[${this.cameraPosition[0]},${this.cameraPosition[1]},${this.cameraPosition[2]}]`)
|
||||
})
|
||||
})
|
||||
const storedPosition = localStorage.getItem("cameraPosition")
|
||||
localStorage.setItem(
|
||||
'cameraPosition',
|
||||
`[${this.cameraPosition[0]},${this.cameraPosition[1]},${this.cameraPosition[2]}]`
|
||||
);
|
||||
});
|
||||
});
|
||||
const storedPosition = localStorage.getItem('cameraPosition');
|
||||
if (storedPosition) {
|
||||
try {
|
||||
const d = JSON.parse(storedPosition);
|
||||
@@ -38,7 +45,7 @@ export class GraphState {
|
||||
this.cameraPosition[1] = d[1];
|
||||
this.cameraPosition[2] = d[2];
|
||||
} catch (e) {
|
||||
console.log("Failed to parsed stored camera position", e);
|
||||
console.log('Failed to parsed stored camera position', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,15 +53,16 @@ export class GraphState {
|
||||
width = $state(100);
|
||||
height = $state(100);
|
||||
|
||||
edgeGeometries = new SvelteMap<string, { geo: Mesh, points: Vector3[] }>();
|
||||
hoveredEdgeId = $state<string | null>(null);
|
||||
edges = new Map<string, EdgeData>();
|
||||
|
||||
wrapper = $state<HTMLDivElement>(null!);
|
||||
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!);
|
||||
cameraPosition: [number, number, number] = $state([0, 0, 4]);
|
||||
cameraPosition: [number, number, number] = $state([0, 0, 100]);
|
||||
|
||||
clipboard: null | {
|
||||
nodes: NodeInstance[];
|
||||
@@ -65,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[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);
|
||||
@@ -73,8 +81,8 @@ export class GraphState {
|
||||
addMenuPosition = $state<[number, number] | null>(null);
|
||||
|
||||
snapToGrid = $state(false);
|
||||
showGrid = $state(true)
|
||||
showHelp = $state(false)
|
||||
showGrid = $state(true);
|
||||
showHelp = $state(false);
|
||||
|
||||
cameraDown = [0, 0];
|
||||
mouseDownNodeId = -1;
|
||||
@@ -90,41 +98,49 @@ export class GraphState {
|
||||
hoveredSocket = $state<Socket | null>(null);
|
||||
possibleSockets = $state<Socket[]>([]);
|
||||
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() {
|
||||
this.selectedNodes.clear();
|
||||
}
|
||||
|
||||
isBodyFocused = () => document?.activeElement?.nodeName !== "INPUT";
|
||||
isBodyFocused = () => document?.activeElement?.nodeName !== 'INPUT';
|
||||
|
||||
setEdgeGeometry(edgeId: string, edgeGeometry: { geo: Mesh, points: Vector3[] }) {
|
||||
this.edgeGeometries.set(edgeId, edgeGeometry);
|
||||
setEdgeGeometry(edgeId: string, x1: number, y1: number, points: Vector3[]) {
|
||||
this.edges.set(edgeId, { x1, y1, points });
|
||||
}
|
||||
|
||||
removeEdgeGeometry(edgeId: string) {
|
||||
this.edgeGeometries.delete(edgeId);
|
||||
this.edges.delete(edgeId);
|
||||
}
|
||||
|
||||
getEdgeData() {
|
||||
return this.edges;
|
||||
}
|
||||
|
||||
updateNodePosition(node: NodeInstance) {
|
||||
if (
|
||||
node.state.x === node.position[0] &&
|
||||
node.state.y === node.position[1]
|
||||
node.state.x === node.position[0]
|
||||
&& node.state.y === node.position[1]
|
||||
) {
|
||||
delete node.state.x;
|
||||
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) {
|
||||
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('--nx', `${node.state.x * 10}px`);
|
||||
node.state.ref.style.setProperty('--ny', `${node.state.y * 10}px`);
|
||||
}
|
||||
} else {
|
||||
if (node.state.ref) {
|
||||
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('--nx', `${node.position[0] * 10}px`);
|
||||
node.state.ref.style.setProperty('--ny', `${node.position[1] * 10}px`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,18 +160,18 @@ export class GraphState {
|
||||
|
||||
getSocketPosition(
|
||||
node: NodeInstance,
|
||||
index: string | number,
|
||||
index: string | number
|
||||
): [number, number] {
|
||||
if (typeof index === "number") {
|
||||
if (typeof index === 'number') {
|
||||
return [
|
||||
(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 {
|
||||
const _index = Object.keys(node.state?.type?.inputs || {}).indexOf(index);
|
||||
return [
|
||||
node?.state?.x ?? node.position[0],
|
||||
(node?.state?.y ?? node.position[1]) + 10 + 10 * _index,
|
||||
(node?.state?.y ?? node.position[1]) + 10 + 10 * _index
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -169,26 +185,26 @@ export class GraphState {
|
||||
if (!node?.inputs) {
|
||||
return 5;
|
||||
}
|
||||
const height =
|
||||
5 +
|
||||
10 *
|
||||
Object.keys(node.inputs).filter(
|
||||
const height = 5
|
||||
+ 10
|
||||
* Object.keys(node.inputs).filter(
|
||||
(p) =>
|
||||
p !== "seed" &&
|
||||
node?.inputs &&
|
||||
!("setting" in node?.inputs?.[p]) &&
|
||||
node.inputs[p].hidden !== true,
|
||||
p !== 'seed'
|
||||
&& node?.inputs
|
||||
&& !('setting' in node?.inputs?.[p])
|
||||
&& node.inputs[p].hidden !== true
|
||||
).length;
|
||||
this.nodeHeightCache[nodeTypeId] = height;
|
||||
return height;
|
||||
}
|
||||
|
||||
copyNodes() {
|
||||
if (this.activeNodeId === -1 && !this.selectedNodes?.size)
|
||||
if (this.activeNodeId === -1 && !this.selectedNodes?.size) {
|
||||
return;
|
||||
}
|
||||
let nodes = [
|
||||
this.activeNodeId,
|
||||
...(this.selectedNodes?.values() || []),
|
||||
...(this.selectedNodes?.values() || [])
|
||||
]
|
||||
.map((id) => this.graph.getNode(id))
|
||||
.filter(b => !!b);
|
||||
@@ -198,14 +214,14 @@ export class GraphState {
|
||||
...node,
|
||||
position: [
|
||||
this.mousePosition[0] - node.position[0],
|
||||
this.mousePosition[1] - node.position[1],
|
||||
this.mousePosition[1] - node.position[1]
|
||||
],
|
||||
tmp: undefined,
|
||||
tmp: undefined
|
||||
}));
|
||||
|
||||
this.clipboard = {
|
||||
nodes: nodes,
|
||||
edges: edges,
|
||||
edges: edges
|
||||
};
|
||||
}
|
||||
|
||||
@@ -227,14 +243,13 @@ export class GraphState {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
setDownSocket(socket: Socket) {
|
||||
this.activeSocket = socket;
|
||||
|
||||
let { node, index, position } = socket;
|
||||
|
||||
// remove existing edge
|
||||
if (typeof index === "string") {
|
||||
if (typeof index === 'string') {
|
||||
const edges = this.graph.getEdgesToNode(node);
|
||||
for (const edge of edges) {
|
||||
if (edge[3] === index) {
|
||||
@@ -251,7 +266,7 @@ export class GraphState {
|
||||
this.activeSocket = {
|
||||
node,
|
||||
index,
|
||||
position,
|
||||
position
|
||||
};
|
||||
|
||||
this.possibleSockets = this.graph
|
||||
@@ -260,18 +275,17 @@ export class GraphState {
|
||||
return {
|
||||
node,
|
||||
index,
|
||||
position: this.getSocketPosition(node, index),
|
||||
position: this.getSocketPosition(node, index)
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
projectScreenToWorld(x: number, y: number): [number, number] {
|
||||
return [
|
||||
this.cameraPosition[0] +
|
||||
(x - this.width / 2) / this.cameraPosition[2],
|
||||
this.cameraPosition[1] +
|
||||
(y - this.height / 2) / this.cameraPosition[2],
|
||||
this.cameraPosition[0]
|
||||
+ (x - this.width / 2) / this.cameraPosition[2],
|
||||
this.cameraPosition[1]
|
||||
+ (y - this.height / 2) / this.cameraPosition[2]
|
||||
];
|
||||
}
|
||||
|
||||
@@ -284,8 +298,8 @@ export class GraphState {
|
||||
if (event.button === 0) {
|
||||
// check if the clicked element is a node
|
||||
if (event.target instanceof HTMLElement) {
|
||||
const nodeElement = event.target.closest(".node");
|
||||
const nodeId = nodeElement?.getAttribute?.("data-node-id");
|
||||
const nodeElement = event.target.closest('.node');
|
||||
const nodeId = nodeElement?.getAttribute?.('data-node-id');
|
||||
if (nodeId) {
|
||||
clickedNodeId = parseInt(nodeId, 10);
|
||||
}
|
||||
@@ -313,10 +327,10 @@ export class GraphState {
|
||||
const height = this.getNodeHeight(node.type);
|
||||
const width = 20;
|
||||
return (
|
||||
node.position[0] > this.cameraBounds[0] - width &&
|
||||
node.position[0] < this.cameraBounds[1] &&
|
||||
node.position[1] > this.cameraBounds[2] - height &&
|
||||
node.position[1] < this.cameraBounds[3]
|
||||
node.position[0] > this.cameraBounds[0] - width
|
||||
&& node.position[0] < this.cameraBounds[1]
|
||||
&& node.position[1] > this.cameraBounds[2] - height
|
||||
&& node.position[1] < this.cameraBounds[3]
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,10 @@
|
||||
import HelpView from "../components/HelpView.svelte";
|
||||
import { getGraphManager, getGraphState } from "../graph-state.svelte";
|
||||
import { HTML } from "@threlte/extras";
|
||||
import { FileDropEventManager, MouseEventManager } from "./events";
|
||||
import { maxZoom, minZoom } from "./constants";
|
||||
import Debug from "../debug/Debug.svelte";
|
||||
import { FileDropEventManager } from "./drop.events";
|
||||
import { MouseEventManager } from "./mouse.events";
|
||||
|
||||
const {
|
||||
keymap,
|
||||
@@ -174,9 +176,18 @@
|
||||
|
||||
{#each graph.edges as 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}
|
||||
|
||||
<Debug />
|
||||
|
||||
<HTML transform={false}>
|
||||
<div
|
||||
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,144 +1,18 @@
|
||||
import { GraphSchema, type NodeId, type NodeInstance } from "@nodarium/types";
|
||||
import type { GraphManager } from "../graph-manager.svelte";
|
||||
import type { GraphState } from "../graph-state.svelte";
|
||||
import { animate, lerp } from "$lib/helpers";
|
||||
import { snapToGrid as snapPointToGrid } from "../helpers";
|
||||
import { maxZoom, minZoom, zoomSpeed } from "./constants";
|
||||
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class EdgeInteractionManager {
|
||||
constructor(
|
||||
private graph: GraphManager,
|
||||
private state: GraphState) { };
|
||||
|
||||
handleMouseDown() {
|
||||
const edges = this.graph.edges;
|
||||
console.log(edges)
|
||||
}
|
||||
|
||||
handleMouseMove() {
|
||||
}
|
||||
|
||||
handleMouseUp() {
|
||||
}
|
||||
}
|
||||
|
||||
import { animate, lerp } from '$lib/helpers';
|
||||
import { type NodeInstance } from '@nodarium/types';
|
||||
import type { GraphManager } from '../graph-manager.svelte';
|
||||
import type { GraphState } from '../graph-state.svelte';
|
||||
import { snapToGrid as snapPointToGrid } from '../helpers';
|
||||
import { maxZoom, minZoom, zoomSpeed } from './constants';
|
||||
import { EdgeInteractionManager } from './edge.events';
|
||||
|
||||
export class MouseEventManager {
|
||||
|
||||
edgeInteractionManager: EdgeInteractionManager
|
||||
edgeInteractionManager: EdgeInteractionManager;
|
||||
|
||||
constructor(
|
||||
private graph: GraphManager,
|
||||
private state: GraphState
|
||||
) {
|
||||
|
||||
this.edgeInteractionManager = new EdgeInteractionManager(graph, state);
|
||||
}
|
||||
|
||||
@@ -167,25 +41,23 @@ export class MouseEventManager {
|
||||
const snapLevel = this.state.getSnapLevel();
|
||||
activeNode.position[0] = snapPointToGrid(
|
||||
activeNode?.state?.x ?? activeNode.position[0],
|
||||
5 / snapLevel,
|
||||
5 / snapLevel
|
||||
);
|
||||
activeNode.position[1] = snapPointToGrid(
|
||||
activeNode?.state?.y ?? activeNode.position[1],
|
||||
5 / snapLevel,
|
||||
5 / snapLevel
|
||||
);
|
||||
} else {
|
||||
activeNode.position[0] = activeNode?.state?.x ?? activeNode.position[0];
|
||||
activeNode.position[1] = activeNode?.state?.y ?? activeNode.position[1];
|
||||
}
|
||||
const nodes = [
|
||||
...[...(this.state.selectedNodes?.values() || [])].map((id) =>
|
||||
this.graph.getNode(id),
|
||||
),
|
||||
...[...(this.state.selectedNodes?.values() || [])].map((id) => this.graph.getNode(id))
|
||||
] as NodeInstance[];
|
||||
|
||||
const vec = [
|
||||
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) {
|
||||
@@ -201,9 +73,9 @@ export class MouseEventManager {
|
||||
animate(500, (a: number) => {
|
||||
for (const node of nodes) {
|
||||
if (
|
||||
node?.state &&
|
||||
node.state["x"] !== undefined &&
|
||||
node.state["y"] !== undefined
|
||||
node?.state
|
||||
&& node.state['x'] !== undefined
|
||||
&& node.state['y'] !== undefined
|
||||
) {
|
||||
node.state.x = lerp(node.state.x, node.position[0], a);
|
||||
node.state.y = lerp(node.state.y, node.position[1], a);
|
||||
@@ -217,24 +89,24 @@ export class MouseEventManager {
|
||||
this.graph.save();
|
||||
} else if (this.state.hoveredSocket && this.state.activeSocket) {
|
||||
if (
|
||||
typeof this.state.hoveredSocket.index === "number" &&
|
||||
typeof this.state.activeSocket.index === "string"
|
||||
typeof this.state.hoveredSocket.index === 'number'
|
||||
&& typeof this.state.activeSocket.index === 'string'
|
||||
) {
|
||||
this.graph.createEdge(
|
||||
this.state.hoveredSocket.node,
|
||||
this.state.hoveredSocket.index || 0,
|
||||
this.state.activeSocket.node,
|
||||
this.state.activeSocket.index,
|
||||
this.state.activeSocket.index
|
||||
);
|
||||
} else if (
|
||||
typeof this.state.activeSocket.index == "number" &&
|
||||
typeof this.state.hoveredSocket.index === "string"
|
||||
typeof this.state.activeSocket.index == 'number'
|
||||
&& typeof this.state.hoveredSocket.index === 'string'
|
||||
) {
|
||||
this.graph.createEdge(
|
||||
this.state.activeSocket.node,
|
||||
this.state.activeSocket.index || 0,
|
||||
this.state.hoveredSocket.node,
|
||||
this.state.hoveredSocket.index,
|
||||
this.state.hoveredSocket.index
|
||||
);
|
||||
}
|
||||
this.graph.save();
|
||||
@@ -242,18 +114,18 @@ export class MouseEventManager {
|
||||
// Handle automatic adding of nodes on ctrl+mouseUp
|
||||
this.state.edgeEndPosition = [
|
||||
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.mousePosition[0],
|
||||
this.state.mousePosition[1] - 25 / this.state.cameraPosition[2],
|
||||
this.state.mousePosition[1] - 25 / this.state.cameraPosition[2]
|
||||
];
|
||||
} else {
|
||||
this.state.addMenuPosition = [
|
||||
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;
|
||||
@@ -261,11 +133,11 @@ export class MouseEventManager {
|
||||
|
||||
// check if camera moved
|
||||
if (
|
||||
clickedNodeId === -1 &&
|
||||
!this.state.boxSelection &&
|
||||
this.state.cameraDown[0] === this.state.cameraPosition[0] &&
|
||||
this.state.cameraDown[1] === this.state.cameraPosition[1] &&
|
||||
this.state.isBodyFocused()
|
||||
clickedNodeId === -1
|
||||
&& !this.state.boxSelection
|
||||
&& this.state.cameraDown[0] === this.state.cameraPosition[0]
|
||||
&& this.state.cameraDown[1] === this.state.cameraPosition[1]
|
||||
&& this.state.isBodyFocused()
|
||||
) {
|
||||
this.state.activeNodeId = -1;
|
||||
this.state.clearSelection();
|
||||
@@ -279,16 +151,15 @@ export class MouseEventManager {
|
||||
this.state.addMenuPosition = null;
|
||||
}
|
||||
|
||||
|
||||
handleMouseDown(event: MouseEvent) {
|
||||
if (this.state.mouseDown) return;
|
||||
this.state.edgeEndPosition = null;
|
||||
|
||||
if (event.target instanceof HTMLElement) {
|
||||
if (
|
||||
event.target.nodeName !== "CANVAS" &&
|
||||
!event.target.classList.contains("node") &&
|
||||
!event.target.classList.contains("content")
|
||||
event.target.nodeName !== 'CANVAS'
|
||||
&& !event.target.classList.contains('node')
|
||||
&& !event.target.classList.contains('content')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -310,7 +181,7 @@ export class MouseEventManager {
|
||||
this.state.activeNodeId = clickedNodeId;
|
||||
// if the selected node is the same as the clicked node
|
||||
} else if (this.state.activeNodeId === clickedNodeId) {
|
||||
//$activeNodeId = -1;
|
||||
// $activeNodeId = -1;
|
||||
// if the clicked node is different from the selected node and secondary
|
||||
} else if (event.ctrlKey) {
|
||||
this.state.selectedNodes.add(this.state.activeNodeId);
|
||||
@@ -358,7 +229,6 @@ export class MouseEventManager {
|
||||
this.state.edgeEndPosition = null;
|
||||
}
|
||||
|
||||
|
||||
handleMouseMove(event: MouseEvent) {
|
||||
let mx = event.clientX - this.state.rect.x;
|
||||
let my = event.clientY - this.state.rect.y;
|
||||
@@ -374,8 +244,8 @@ export class MouseEventManager {
|
||||
let _socket;
|
||||
for (const socket of this.state.possibleSockets) {
|
||||
const dist = Math.sqrt(
|
||||
(socket.position[0] - this.state.mousePosition[0]) ** 2 +
|
||||
(socket.position[1] - this.state.mousePosition[1]) ** 2,
|
||||
(socket.position[0] - this.state.mousePosition[0]) ** 2
|
||||
+ (socket.position[1] - this.state.mousePosition[1]) ** 2
|
||||
);
|
||||
if (dist < smallestDist) {
|
||||
smallestDist = dist;
|
||||
@@ -398,7 +268,7 @@ export class MouseEventManager {
|
||||
event.stopPropagation();
|
||||
const mouseD = this.state.projectScreenToWorld(
|
||||
this.state.mouseDown[0],
|
||||
this.state.mouseDown[1],
|
||||
this.state.mouseDown[1]
|
||||
);
|
||||
const x1 = Math.min(mouseD[0], this.state.mousePosition[0]);
|
||||
const x2 = Math.max(mouseD[0], this.state.mousePosition[0]);
|
||||
@@ -429,10 +299,8 @@ export class MouseEventManager {
|
||||
const oldX = node.state.downX || 0;
|
||||
const oldY = node.state.downY || 0;
|
||||
|
||||
let newX =
|
||||
oldX + (mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
|
||||
let newY =
|
||||
oldY + (my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
|
||||
let newX = oldX + (mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
|
||||
let newY = oldY + (my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
|
||||
|
||||
if (event.ctrlKey) {
|
||||
const snapLevel = this.state.getSnapLevel();
|
||||
@@ -472,23 +340,19 @@ export class MouseEventManager {
|
||||
|
||||
// here we are handling panning of camera
|
||||
this.state.isPanning = true;
|
||||
let newX =
|
||||
this.state.cameraDown[0] -
|
||||
(mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
|
||||
let newY =
|
||||
this.state.cameraDown[1] -
|
||||
(my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
|
||||
let newX = this.state.cameraDown[0]
|
||||
- (mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
|
||||
let newY = this.state.cameraDown[1]
|
||||
- (my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
|
||||
|
||||
this.state.cameraPosition[0] = newX;
|
||||
this.state.cameraPosition[1] = newY;
|
||||
}
|
||||
|
||||
|
||||
handleMouseScroll(event: WheelEvent) {
|
||||
const bodyIsFocused =
|
||||
document.activeElement === document.body ||
|
||||
document.activeElement === this.state.wrapper ||
|
||||
document?.activeElement?.id === "graph";
|
||||
const bodyIsFocused = document.activeElement === document.body
|
||||
|| document.activeElement === this.state.wrapper
|
||||
|| document?.activeElement?.id === 'graph';
|
||||
if (!bodyIsFocused) return;
|
||||
|
||||
// Define zoom speed and clamp it between -1 and 1
|
||||
@@ -503,21 +367,19 @@ export class MouseEventManager {
|
||||
maxZoom,
|
||||
isNegative
|
||||
? 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
|
||||
const zoomRatio = newZoom / this.state.cameraPosition[2];
|
||||
|
||||
// Update camera position and zoom level
|
||||
this.state.cameraPosition[0] = this.state.mousePosition[0] -
|
||||
(this.state.mousePosition[0] - this.state.cameraPosition[0]) /
|
||||
zoomRatio;
|
||||
this.state.cameraPosition[1] = this.state.mousePosition[1] -
|
||||
(this.state.mousePosition[1] - this.state.cameraPosition[1]) /
|
||||
zoomRatio,
|
||||
this.state.cameraPosition[2] = newZoom;
|
||||
this.state.cameraPosition[0] = this.state.mousePosition[0]
|
||||
- (this.state.mousePosition[0] - this.state.cameraPosition[0])
|
||||
/ zoomRatio;
|
||||
this.state.cameraPosition[1] = this.state.mousePosition[1]
|
||||
- (this.state.mousePosition[1] - this.state.cameraPosition[1])
|
||||
/ zoomRatio, this.state.cameraPosition[2] = newZoom;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,7 +8,7 @@ export function lerp(a: number, b: number, t: number) {
|
||||
|
||||
export function animate(
|
||||
duration: number,
|
||||
callback: (progress: number) => void | false,
|
||||
callback: (progress: number) => void | false
|
||||
) {
|
||||
const start = performance.now();
|
||||
const loop = (time: number) => {
|
||||
@@ -33,41 +33,37 @@ export function createNodePath({
|
||||
cornerBottom = 0,
|
||||
leftBump = false,
|
||||
rightBump = false,
|
||||
aspectRatio = 1,
|
||||
aspectRatio = 1
|
||||
} = {}) {
|
||||
return `M0,${cornerTop}
|
||||
${
|
||||
cornerTop
|
||||
? ` V${cornerTop}
|
||||
${cornerTop
|
||||
? ` V${cornerTop}
|
||||
Q0,0 ${cornerTop * aspectRatio},0
|
||||
H${100 - cornerTop * aspectRatio}
|
||||
Q100,0 100,${cornerTop}
|
||||
`
|
||||
: ` V0
|
||||
: ` 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}
|
||||
${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, " ");
|
||||
: `${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, ' ');
|
||||
}
|
||||
|
||||
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 =
|
||||
"structedClone" in globalThis
|
||||
? globalThis.structuredClone
|
||||
: (obj) => JSON.parse(JSON.stringify(obj));
|
||||
export const clone: <T>(v: T) => T = 'structedClone' in globalThis
|
||||
? globalThis.structuredClone
|
||||
: (obj) => JSON.parse(JSON.stringify(obj));
|
||||
|
||||
export function withSubComponents<A, B extends Record<string, any>>(
|
||||
component: A,
|
||||
subcomponents: B,
|
||||
subcomponents: B
|
||||
): A & B {
|
||||
Object.keys(subcomponents).forEach((key) => {
|
||||
// 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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user