feat: basic edge creation

This commit is contained in:
max_richter 2024-03-11 19:37:58 +01:00
parent 1d6ae65630
commit e473284797
23 changed files with 604 additions and 316 deletions

View File

@ -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}

View File

@ -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>

View File

@ -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>

View File

@ -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}

View File

@ -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;

View File

@ -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>

View File

@ -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}

View 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}

View 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

View 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[][]>([]);

View 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>

View 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");
}

View 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]
}
}
});
}
}
}

View 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,
}); });
} }

View File

@ -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 = {

View 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;

View File

@ -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>