feat: basic edge creation
This commit is contained in:
parent
1d6ae65630
commit
e473284797
@ -55,7 +55,7 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<T.OrthographicCamera bind:ref={camera} position.y={1} makeDefault>
|
<T.OrthographicCamera bind:ref={camera} position.y={10} makeDefault>
|
||||||
<OrbitControls
|
<OrbitControls
|
||||||
bind:ref={controls}
|
bind:ref={controls}
|
||||||
enableZoom={true}
|
enableZoom={true}
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Node } from "$lib/types";
|
import { T, extend } from "@threlte/core";
|
||||||
import { T } from "@threlte/core";
|
|
||||||
import { MeshLineGeometry, MeshLineMaterial } from "@threlte/extras";
|
import { MeshLineGeometry, MeshLineMaterial } from "@threlte/extras";
|
||||||
import { CubicBezierCurve, Vector2, Vector3 } from "three";
|
import { onMount } from "svelte";
|
||||||
|
import { CubicBezierCurve, Mesh, Vector2, Vector3 } from "three";
|
||||||
|
|
||||||
export let from: { position: { x: number; y: number } };
|
extend({ MeshLineGeometry, MeshLineMaterial });
|
||||||
export let to: { position: { x: number; y: number } };
|
|
||||||
|
|
||||||
let samples = 12;
|
export let from: { x: number; y: number };
|
||||||
|
export let to: { x: number; y: number };
|
||||||
|
|
||||||
const curve = new CubicBezierCurve(
|
const curve = new CubicBezierCurve(
|
||||||
new Vector2(from.position.x + 20, from.position.y),
|
new Vector2(from.x, from.y),
|
||||||
new Vector2(from.position.x + 2, from.position.y),
|
new Vector2(from.x + 2, from.y),
|
||||||
new Vector2(to.position.x - 2, to.position.y),
|
new Vector2(to.x - 2, to.y),
|
||||||
new Vector2(to.position.x, to.position.y),
|
new Vector2(to.x, to.y),
|
||||||
);
|
);
|
||||||
|
|
||||||
let points: Vector3[] = [];
|
let points: Vector3[] = [];
|
||||||
@ -21,32 +21,47 @@
|
|||||||
let last_from_x = 0;
|
let last_from_x = 0;
|
||||||
let last_from_y = 0;
|
let last_from_y = 0;
|
||||||
|
|
||||||
|
let mesh: Mesh;
|
||||||
|
|
||||||
function update(force = false) {
|
function update(force = false) {
|
||||||
if (!force) {
|
if (!force) {
|
||||||
const new_x = from.position.x + to.position.x;
|
const new_x = from.x + to.x;
|
||||||
const new_y = from.position.y + to.position.y;
|
const new_y = from.y + to.y;
|
||||||
if (last_from_x === new_x && last_from_y === new_y) {
|
if (last_from_x === new_x && last_from_y === new_y) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
last_from_x = new_x;
|
last_from_x = new_x;
|
||||||
last_from_y = new_y;
|
last_from_y = new_y;
|
||||||
}
|
}
|
||||||
curve.v0.set(from.position.x + 5, from.position.y + 0.65);
|
|
||||||
curve.v1.set(from.position.x + 7, from.position.y + 0.65);
|
const mid = new Vector2((from.x + to.x) / 2, (from.y + to.y) / 2);
|
||||||
curve.v2.set(to.position.x - 2, to.position.y + 2.5);
|
|
||||||
curve.v3.set(to.position.x, to.position.y + 2.5);
|
// const length = Math.sqrt(
|
||||||
|
// Math.pow(to.x - from.x, 2) + Math.pow(to.y - from.y, 2),
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// let samples = Math.max(5, Math.floor(length));
|
||||||
|
// console.log(samples);
|
||||||
|
const samples = 12;
|
||||||
|
|
||||||
|
curve.v0.set(from.x, from.y);
|
||||||
|
curve.v1.set(mid.x, from.y);
|
||||||
|
curve.v2.set(mid.x, to.y);
|
||||||
|
curve.v3.set(to.x, to.y);
|
||||||
points = curve.getPoints(samples).map((p) => new Vector3(p.x, 0, p.y));
|
points = curve.getPoints(samples).map((p) => new Vector3(p.x, 0, p.y));
|
||||||
|
// mesh.setGeometry(points);
|
||||||
|
// mesh.needsUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
update();
|
update();
|
||||||
$: if (from.position || to.position) {
|
$: if (from || to) {
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<T.Mesh
|
<T.Mesh
|
||||||
position.x={from.position.x + 5}
|
position.x={from.x}
|
||||||
position.z={from.position.y + 0.65}
|
position.z={from.y}
|
||||||
position.y={0.8}
|
position.y={0.8}
|
||||||
rotation.x={-Math.PI / 2}
|
rotation.x={-Math.PI / 2}
|
||||||
>
|
>
|
||||||
@ -55,8 +70,8 @@
|
|||||||
</T.Mesh>
|
</T.Mesh>
|
||||||
|
|
||||||
<T.Mesh
|
<T.Mesh
|
||||||
position.x={to.position.x}
|
position.x={to.x}
|
||||||
position.z={to.position.y + 2.5}
|
position.z={to.y}
|
||||||
position.y={0.8}
|
position.y={0.8}
|
||||||
rotation.x={-Math.PI / 2}
|
rotation.x={-Math.PI / 2}
|
||||||
>
|
>
|
||||||
@ -64,7 +79,7 @@
|
|||||||
<T.MeshBasicMaterial color={0x555555} />
|
<T.MeshBasicMaterial color={0x555555} />
|
||||||
</T.Mesh>
|
</T.Mesh>
|
||||||
|
|
||||||
<T.Mesh position.y={0.5}>
|
<T.Mesh position.y={0.5} bind:ref={mesh}>
|
||||||
<MeshLineGeometry {points} />
|
<MeshLineGeometry {points} />
|
||||||
<MeshLineMaterial width={1} attenuate={false} color={0x555555} />
|
<MeshLineMaterial width={2} attenuate={false} color={0x555555} />
|
||||||
</T.Mesh>
|
</T.Mesh>
|
||||||
|
@ -1,175 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Edge from "./Edge.svelte";
|
|
||||||
|
|
||||||
import { HTML } from "@threlte/extras";
|
|
||||||
import Node from "./Node.svelte";
|
|
||||||
import type { GraphManager } from "$lib/graph-manager";
|
|
||||||
import { snapToGrid } from "$lib/helpers";
|
|
||||||
import { writable, type Writable } from "svelte/store";
|
|
||||||
|
|
||||||
export let graph: GraphManager;
|
|
||||||
|
|
||||||
export let width = globalThis?.innerWidth || 100;
|
|
||||||
export let height = globalThis?.innerHeight || 100;
|
|
||||||
|
|
||||||
let edges = graph?.getEdges() || [];
|
|
||||||
|
|
||||||
export let cameraPosition: [number, number, number] = [0, 1, 0];
|
|
||||||
let cameraBounds = [-Infinity, Infinity, -Infinity, Infinity];
|
|
||||||
|
|
||||||
$: if (cameraPosition[0]) {
|
|
||||||
cameraBounds[0] = cameraPosition[0] - width / cameraPosition[2];
|
|
||||||
cameraBounds[1] = cameraPosition[0] + width / cameraPosition[2];
|
|
||||||
cameraBounds[2] = cameraPosition[1] - height / cameraPosition[2];
|
|
||||||
cameraBounds[3] = cameraPosition[1] + height / cameraPosition[2];
|
|
||||||
}
|
|
||||||
|
|
||||||
let mouseX = 0;
|
|
||||||
let mouseY = 0;
|
|
||||||
|
|
||||||
let mouseDown: Writable<false | { x: number; y: number; socket: any }> =
|
|
||||||
writable(false);
|
|
||||||
|
|
||||||
let activeNodeId: string;
|
|
||||||
|
|
||||||
function handleMouseMove(event: MouseEvent) {
|
|
||||||
if (!$mouseDown) return;
|
|
||||||
|
|
||||||
mouseX =
|
|
||||||
cameraPosition[0] + (event.clientX - width / 2) / cameraPosition[2];
|
|
||||||
mouseY =
|
|
||||||
cameraPosition[1] + (event.clientY - height / 2) / cameraPosition[2];
|
|
||||||
|
|
||||||
if (!activeNodeId) return;
|
|
||||||
|
|
||||||
const node = graph.getNode(activeNodeId);
|
|
||||||
|
|
||||||
if (!node) return;
|
|
||||||
|
|
||||||
if (!node.tmp) node.tmp = {};
|
|
||||||
node.tmp.isMoving = true;
|
|
||||||
|
|
||||||
let newX =
|
|
||||||
(node?.tmp?.downX || 0) +
|
|
||||||
(event.clientX - $mouseDown.x) / cameraPosition[2];
|
|
||||||
let newY =
|
|
||||||
(node?.tmp?.downY || 0) +
|
|
||||||
(event.clientY - $mouseDown.y) / cameraPosition[2];
|
|
||||||
|
|
||||||
if (event.ctrlKey) {
|
|
||||||
const snapLevel = getSnapLevel();
|
|
||||||
newX = snapToGrid(newX, 5 / snapLevel);
|
|
||||||
newY = snapToGrid(newY, 5 / snapLevel);
|
|
||||||
}
|
|
||||||
node.position.x = newX;
|
|
||||||
node.position.y = newY;
|
|
||||||
node.position = node.position;
|
|
||||||
edges = [...edges];
|
|
||||||
graph.nodes = [...graph.nodes];
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMouseDown(ev: MouseEvent) {
|
|
||||||
for (const node of ev.composedPath()) {
|
|
||||||
activeNodeId = (node as unknown as HTMLElement)?.getAttribute?.(
|
|
||||||
"data-node-id",
|
|
||||||
)!;
|
|
||||||
if (activeNodeId) break;
|
|
||||||
}
|
|
||||||
if (!activeNodeId) return;
|
|
||||||
|
|
||||||
$mouseDown = { x: ev.clientX, y: ev.clientY, socket: null };
|
|
||||||
const node = graph.nodes.find((node) => node.id === activeNodeId);
|
|
||||||
if (!node) return;
|
|
||||||
node.tmp = node.tmp || {};
|
|
||||||
node.tmp.downX = node.position.x;
|
|
||||||
node.tmp.downY = node.position.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSnapLevel() {
|
|
||||||
const z = cameraPosition[2];
|
|
||||||
if (z > 66) {
|
|
||||||
return 8;
|
|
||||||
} else if (z > 55) {
|
|
||||||
return 4;
|
|
||||||
} else if (z > 11) {
|
|
||||||
return 2;
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isNodeInView(node: any) {
|
|
||||||
return (
|
|
||||||
node.position.x > cameraBounds[0] &&
|
|
||||||
node.position.x < cameraBounds[1] &&
|
|
||||||
node.position.y > cameraBounds[2] &&
|
|
||||||
node.position.y < cameraBounds[3]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMouseUp() {
|
|
||||||
$mouseDown = false;
|
|
||||||
|
|
||||||
const node = graph.getNode(activeNodeId);
|
|
||||||
|
|
||||||
if (!node) return;
|
|
||||||
node.tmp = node.tmp || {};
|
|
||||||
node.tmp.isMoving = false;
|
|
||||||
const snapLevel = getSnapLevel();
|
|
||||||
node.position.x = snapToGrid(node.position.x, 5 / snapLevel);
|
|
||||||
node.position.y = snapToGrid(node.position.y, 5 / snapLevel);
|
|
||||||
|
|
||||||
graph.nodes = [...graph.nodes];
|
|
||||||
edges = [...edges];
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:document
|
|
||||||
on:mousemove={handleMouseMove}
|
|
||||||
on:mouseup={handleMouseUp}
|
|
||||||
on:mousedown={handleMouseDown}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#each edges as edge}
|
|
||||||
<Edge from={edge[0]} to={edge[1]} />
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
{#if $mouseDown && $mouseDown?.socket}
|
|
||||||
<Edge
|
|
||||||
from={{ position: $mouseDown.socket }}
|
|
||||||
to={{ position: { x: mouseX, y: mouseY } }}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<HTML transform={false}>
|
|
||||||
<div
|
|
||||||
role="tree"
|
|
||||||
tabindex="0"
|
|
||||||
class="wrapper"
|
|
||||||
class:zoom-small={cameraPosition[2] < 10}
|
|
||||||
style={`--cz: ${cameraPosition[2]}`}
|
|
||||||
>
|
|
||||||
{#each graph.nodes as node}
|
|
||||||
<Node
|
|
||||||
{node}
|
|
||||||
{graph}
|
|
||||||
inView={cameraPosition && isNodeInView(node)}
|
|
||||||
{mouseDown}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</HTML>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
:global(body) {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 100;
|
|
||||||
width: 0px;
|
|
||||||
height: 0px;
|
|
||||||
transform: scale(calc(var(--cz) * 0.1));
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,17 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { GraphManager } from "$lib/graph-manager";
|
|
||||||
import type { Node } from "$lib/types";
|
import type { Node } from "$lib/types";
|
||||||
import type { Writable } from "svelte/store";
|
|
||||||
import NodeHeader from "./NodeHeader.svelte";
|
import NodeHeader from "./NodeHeader.svelte";
|
||||||
import NodeParameter from "./NodeParameter.svelte";
|
import NodeParameter from "./NodeParameter.svelte";
|
||||||
|
import { getGraphManager } from "./graph/context";
|
||||||
|
|
||||||
export let node: Node;
|
export let node: Node;
|
||||||
export let graph: GraphManager;
|
|
||||||
|
const graph = getGraphManager();
|
||||||
|
|
||||||
export let inView = true;
|
export let inView = true;
|
||||||
|
|
||||||
export let mouseDown: Writable<false | { x: number; y: number; socket: any }>;
|
|
||||||
|
|
||||||
const type = graph.getNodeType(node.type);
|
const type = graph.getNodeType(node.type);
|
||||||
|
|
||||||
const parameters = Object.entries(type?.inputs || {});
|
const parameters = Object.entries(type?.inputs || {});
|
||||||
@ -25,13 +23,15 @@
|
|||||||
style={`--nx:${node.position.x * 10}px;
|
style={`--nx:${node.position.x * 10}px;
|
||||||
--ny: ${node.position.y * 10}px`}
|
--ny: ${node.position.y * 10}px`}
|
||||||
>
|
>
|
||||||
<NodeHeader {node} {mouseDown} />
|
<NodeHeader {node} />
|
||||||
|
|
||||||
{#each parameters as [key, value], i}
|
{#each parameters as [key, value], i}
|
||||||
<NodeParameter
|
<NodeParameter
|
||||||
|
{node}
|
||||||
|
id={key}
|
||||||
|
index={i}
|
||||||
value={node?.props?.[key]}
|
value={node?.props?.[key]}
|
||||||
input={value}
|
input={value}
|
||||||
label={key}
|
|
||||||
isLast={i == parameters.length - 1}
|
isLast={i == parameters.length - 1}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Node } from "$lib/types";
|
import type { Node } from "$lib/types";
|
||||||
import type { Writable } from "svelte/store";
|
import { getGraphManager, getGraphState } from "./graph/context";
|
||||||
|
|
||||||
export let node: Node;
|
export let node: Node;
|
||||||
|
|
||||||
export let mouseDown: Writable<false | { x: number; y: number; socket: any }>;
|
const graph = getGraphManager();
|
||||||
|
const state = getGraphState();
|
||||||
|
|
||||||
function createPath({ depth = 8, height = 20, y = 50 } = {}) {
|
function createPath({ depth = 8, height = 20, y = 50 } = {}) {
|
||||||
let corner = 10;
|
let corner = 10;
|
||||||
@ -34,21 +35,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleMouseDown(event: MouseEvent) {
|
function handleMouseDown(event: MouseEvent) {
|
||||||
$mouseDown = {
|
|
||||||
x: event.clientX,
|
|
||||||
y: event.clientY,
|
|
||||||
socket: { x: node.position.x + 5, y: node.position.y + 0.65 },
|
|
||||||
};
|
|
||||||
console.log("click");
|
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
state.setMouseDown({
|
||||||
|
x: node.position.x + 5,
|
||||||
|
y: node.position.y + 0.625,
|
||||||
|
node,
|
||||||
|
socketIndex: 0,
|
||||||
|
isInput: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="wrapper" data-node-id={node.id}>
|
<div class="wrapper" data-node-id={node.id}>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{node.type}
|
{node.type} / {node.id}
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="click-target"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
on:mousedown={handleMouseDown}
|
||||||
|
/>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 100 100"
|
viewBox="0 0 100 100"
|
||||||
@ -56,18 +64,18 @@
|
|||||||
height="100"
|
height="100"
|
||||||
preserveAspectRatio="none"
|
preserveAspectRatio="none"
|
||||||
style={`
|
style={`
|
||||||
--path: path("${createPath({ depth: 5, height: 27, y: 48.2 })}");
|
--path: path("${createPath({ depth: 5, height: 27, y: 46 })}");
|
||||||
--hover-path: path("${createPath({ depth: 6, height: 33, y: 48.2 })}");
|
--hover-path: path("${createPath({ depth: 6, height: 33, y: 46 })}");
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<ellipse
|
<!-- <ellipse -->
|
||||||
cx="100"
|
<!-- cx="100" -->
|
||||||
cy="48"
|
<!-- cy="48" -->
|
||||||
rx="2.7"
|
<!-- rx="5.4" -->
|
||||||
ry="10"
|
<!-- ry="20" -->
|
||||||
fill="red"
|
<!-- fill="rgba(255,0,0,0.3)" -->
|
||||||
on:mousedown={handleMouseDown}
|
<!-- id="one" -->
|
||||||
/>
|
<!-- /> -->
|
||||||
<path
|
<path
|
||||||
vector-effect="non-scaling-stroke"
|
vector-effect="non-scaling-stroke"
|
||||||
fill="none"
|
fill="none"
|
||||||
@ -87,6 +95,20 @@
|
|||||||
/* pointer-events: none; */
|
/* pointer-events: none; */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.click-target {
|
||||||
|
position: absolute;
|
||||||
|
right: -2.5px;
|
||||||
|
top: 4px;
|
||||||
|
height: 5px;
|
||||||
|
width: 5px;
|
||||||
|
z-index: 100;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.click-target:hover + svg path {
|
||||||
|
d: var(--hover-path);
|
||||||
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -98,6 +120,10 @@
|
|||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ellipse {
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
|
||||||
svg path {
|
svg path {
|
||||||
stroke-width: 0.2px;
|
stroke-width: 0.2px;
|
||||||
transition: 0.2s;
|
transition: 0.2s;
|
||||||
|
@ -1,12 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { NodeInput } from "$lib/types";
|
import type { NodeInput } from "$lib/types";
|
||||||
|
import type { Node } from "$lib/types";
|
||||||
|
import { getGraphState } from "./graph/context";
|
||||||
|
|
||||||
|
export let node: Node;
|
||||||
export let value: unknown;
|
export let value: unknown;
|
||||||
export let input: NodeInput;
|
export let input: NodeInput;
|
||||||
export let label: string;
|
export let id: string;
|
||||||
|
export let index: number;
|
||||||
|
|
||||||
export let isLast = false;
|
export let isLast = false;
|
||||||
|
|
||||||
|
const state = getGraphState();
|
||||||
|
|
||||||
function createPath({ depth = 8, height = 20, y = 50 } = {}) {
|
function createPath({ depth = 8, height = 20, y = 50 } = {}) {
|
||||||
let corner = isLast ? 2 : 0;
|
let corner = isLast ? 2 : 0;
|
||||||
|
|
||||||
@ -39,15 +45,29 @@
|
|||||||
}
|
}
|
||||||
Z`.replace(/\s+/g, " ");
|
Z`.replace(/\s+/g, " ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleMouseDown(ev: MouseEvent) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
state.setMouseDown({
|
||||||
|
x: node.position.x,
|
||||||
|
y: node.position.y + 2.5 + index * 2.5,
|
||||||
|
node,
|
||||||
|
socketIndex: index,
|
||||||
|
isInput: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<label>{label}</label>
|
<label>{id}</label>
|
||||||
|
|
||||||
<div class="input">input</div>
|
<div class="input">input</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="click-target" on:mousedown={handleMouseDown} />
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 100 100"
|
viewBox="0 0 100 100"
|
||||||
@ -70,6 +90,15 @@
|
|||||||
height: 25px;
|
height: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.click-target {
|
||||||
|
position: absolute;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
top: 10px;
|
||||||
|
left: -3px;
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 2px 5px;
|
padding: 2px 5px;
|
||||||
@ -83,13 +112,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:global(.zoom-small) .content {
|
:global(.zoom-small) .content {
|
||||||
/* display: none; */
|
display: none;
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
font-size: 0.5em;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
@ -126,7 +149,7 @@
|
|||||||
d: var(--path);
|
d: var(--path);
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrapper:hover svg path {
|
.click-target:hover + svg path {
|
||||||
d: var(--hover-path) !important;
|
d: var(--hover-path) !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { OrthographicCamera } from "three";
|
|
||||||
import Camera from "./Camera.svelte";
|
|
||||||
import Background from "./Background.svelte";
|
|
||||||
import type { GraphManager } from "$lib/graph-manager";
|
|
||||||
import Graph from "./Graph.svelte";
|
|
||||||
|
|
||||||
export let graph: GraphManager;
|
|
||||||
|
|
||||||
const status = graph.status;
|
|
||||||
|
|
||||||
let camera: OrthographicCamera;
|
|
||||||
let cameraPosition: [number, number, number] = [0, 1, 0];
|
|
||||||
|
|
||||||
const minZoom = 4;
|
|
||||||
const maxZoom = 150;
|
|
||||||
|
|
||||||
let width = globalThis?.innerWidth || 100;
|
|
||||||
let height = globalThis?.innerHeight || 100;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:window bind:innerHeight={height} bind:innerWidth={width} />
|
|
||||||
|
|
||||||
<Camera bind:camera {maxZoom} {minZoom} bind:position={cameraPosition} />
|
|
||||||
|
|
||||||
<Background
|
|
||||||
cx={cameraPosition[0]}
|
|
||||||
cy={cameraPosition[1]}
|
|
||||||
cz={cameraPosition[2]}
|
|
||||||
{maxZoom}
|
|
||||||
{minZoom}
|
|
||||||
{width}
|
|
||||||
{height}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#if $status === "idle"}
|
|
||||||
<Graph {graph} {cameraPosition} />
|
|
||||||
{:else if $status === "loading"}
|
|
||||||
<a href="/graph">Loading...</a>
|
|
||||||
{:else if $status === "error"}
|
|
||||||
<a href="/graph">Error</a>
|
|
||||||
{/if}
|
|
0
frontend/src/lib/components/background/index.ts
Normal file
0
frontend/src/lib/components/background/index.ts
Normal file
24
frontend/src/lib/components/debug/Debug.svelte
Normal file
24
frontend/src/lib/components/debug/Debug.svelte
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { MeshLineGeometry, MeshLineMaterial } from "@threlte/extras";
|
||||||
|
import { points, lines } from "./store";
|
||||||
|
import { T } from "@threlte/core";
|
||||||
|
</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>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#each $lines as line}
|
||||||
|
<T.Mesh>
|
||||||
|
<MeshLineGeometry points={line} />
|
||||||
|
<MeshLineMaterial color="red" linewidth={1} attenuate={false} />
|
||||||
|
</T.Mesh>
|
||||||
|
{/each}
|
26
frontend/src/lib/components/debug/index.ts
Normal file
26
frontend/src/lib/components/debug/index.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import type { Vector3 } from "three";
|
||||||
|
import { lines, points } from "./store";
|
||||||
|
|
||||||
|
export function debugPosition(pos: Vector3) {
|
||||||
|
points.update((p) => {
|
||||||
|
p.push(pos);
|
||||||
|
return p;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clear() {
|
||||||
|
points.set([]);
|
||||||
|
lines.set([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function debugLine(line: Vector3[]) {
|
||||||
|
lines.update((l) => {
|
||||||
|
l.push(line);
|
||||||
|
return l;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
import Component from "./Debug.svelte";
|
||||||
|
|
||||||
|
export default Component
|
6
frontend/src/lib/components/debug/store.ts
Normal file
6
frontend/src/lib/components/debug/store.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { writable } from "svelte/store";
|
||||||
|
import type { Vector3 } from "three";
|
||||||
|
|
||||||
|
export const points = writable<Vector3[]>([]);
|
||||||
|
|
||||||
|
export const lines = writable<Vector3[][]>([]);
|
231
frontend/src/lib/components/graph/Graph.svelte
Normal file
231
frontend/src/lib/components/graph/Graph.svelte
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Edge from "../Edge.svelte";
|
||||||
|
import { HTML } from "@threlte/extras";
|
||||||
|
import Node from "../Node.svelte";
|
||||||
|
import { snapToGrid } from "$lib/helpers";
|
||||||
|
import Debug from "../debug/Debug.svelte";
|
||||||
|
import { OrthographicCamera } from "three";
|
||||||
|
import Background from "../background/Background.svelte";
|
||||||
|
import type { GraphManager } from "$lib/graph-manager";
|
||||||
|
import { setContext } from "svelte";
|
||||||
|
import { GraphState } from "./graph-state";
|
||||||
|
import Camera from "../Camera.svelte";
|
||||||
|
import { event } from "@tauri-apps/api";
|
||||||
|
|
||||||
|
export let graph: GraphManager;
|
||||||
|
setContext("graphManager", graph);
|
||||||
|
const status = graph.status;
|
||||||
|
|
||||||
|
const state = new GraphState(graph);
|
||||||
|
setContext("graphState", state);
|
||||||
|
const mouse = state.mouse;
|
||||||
|
const dimensions = state.dimensions;
|
||||||
|
const mouseDown = state.mouseDown;
|
||||||
|
const cameraPosition = state.cameraPosition;
|
||||||
|
const cameraBounds = state.cameraBounds;
|
||||||
|
const activeNodeId = state.activeNodeId;
|
||||||
|
const hoveredSocket = state.hoveredSocket;
|
||||||
|
|
||||||
|
let camera: OrthographicCamera;
|
||||||
|
|
||||||
|
const minZoom = 4;
|
||||||
|
const maxZoom = 150;
|
||||||
|
|
||||||
|
let edges = graph?.getEdges() || [];
|
||||||
|
|
||||||
|
function handleMouseMove(event: MouseEvent) {
|
||||||
|
state.setMouseFromEvent(event);
|
||||||
|
|
||||||
|
if (!$mouseDown) return;
|
||||||
|
if (state?.possibleSockets?.length) {
|
||||||
|
let smallestDist = 1000;
|
||||||
|
let _socket;
|
||||||
|
for (const socket of state.possibleSockets) {
|
||||||
|
const posX = socket.position[0];
|
||||||
|
const posY = socket.position[1];
|
||||||
|
|
||||||
|
const dist = Math.sqrt(
|
||||||
|
(posX - $mouse[0]) ** 2 + (posY - $mouse[1]) ** 2,
|
||||||
|
);
|
||||||
|
if (dist < smallestDist) {
|
||||||
|
smallestDist = dist;
|
||||||
|
_socket = socket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_socket && smallestDist < 0.3) {
|
||||||
|
state.setMouse(_socket.position[0], _socket.position[1]);
|
||||||
|
state.hoveredSocket.set(_socket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($activeNodeId === -1) return;
|
||||||
|
|
||||||
|
const node = graph.getNode($activeNodeId);
|
||||||
|
|
||||||
|
if (!node) return;
|
||||||
|
|
||||||
|
if (!node.tmp) node.tmp = {};
|
||||||
|
node.tmp.isMoving = true;
|
||||||
|
|
||||||
|
let newX =
|
||||||
|
(node?.tmp?.downX || 0) +
|
||||||
|
(event.clientX - $mouseDown.x) / $cameraPosition[2];
|
||||||
|
let newY =
|
||||||
|
(node?.tmp?.downY || 0) +
|
||||||
|
(event.clientY - $mouseDown.y) / $cameraPosition[2];
|
||||||
|
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
const snapLevel = getSnapLevel();
|
||||||
|
newX = snapToGrid(newX, 5 / snapLevel);
|
||||||
|
newY = snapToGrid(newY, 5 / snapLevel);
|
||||||
|
}
|
||||||
|
node.position.x = newX;
|
||||||
|
node.position.y = newY;
|
||||||
|
node.position = node.position;
|
||||||
|
edges = [...edges];
|
||||||
|
graph.nodes = [...graph.nodes];
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseDown(ev: MouseEvent) {
|
||||||
|
if ($mouseDown) return;
|
||||||
|
|
||||||
|
for (const node of ev.composedPath()) {
|
||||||
|
let _activeNodeId = (node as unknown as HTMLElement)?.getAttribute?.(
|
||||||
|
"data-node-id",
|
||||||
|
)!;
|
||||||
|
if (_activeNodeId) {
|
||||||
|
$activeNodeId = parseInt(_activeNodeId, 10);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($activeNodeId < 0) return;
|
||||||
|
|
||||||
|
$mouseDown = { x: ev.clientX, y: ev.clientY };
|
||||||
|
const node = graph.nodes.find((node) => node.id === $activeNodeId);
|
||||||
|
if (!node) return;
|
||||||
|
node.tmp = node.tmp || {};
|
||||||
|
node.tmp.downX = node.position.x;
|
||||||
|
node.tmp.downY = node.position.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSnapLevel() {
|
||||||
|
const z = $cameraPosition[2];
|
||||||
|
if (z > 66) {
|
||||||
|
return 8;
|
||||||
|
} else if (z > 55) {
|
||||||
|
return 4;
|
||||||
|
} else if (z > 11) {
|
||||||
|
return 2;
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNodeInView(node: any) {
|
||||||
|
return (
|
||||||
|
node.position.x > $cameraBounds[0] &&
|
||||||
|
node.position.x < $cameraBounds[1] &&
|
||||||
|
node.position.y > $cameraBounds[2] &&
|
||||||
|
node.position.y < $cameraBounds[3]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseUp(ev: MouseEvent) {
|
||||||
|
if (ev.button !== 0) return;
|
||||||
|
|
||||||
|
const node = graph.getNode($activeNodeId);
|
||||||
|
if (node) {
|
||||||
|
node.tmp = node.tmp || {};
|
||||||
|
node.tmp.isMoving = false;
|
||||||
|
const snapLevel = getSnapLevel();
|
||||||
|
node.position.x = snapToGrid(node.position.x, 5 / snapLevel);
|
||||||
|
node.position.y = snapToGrid(node.position.y, 5 / snapLevel);
|
||||||
|
} else if ($hoveredSocket && $mouseDown && $mouseDown?.node) {
|
||||||
|
const newEdge = [
|
||||||
|
$mouseDown.node,
|
||||||
|
$mouseDown.socketIndex,
|
||||||
|
$hoveredSocket.node,
|
||||||
|
$hoveredSocket.index,
|
||||||
|
];
|
||||||
|
edges.push(newEdge);
|
||||||
|
}
|
||||||
|
|
||||||
|
$mouseDown = false;
|
||||||
|
$hoveredSocket = null;
|
||||||
|
$activeNodeId = -1;
|
||||||
|
graph.nodes = [...graph.nodes];
|
||||||
|
edges = [...edges];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:document
|
||||||
|
on:mousemove={handleMouseMove}
|
||||||
|
on:mouseup={handleMouseUp}
|
||||||
|
on:mousedown={handleMouseDown}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Debug />
|
||||||
|
|
||||||
|
<Camera bind:camera {maxZoom} {minZoom} bind:position={$cameraPosition} />
|
||||||
|
|
||||||
|
<Background
|
||||||
|
cx={$cameraPosition[0]}
|
||||||
|
cy={$cameraPosition[1]}
|
||||||
|
cz={$cameraPosition[2]}
|
||||||
|
{maxZoom}
|
||||||
|
{minZoom}
|
||||||
|
width={$dimensions[0]}
|
||||||
|
height={$dimensions[1]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if $status === "idle"}
|
||||||
|
{#each edges as edge}
|
||||||
|
<Edge
|
||||||
|
from={{
|
||||||
|
x: edge[0].position.x + 5,
|
||||||
|
y: edge[0].position.y + 0.625 + edge[1] * 2.5,
|
||||||
|
}}
|
||||||
|
to={{
|
||||||
|
x: edge[2].position.x,
|
||||||
|
y: edge[2].position.y + 2.5 + edge[3] * 2.5,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if $mouseDown && $mouseDown?.node}
|
||||||
|
<Edge from={$mouseDown} to={{ x: $mouse[0], y: $mouse[1] }} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<HTML transform={false}>
|
||||||
|
<div
|
||||||
|
role="tree"
|
||||||
|
tabindex="0"
|
||||||
|
class="wrapper"
|
||||||
|
class:zoom-small={$cameraPosition[2] < 10}
|
||||||
|
style={`--cz: ${$cameraPosition[2]}`}
|
||||||
|
>
|
||||||
|
{#each graph.nodes as node}
|
||||||
|
<Node {node} inView={$cameraPosition && isNodeInView(node)} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</HTML>
|
||||||
|
{:else if $status === "loading"}
|
||||||
|
<span>Loading</span>
|
||||||
|
{:else if $status === "error"}
|
||||||
|
<span>Error</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(body) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
width: 0px;
|
||||||
|
height: 0px;
|
||||||
|
transform: scale(calc(var(--cz) * 0.1));
|
||||||
|
}
|
||||||
|
</style>
|
11
frontend/src/lib/components/graph/context.ts
Normal file
11
frontend/src/lib/components/graph/context.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type { GraphManager } from "$lib/graph-manager";
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
import type { GraphState } from "./graph-state";
|
||||||
|
|
||||||
|
export function getGraphManager(): GraphManager {
|
||||||
|
return getContext("graphManager");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGraphState(): GraphState {
|
||||||
|
return getContext("graphState");
|
||||||
|
}
|
94
frontend/src/lib/components/graph/graph-state.ts
Normal file
94
frontend/src/lib/components/graph/graph-state.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import type { GraphManager } from "$lib/graph-manager";
|
||||||
|
import type { Node } from "$lib/types";
|
||||||
|
import { derived, get, writable, type Writable } from "svelte/store";
|
||||||
|
import * as debug from "../debug";
|
||||||
|
|
||||||
|
|
||||||
|
type Socket = {
|
||||||
|
node: Node;
|
||||||
|
index: number;
|
||||||
|
isInput: boolean;
|
||||||
|
position: [number, number];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GraphState {
|
||||||
|
|
||||||
|
activeNodeId: Writable<number> = writable(-1);
|
||||||
|
dimensions: Writable<[number, number]> = writable([100, 100]);
|
||||||
|
mouse: Writable<[number, number]> = writable([0, 0]);
|
||||||
|
mouseDown: Writable<false | { x: number, y: number, node?: Node, socketIndex?: number, isInput?: boolean }> = writable(false);
|
||||||
|
cameraPosition: Writable<[number, number, number]> = writable([0, 1, 0]);
|
||||||
|
cameraBounds = derived([this.cameraPosition, this.dimensions], ([_cameraPosition, [width, height]]) => {
|
||||||
|
return [
|
||||||
|
_cameraPosition[0] - width / _cameraPosition[2],
|
||||||
|
_cameraPosition[0] + width / _cameraPosition[2],
|
||||||
|
_cameraPosition[1] - height / _cameraPosition[2],
|
||||||
|
_cameraPosition[1] + height / _cameraPosition[2],
|
||||||
|
] as const
|
||||||
|
});
|
||||||
|
|
||||||
|
possibleSockets: Socket[] = [];
|
||||||
|
hoveredSocket: Writable<Socket | null> = writable(null);
|
||||||
|
|
||||||
|
constructor(private graph: GraphManager) {
|
||||||
|
if (globalThis?.innerWidth && globalThis?.innerHeight) {
|
||||||
|
this.dimensions.set([window.innerWidth, window.innerHeight]);
|
||||||
|
globalThis.addEventListener("resize", () => {
|
||||||
|
this.dimensions.set([window.innerWidth, window.innerHeight]);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setMouse(x: number, y: number) {
|
||||||
|
this.mouse.set([x, y]);
|
||||||
|
}
|
||||||
|
|
||||||
|
setMouseFromEvent(event: MouseEvent) {
|
||||||
|
const x = event.clientX;
|
||||||
|
const y = event.clientY;
|
||||||
|
|
||||||
|
const cameraPosition = get(this.cameraPosition);
|
||||||
|
const dimensions = get(this.dimensions);
|
||||||
|
|
||||||
|
this.mouse.set([
|
||||||
|
cameraPosition[0] + (x - dimensions[0] / 2) / cameraPosition[2],
|
||||||
|
cameraPosition[1] + (y - dimensions[1] / 2) / cameraPosition[2],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
setMouseDown(opts: { x: number, y: number, node?: Node, socketIndex?: number, isInput?: boolean } | false) {
|
||||||
|
if (!opts) {
|
||||||
|
this.mouseDown.set(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { x, y, node, socketIndex, isInput } = opts;
|
||||||
|
this.mouseDown.set({ x, y, node, socketIndex, isInput });
|
||||||
|
|
||||||
|
if (node && socketIndex !== undefined) {
|
||||||
|
|
||||||
|
debug.clear();
|
||||||
|
|
||||||
|
this.possibleSockets = this.graph.getPossibleSockets(node, socketIndex, isInput).map(([node, index]) => {
|
||||||
|
if (isInput) {
|
||||||
|
// debug.debugPosition(new Vector3(node.position.x + 5, 0, node.position.y + 0.625 + 2.5 * index));
|
||||||
|
return {
|
||||||
|
node,
|
||||||
|
index,
|
||||||
|
position: [node.position.x + 5, node.position.y + 0.625 + 2.5 * index]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// debug.debugPosition(new Vector3(node.position.x, 0, node.position.y + 2.5 + 2.5 * index));
|
||||||
|
return {
|
||||||
|
node,
|
||||||
|
index,
|
||||||
|
position: [node.position.x, node.position.y + 2.5 + 2.5 * index]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
0
frontend/src/lib/components/graph/index.ts
Normal file
0
frontend/src/lib/components/graph/index.ts
Normal file
@ -1,6 +1,5 @@
|
|||||||
import { writable, type Writable } from "svelte/store";
|
import { writable, type Writable } from "svelte/store";
|
||||||
import type { Graph, NodeRegistry as INodeRegistry, NodeType, Node } from "./types";
|
import type { Graph, NodeRegistry as INodeRegistry, NodeType, Node, Edge } from "./types";
|
||||||
import { snapToGrid } from "./helpers";
|
|
||||||
|
|
||||||
const nodeTypes: NodeType[] = [
|
const nodeTypes: NodeType[] = [
|
||||||
{
|
{
|
||||||
@ -32,7 +31,7 @@ export class GraphManager {
|
|||||||
status: Writable<"loading" | "idle" | "error"> = writable("loading");
|
status: Writable<"loading" | "idle" | "error"> = writable("loading");
|
||||||
|
|
||||||
nodes: Node[] = [];
|
nodes: Node[] = [];
|
||||||
edges: { from: string, to: string }[] = [];
|
edges: Edge[] = [];
|
||||||
|
|
||||||
private constructor(private graph: Graph, private nodeRegistry: NodeRegistry = new NodeRegistry()) {
|
private constructor(private graph: Graph, private nodeRegistry: NodeRegistry = new NodeRegistry()) {
|
||||||
}
|
}
|
||||||
@ -55,23 +54,69 @@ export class GraphManager {
|
|||||||
this.status.set("idle");
|
this.status.set("idle");
|
||||||
}
|
}
|
||||||
|
|
||||||
getNode(id: string) {
|
getNode(id: number) {
|
||||||
return this.nodes.find((node) => node.id === id);
|
return this.nodes.find((node) => node.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getNodeType(id: string): NodeType {
|
getPossibleSockets(node: Node, socketIndex: number, isInput: boolean): [Node, number][] {
|
||||||
|
|
||||||
|
const nodeType = this.getNodeType(node.type);
|
||||||
|
if (!nodeType) return [];
|
||||||
|
|
||||||
|
const nodes = this.nodes.filter(n => n.id !== node.id);
|
||||||
|
|
||||||
|
|
||||||
|
const sockets: [Node, number][] = []
|
||||||
|
if (isInput) {
|
||||||
|
|
||||||
|
|
||||||
|
const ownType = Object.values(nodeType?.inputs || {})[socketIndex].type;
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
const nodeType = this.getNodeType(node.type);
|
||||||
|
const inputs = nodeType?.outputs;
|
||||||
|
if (!inputs) continue;
|
||||||
|
for (let index = 0; index < inputs.length; index++) {
|
||||||
|
if (inputs[index] === ownType) {
|
||||||
|
sockets.push([node, index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
const ownType = nodeType.outputs?.[socketIndex];
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
const nodeType = this.getNodeType(node.type);
|
||||||
|
const inputs = nodeType?.inputs;
|
||||||
|
const entries = Object.values(inputs || {});
|
||||||
|
entries.map((input, index) => {
|
||||||
|
if (input.type === ownType) {
|
||||||
|
sockets.push([node, index]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return sockets;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
getNodeType(id: string): NodeType {
|
||||||
return this.nodeRegistry.getNode(id)!;
|
return this.nodeRegistry.getNode(id)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getEdges() {
|
getEdges() {
|
||||||
return this.edges
|
return this.edges
|
||||||
.map((edge) => {
|
.map((edge) => {
|
||||||
const from = this.nodes.find((node) => node.id === edge.from);
|
const from = this.nodes.find((node) => node.id === edge.from);
|
||||||
const to = this.nodes.find((node) => node.id === edge.to);
|
const to = this.nodes.find((node) => node.id === edge.to);
|
||||||
if (!from || !to) return;
|
if (!from || !to) return;
|
||||||
return [from, to] as const;
|
return [from, edge.fromSocket, to, edge.toSocket] as const;
|
||||||
})
|
})
|
||||||
.filter(Boolean) as unknown as [Node, Node][];
|
.filter(Boolean) as unknown as [Node, number, Node, number][];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -89,7 +134,7 @@ export class GraphManager {
|
|||||||
const y = Math.floor(i / height);
|
const y = Math.floor(i / height);
|
||||||
|
|
||||||
graph.nodes.push({
|
graph.nodes.push({
|
||||||
id: `${i.toString()}`,
|
id: i,
|
||||||
tmp: {
|
tmp: {
|
||||||
visible: false,
|
visible: false,
|
||||||
},
|
},
|
||||||
@ -102,8 +147,10 @@ export class GraphManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
graph.edges.push({
|
graph.edges.push({
|
||||||
from: i.toString(),
|
from: i,
|
||||||
to: (i + 1).toString(),
|
fromSocket: 0,
|
||||||
|
to: (i + 1),
|
||||||
|
toSocket: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
export type { NodeInput } from "./inputs";
|
||||||
|
|
||||||
export type Node = {
|
export type Node = {
|
||||||
id: string;
|
id: number;
|
||||||
type: string;
|
type: string;
|
||||||
props?: Record<string, any>,
|
props?: Record<string, any>,
|
||||||
tmp?: {
|
tmp?: {
|
||||||
@ -19,28 +20,6 @@ export type Node = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type NodeInputFloat = {
|
|
||||||
type: "float";
|
|
||||||
value?: number;
|
|
||||||
min?: number;
|
|
||||||
max?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
type NodeInputInteger = {
|
|
||||||
type: "integer";
|
|
||||||
value?: number;
|
|
||||||
min?: number;
|
|
||||||
max?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
type NodeInputSelect = {
|
|
||||||
type: "select";
|
|
||||||
value?: string;
|
|
||||||
options: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NodeInput = NodeInputFloat | NodeInputInteger | NodeInputSelect;
|
|
||||||
|
|
||||||
export type NodeType = {
|
export type NodeType = {
|
||||||
id: string;
|
id: string;
|
||||||
inputs?: Record<string, NodeInput>;
|
inputs?: Record<string, NodeInput>;
|
||||||
@ -56,8 +35,10 @@ export interface NodeRegistry {
|
|||||||
|
|
||||||
|
|
||||||
export type Edge = {
|
export type Edge = {
|
||||||
from: string;
|
from: number;
|
||||||
to: string;
|
fromSocket: number;
|
||||||
|
to: number;
|
||||||
|
toSocket: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Graph = {
|
export type Graph = {
|
21
frontend/src/lib/types/inputs.ts
Normal file
21
frontend/src/lib/types/inputs.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
type NodeInputFloat = {
|
||||||
|
type: "float";
|
||||||
|
value?: number;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeInputInteger = {
|
||||||
|
type: "integer";
|
||||||
|
value?: number;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeInputSelect = {
|
||||||
|
type: "select";
|
||||||
|
value?: string;
|
||||||
|
options: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NodeInput = NodeInputFloat | NodeInputInteger | NodeInputSelect;
|
@ -4,10 +4,10 @@
|
|||||||
import { PerfMonitor } from "@threlte/extras";
|
import { PerfMonitor } from "@threlte/extras";
|
||||||
|
|
||||||
import { Canvas } from "@threlte/core";
|
import { Canvas } from "@threlte/core";
|
||||||
import Scene from "$lib/components/Scene.svelte";
|
|
||||||
import { GraphManager } from "$lib/graph-manager";
|
import { GraphManager } from "$lib/graph-manager";
|
||||||
|
import Graph from "$lib/components/graph/Graph.svelte";
|
||||||
|
|
||||||
const graph = GraphManager.createEmptyGraph();
|
const graph = GraphManager.createEmptyGraph({ width: 3, height: 3 });
|
||||||
graph.load();
|
graph.load();
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
@ -30,7 +30,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<Canvas shadows={false} renderMode="on-demand">
|
<Canvas shadows={false} renderMode="on-demand">
|
||||||
<PerfMonitor />
|
<PerfMonitor />
|
||||||
<Scene {graph} />
|
<Graph {graph} />
|
||||||
</Canvas>
|
</Canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user