Compare commits

..

24 Commits

Author SHA1 Message Date
05b192e7ab commit to trigger deploy 2025-01-15 19:30:12 +01:00
edcaab4bd4 fix: use correct url 2025-01-15 18:17:04 +01:00
a99040f42e feat: some shit
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 47s
2024-12-20 16:35:23 +01:00
fca59e87e5 feat: some shit 2024-12-20 16:35:16 +01:00
05e8970475 feat: use remote registry
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m4s
2024-12-20 16:11:30 +01:00
385d1dd831 feat: use remote registry
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m2s
2024-12-20 16:06:18 +01:00
dc46c4b64c feat: some stuff
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m0s
2024-12-20 15:55:45 +01:00
15ff1cc52d feat: some shit
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m4s
2024-12-20 15:24:54 +01:00
a70e8195a2 feat: add some more versining stuff 2024-12-20 14:06:33 +01:00
4ca36b324b fix: some shit
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m55s
2024-12-20 13:40:14 +01:00
221817fc16 fix: some node
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m1s
2024-12-20 12:51:56 +01:00
7060b37df5 fix: error in schema
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 36s
2024-12-20 12:49:25 +01:00
ec037a3bbd fix: error in schema
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 36s
2024-12-20 12:46:44 +01:00
2814165ee6 fix: run migrations in code
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 36s
2024-12-20 12:42:45 +01:00
c6badff1ee fix: dockerfile
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 36s
2024-12-20 12:36:11 +01:00
a0d420517c feat: some shit
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 36s
2024-12-20 12:21:50 +01:00
eadd37bfa4 feat: some shit
Some checks failed
Deploy to GitHub Pages / build_site (push) Has been cancelled
2024-12-20 12:21:46 +01:00
9d698be86f fix: dockerfile
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 38s
2024-12-20 12:09:30 +01:00
540d0549d7 feat: some shit
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 37s
2024-12-19 23:55:07 +01:00
a740da1099 feat: merge svelte-5
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 38s
2024-12-19 18:31:19 +01:00
33d5ed14dd WIP 2024-12-19 18:28:17 +01:00
53f400a4f6 fix: some svelte 5 issues 2024-12-19 15:35:22 +01:00
74b7cc4232 WIP 2024-12-19 15:09:17 +01:00
5c1c8c480b feat: some shit 2024-12-17 19:22:57 +01:00
80 changed files with 3705 additions and 3194 deletions

View File

@@ -13,34 +13,34 @@
"@nodes/registry": "link:../packages/registry", "@nodes/registry": "link:../packages/registry",
"@nodes/ui": "link:../packages/ui", "@nodes/ui": "link:../packages/ui",
"@nodes/utils": "link:../packages/utils", "@nodes/utils": "link:../packages/utils",
"@sveltejs/kit": "^2.7.4", "@sveltejs/kit": "^2.12.2",
"@threlte/core": "8.0.0-next.23", "@threlte/core": "8.0.0-next.23",
"@threlte/extras": "9.0.0-next.33", "@threlte/extras": "9.0.0-next.33",
"@types/three": "^0.169.0", "@types/three": "^0.171.0",
"@unocss/reset": "^0.63.6", "@unocss/reset": "^0.65.2",
"comlink": "^4.4.1", "comlink": "^4.4.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"idb": "^8.0.0", "idb": "^8.0.1",
"jsondiffpatch": "^0.6.0", "jsondiffpatch": "^0.6.0",
"three": "^0.170.0" "three": "^0.171.0"
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/tabler": "^1.2.7", "@iconify-json/tabler": "^1.2.13",
"@nodes/types": "link:../packages/types", "@nodes/types": "link:../packages/types",
"@sveltejs/adapter-static": "^3.0.6", "@sveltejs/adapter-static": "^3.0.6",
"@sveltejs/vite-plugin-svelte": "^4.0.0", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tsconfig/svelte": "^5.0.4", "@tsconfig/svelte": "^5.0.4",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@unocss/preset-icons": "^0.63.6", "@unocss/preset-icons": "^0.65.2",
"svelte": "^5.1.9", "svelte": "^5.14.4",
"svelte-check": "^4.0.5", "svelte-check": "^4.1.1",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.6.3", "typescript": "^5.7.2",
"unocss": "^0.63.6", "unocss": "^0.65.2",
"vite": "^5.4.10", "vite": "^6.0.4",
"vite-plugin-comlink": "^5.1.0", "vite-plugin-comlink": "^5.1.0",
"vite-plugin-glsl": "^1.3.0", "vite-plugin-glsl": "^1.3.1",
"vite-plugin-wasm": "^3.3.0", "vite-plugin-wasm": "^3.3.0",
"vitest": "^2.1.4" "vitest": "^2.1.8"
} }
} }

View File

@@ -15,7 +15,7 @@
var value = JSON.parse(store); var value = JSON.parse(store);
var themes = ["dark", "light", "catppuccin"]; var themes = ["dark", "light", "catppuccin"];
if (themes[value.theme]) { if (themes[value.theme]) {
document.body.classList.add("theme-" + themes[value.theme]); document.documentElement.classList.add("theme-" + themes[value.theme]);
} }
} catch (e) { } } catch (e) { }
} }

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { NodeDefinition, NodeRegistry } from "@nodes/types"; import type { NodeDefinition, NodeRegistry } from "@nodes/types";
import { onMount } from "svelte"; import { onDestroy, onMount } from "svelte";
let mx = $state(0); let mx = $state(0);
let my = $state(0); let my = $state(0);
@@ -39,9 +39,10 @@
} }
onMount(() => { onMount(() => {
wrapper?.parentElement?.setAttribute("style", "cursor:help !important"); const style = wrapper.parentElement?.style;
style?.setProperty("cursor", "help");
return () => { return () => {
wrapper?.parentElement?.style.removeProperty("cursor"); style?.removeProperty("cursor");
}; };
}); });
</script> </script>
@@ -91,8 +92,9 @@
border-radius: 5px; border-radius: 5px;
top: 10px; top: 10px;
left: 10px; left: 10px;
max-width: 250px;
border: 1px solid var(--outline); border: 1px solid var(--outline);
z-index: 1000; z-index: 10000;
display: none; display: none;
} }

View File

@@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import { T } from "@threlte/core"; import { T } from "@threlte/core";
import BackgroundVert from "./Background.vert"; import BackgroundVert from "./Background.vert";
import BackgroundFrag from "./Background.frag"; import BackgroundFrag from "./Background.frag";
import { colors } from "../graph/state.svelte"; import { colors } from "../graph/colors.svelte";
import { Color } from "three"; import { Color } from "three";
import { appSettings } from "$lib/settings/app-settings.svelte";
type Props = { type Props = {
minZoom: number; minZoom: number;
@@ -42,10 +42,10 @@
value: [0, 1, 0], value: [0, 1, 0],
}, },
backgroundColor: { backgroundColor: {
value: new Color(0x171717), value: colors["layer-0"].clone(),
}, },
lineColor: { lineColor: {
value: new Color(0x111111), value: colors["outline"].clone(),
}, },
zoomLimits: { zoomLimits: {
value: [2, 50], value: [2, 50],
@@ -55,8 +55,9 @@
}, },
}} }}
uniforms.camPos.value={cameraPosition} uniforms.camPos.value={cameraPosition}
uniforms.backgroundColor.value={$colors["layer-0"]} uniforms.backgroundColor.value={appSettings.theme &&
uniforms.lineColor.value={$colors["outline"]} colors["layer-0"].clone()}
uniforms.lineColor.value={appSettings.theme && colors["outline"].clone()}
uniforms.zoomLimits.value={[minZoom, maxZoom]} uniforms.zoomLimits.value={[minZoom, maxZoom]}
uniforms.dimensions.value={[width, height]} uniforms.dimensions.value={[width, height]}
/> />

View File

@@ -1,13 +1,15 @@
<script module lang="ts"> <script module lang="ts">
import { colors } from "../graph/state.svelte"; import { colors } from "../graph/colors.svelte";
const circleMaterial = new MeshBasicMaterial({ const circleMaterial = new MeshBasicMaterial({
color: get(colors).edge, color: colors.edge.clone(),
toneMapped: false, toneMapped: false,
}); });
$effect.root(() => {
colors.subscribe((c) => { $effect(() => {
circleMaterial.color.copy(c.edge.clone().convertSRGBToLinear()); appSettings.theme;
circleMaterial.color = colors.edge.clone().convertSRGBToLinear();
});
}); });
const lineCache = new Map<number, BufferGeometry>(); const lineCache = new Map<number, BufferGeometry>();
@@ -27,19 +29,22 @@
import { CubicBezierCurve } from "three/src/extras/curves/CubicBezierCurve.js"; import { CubicBezierCurve } from "three/src/extras/curves/CubicBezierCurve.js";
import { Vector2 } from "three/src/math/Vector2.js"; import { Vector2 } from "three/src/math/Vector2.js";
import { createEdgeGeometry } from "./createEdgeGeometry.js"; import { createEdgeGeometry } from "./createEdgeGeometry.js";
import { get } from "svelte/store"; import { appSettings } from "$lib/settings/app-settings.svelte";
type Props = { type Props = {
from: { x: number; y: number }; from: { x: number; y: number };
to: { x: number; y: number }; to: { x: number; y: number };
z: number;
}; };
const { from, to }: Props = $props(); const { from, to, z }: Props = $props();
let samples = 5;
let geometry: BufferGeometry | null = $state(null); let geometry: BufferGeometry | null = $state(null);
const lineColor = $derived(
appSettings.theme && colors.edge.clone().convertSRGBToLinear(),
);
let lastId: number | null = null; let lastId: number | null = null;
const primeA = 31; const primeA = 31;
@@ -63,7 +68,8 @@
const length = Math.floor( const length = Math.floor(
Math.sqrt(Math.pow(new_x, 2) + Math.pow(new_y, 2)) / 4, Math.sqrt(Math.pow(new_x, 2) + Math.pow(new_y, 2)) / 4,
); );
samples = Math.min(Math.max(10, length), 60) * 2;
const samples = Math.max(length * 16, 10);
curve.v0.set(0, 0); curve.v0.set(0, 0);
curve.v1.set(mid.x, 0); curve.v1.set(mid.x, 0);
@@ -77,15 +83,13 @@
geometry = createEdgeGeometry(points); geometry = createEdgeGeometry(points);
lineCache.set(curveId, geometry); lineCache.set(curveId, geometry);
}; }
$effect(() => { $effect(() => {
if (from || to) { if (from || to) {
update(); update();
} }
}); });
const lineColor = $derived($colors.edge.clone().convertSRGBToLinear());
</script> </script>
<T.Mesh <T.Mesh
@@ -110,6 +114,6 @@
{#if geometry} {#if geometry}
<T.Mesh position.x={from.x} position.z={from.y} position.y={0.1} {geometry}> <T.Mesh position.x={from.x} position.z={from.y} position.y={0.1} {geometry}>
<MeshLineMaterial width={3} attenuate={false} color={lineColor} /> <MeshLineMaterial width={Math.max(z * 0.0001, 0.00001)} color={lineColor} />
</T.Mesh> </T.Mesh>
{/if} {/if}

View File

@@ -1,8 +1,12 @@
<script lang="ts"> <script lang="ts">
import Edge from "./Edge.svelte"; import Edge from "./Edge.svelte";
type Props = { from: { x: number; y: number }; to: { x: number; y: number } }; type Props = {
const { from, to }: Props = $props(); from: { x: number; y: number };
to: { x: number; y: number };
z: number;
};
const { from, to, z }: Props = $props();
</script> </script>
<Edge {from} {to} /> <Edge {from} {to} {z} />

View File

@@ -1,11 +1,29 @@
import { BufferGeometry, Vector3, BufferAttribute } from 'three' import { BufferAttribute, BufferGeometry, Vector3 } from 'three';
import { setXY, setXYZ, setXYZW, setXYZXYZ } from './utils.js' import { setXY, setXYZ, setXYZW, setXYZXYZ } from './utils.js';
export function createEdgeGeometry(points: Vector3[]) { export function createEdgeGeometry(points: Vector3[]) {
let shape = 'none' const length = points[0].distanceTo(points[points.length - 1]);
let shapeFunction = (p: number) => 1
const startRadius = 8;
const constantWidth = 2;
const taperFraction = 0.8 / length;
function ease(t: number) {
return t * t * (3 - 2 * t);
}
let shapeFunction = (alpha: number) => {
if (alpha < taperFraction) {
const easedAlpha = ease(alpha / taperFraction);
return startRadius + (constantWidth - startRadius) * easedAlpha;
} else if (alpha > 1 - taperFraction) {
const easedAlpha = ease((alpha - (1 - taperFraction)) / taperFraction);
return constantWidth + (startRadius - constantWidth) * easedAlpha;
} else {
return constantWidth;
}
};
// When the component first runs we create the buffer geometry and allocate the buffer attributes // When the component first runs we create the buffer geometry and allocate the buffer attributes
let pointCount = points.length let pointCount = points.length
@@ -19,9 +37,7 @@ export function createEdgeGeometry(points: Vector3[]) {
let indices: number[] = [] let indices: number[] = []
let indicesIndex = 0 let indicesIndex = 0
if (shape === 'taper') {
shapeFunction = (p: number) => 1 * Math.pow(4 * p * (1 - p), 1)
}
for (let j = 0; j < pointCount; j++) { for (let j = 0; j < pointCount; j++) {
const c = j / points.length const c = j / points.length
@@ -30,7 +46,7 @@ export function createEdgeGeometry(points: Vector3[]) {
counterIndex += 2 counterIndex += 2
setXY(side, doubleIndex, 1, -1) setXY(side, doubleIndex, 1, -1)
let width = shape === 'none' ? 1 : shapeFunction(j / (pointCount - 1)) let width = shapeFunction((j / (pointCount - 1)))
setXY(widthArray, doubleIndex, width, width) setXY(widthArray, doubleIndex, width, width)
doubleIndex += 2 doubleIndex += 2

View File

@@ -6,7 +6,6 @@
} from "../helpers/index.js"; } from "../helpers/index.js";
import type { OrthographicCamera } from "three"; import type { OrthographicCamera } from "three";
import Background from "../background/Background.svelte"; import Background from "../background/Background.svelte";
import type { GraphManager } from "../graph-manager.js";
import { getContext, onMount, setContext } from "svelte"; import { getContext, onMount, setContext } from "svelte";
import Camera from "../Camera.svelte"; import Camera from "../Camera.svelte";
import GraphView from "./GraphView.svelte"; import GraphView from "./GraphView.svelte";
@@ -23,14 +22,13 @@
import { Canvas } from "@threlte/core"; import { Canvas } from "@threlte/core";
import { getGraphManager } from "./context.js"; import { getGraphManager } from "./context.js";
const state = getGraphState(); const graphState = getGraphState();
export let snapToGrid = true; export let snapToGrid = true;
export let showGrid = true; export let showGrid = true;
export let showHelp = false; export let showHelp = false;
let keymap = const keymap = getContext<ReturnType<typeof createKeyMap>>("keymap");
getContext<ReturnType<typeof createKeyMap>>("keymap") || createKeyMap([]);
const manager = getGraphManager(); const manager = getGraphManager();
@@ -179,7 +177,7 @@
} }
setContext("setDownSocket", (socket: Socket) => { setContext("setDownSocket", (socket: Socket) => {
state.activeSocket = socket; graphState.activeSocket = socket;
let { node, index, position } = socket; let { node, index, position } = socket;
@@ -198,14 +196,14 @@
} }
mouseDown = position; mouseDown = position;
state.activeSocket = { graphState.activeSocket = {
node, node,
index, index,
position, position,
}; };
state.possibleSockets = manager graphState.possibleSockets = manager
.getPossibleSockets(state.activeSocket) .getPossibleSockets(graphState.activeSocket)
.map(([node, index]) => { .map(([node, index]) => {
return { return {
node, node,
@@ -259,14 +257,15 @@
let my = event.clientY - rect.y; let my = event.clientY - rect.y;
mousePosition = projectScreenToWorld(mx, my); mousePosition = projectScreenToWorld(mx, my);
hoveredNodeId = getNodeIdFromEvent(event);
if (!mouseDown) return; if (!mouseDown) return;
// we are creating a new edge here // we are creating a new edge here
if (state.activeSocket || state.possibleSockets?.length) { if (graphState.activeSocket || graphState.possibleSockets?.length) {
let smallestDist = 1000; let smallestDist = 1000;
let _socket; let _socket;
for (const socket of state.possibleSockets) { for (const socket of graphState.possibleSockets) {
const dist = Math.sqrt( const dist = Math.sqrt(
(socket.position[0] - mousePosition[0]) ** 2 + (socket.position[0] - mousePosition[0]) ** 2 +
(socket.position[1] - mousePosition[1]) ** 2, (socket.position[1] - mousePosition[1]) ** 2,
@@ -279,9 +278,9 @@
if (_socket && smallestDist < 0.9) { if (_socket && smallestDist < 0.9) {
mousePosition = _socket.position; mousePosition = _socket.position;
state.hoveredSocket = _socket; graphState.hoveredSocket = _socket;
} else { } else {
state.hoveredSocket = null; graphState.hoveredSocket = null;
} }
return; return;
} }
@@ -301,17 +300,17 @@
const y = node.position[1]; const y = node.position[1];
const height = getNodeHeight(node.type); const height = getNodeHeight(node.type);
if (x > x1 - 20 && x < x2 && y > y1 - height && y < y2) { if (x > x1 - 20 && x < x2 && y > y1 - height && y < y2) {
state.selectedNodes?.add(node.id); graphState.selectedNodes?.add(node.id);
} else { } else {
state.selectedNodes?.delete(node.id); graphState.selectedNodes?.delete(node.id);
} }
} }
return; return;
} }
// here we are handling dragging of nodes // here we are handling dragging of nodes
if (state.activeNodeId !== -1 && mouseDownId !== -1) { if (graphState.activeNodeId !== -1 && mouseDownId !== -1) {
const node = manager.getNode(state.activeNodeId); const node = manager.getNode(graphState.activeNodeId);
if (!node || event.buttons !== 1) return; if (!node || event.buttons !== 1) return;
node.tmp = node.tmp || {}; node.tmp = node.tmp || {};
@@ -340,8 +339,8 @@
const vecX = oldX - newX; const vecX = oldX - newX;
const vecY = oldY - newY; const vecY = oldY - newY;
if (state.selectedNodes?.size) { if (graphState.selectedNodes?.size) {
for (const nodeId of state.selectedNodes) { for (const nodeId of graphState.selectedNodes) {
const n = manager.getNode(nodeId); const n = manager.getNode(nodeId);
if (!n?.tmp) continue; if (!n?.tmp) continue;
n.tmp.x = (n?.tmp?.downX || 0) - vecX; n.tmp.x = (n?.tmp?.downX || 0) - vecX;
@@ -360,6 +359,7 @@
} }
// here we are handling panning of camera // here we are handling panning of camera
isPanning = true;
let newX = cameraDown[0] - (mx - mouseDown[0]) / cameraPosition[2]; let newX = cameraDown[0] - (mx - mouseDown[0]) / cameraPosition[2];
let newY = cameraDown[1] - (my - mouseDown[1]) / cameraPosition[2]; let newY = cameraDown[1] - (my - mouseDown[1]) / cameraPosition[2];
@@ -424,43 +424,46 @@
// if we clicked on a node // if we clicked on a node
if (clickedNodeId !== -1) { if (clickedNodeId !== -1) {
if (state.activeNodeId === -1) { if (graphState.activeNodeId === -1) {
state.activeNodeId = clickedNodeId; graphState.activeNodeId = clickedNodeId;
// if the selected node is the same as the clicked node // if the selected node is the same as the clicked node
} else if (state.activeNodeId === clickedNodeId) { } else if (graphState.activeNodeId === clickedNodeId) {
//$activeNodeId = -1; //$activeNodeId = -1;
// if the clicked node is different from the selected node and secondary // if the clicked node is different from the selected node and secondary
} else if (event.ctrlKey) { } else if (event.ctrlKey) {
state.selectedNodes = state.selectedNodes || new Set(); graphState.selectedNodes.add(graphState.activeNodeId);
state.selectedNodes.add(state.activeNodeId); graphState.selectedNodes.delete(clickedNodeId);
state.selectedNodes.delete(clickedNodeId); graphState.activeNodeId = clickedNodeId;
state.activeNodeId = clickedNodeId;
// select the node // select the node
} else if (event.shiftKey) { } else if (event.shiftKey) {
const activeNode = manager.getNode(state.activeNodeId); const activeNode = manager.getNode(graphState.activeNodeId);
const newNode = manager.getNode(clickedNodeId); const newNode = manager.getNode(clickedNodeId);
if (activeNode && newNode) { if (activeNode && newNode) {
const edge = manager.getNodesBetween(activeNode, newNode); const edge = manager.getNodesBetween(activeNode, newNode);
if (edge) { if (edge) {
const selected = new Set(edge.map((n) => n.id)); graphState.selectedNodes.clear();
selected.add(clickedNodeId); for (const node of edge) {
state.selectedNodes = selected; graphState.selectedNodes.add(node.id);
}
graphState.selectedNodes.add(clickedNodeId);
} }
} }
} else if (!state.selectedNodes?.has(clickedNodeId)) { } else if (!graphState.selectedNodes.has(clickedNodeId)) {
state.activeNodeId = clickedNodeId; graphState.activeNodeId = clickedNodeId;
state.clearSelection(); graphState.clearSelection();
} }
} else if (event.ctrlKey) { } else if (event.ctrlKey) {
boxSelection = true; boxSelection = true;
} }
const node = manager.getNode(state.activeNodeId);
const node = manager.getNode(graphState.activeNodeId);
if (!node) return; if (!node) return;
node.tmp = node.tmp || {}; node.tmp = node.tmp || {};
node.tmp.downX = node.position[0]; node.tmp.downX = node.position[0];
node.tmp.downY = node.position[1]; node.tmp.downY = node.position[1];
if (state.selectedNodes) {
for (const nodeId of state.selectedNodes) { if (graphState.selectedNodes) {
for (const nodeId of graphState.selectedNodes) {
const n = manager.getNode(nodeId); const n = manager.getNode(nodeId);
if (!n) continue; if (!n) continue;
n.tmp = n.tmp || {}; n.tmp = n.tmp || {};
@@ -471,8 +474,12 @@
} }
function copyNodes() { function copyNodes() {
if (state.activeNodeId === -1 && !state.selectedNodes?.size) return; if (graphState.activeNodeId === -1 && !graphState.selectedNodes?.size)
let _nodes = [state.activeNodeId, ...(state.selectedNodes?.values() || [])] return;
let _nodes = [
graphState.activeNodeId,
...(graphState.selectedNodes?.values() || []),
]
.map((id) => manager.getNode(id)) .map((id) => manager.getNode(id))
.filter(Boolean) as Node[]; .filter(Boolean) as Node[];
@@ -508,7 +515,10 @@
.filter(Boolean) as Node[]; .filter(Boolean) as Node[];
const newNodes = manager.createGraph(_nodes, clipboard.edges); const newNodes = manager.createGraph(_nodes, clipboard.edges);
state.selectedNodes = new Set(newNodes.map((n) => n.id)); graphState.selectedNodes.clear();
for (const node of newNodes) {
graphState.selectedNodes.add(node.id);
}
} }
const isBodyFocused = () => document?.activeElement?.nodeName !== "INPUT"; const isBodyFocused = () => document?.activeElement?.nodeName !== "INPUT";
@@ -517,12 +527,14 @@
key: "l", key: "l",
description: "Select linked nodes", description: "Select linked nodes",
callback: () => { callback: () => {
const activeNode = manager.getNode(state.activeNodeId); const activeNode = manager.getNode(graphState.activeNodeId);
if (activeNode) { if (activeNode) {
const nodes = manager.getLinkedNodes(activeNode); const nodes = manager.getLinkedNodes(activeNode);
state.selectedNodes = new Set(nodes.map((n) => n.id)); graphState.selectedNodes.clear();
for (const node of nodes) {
graphState.selectedNodes.add(node.id);
}
} }
console.log(activeNode);
}, },
}); });
@@ -552,8 +564,8 @@
key: "Escape", key: "Escape",
description: "Deselect nodes", description: "Deselect nodes",
callback: () => { callback: () => {
state.activeNodeId = -1; graphState.activeNodeId = -1;
state.clearSelection(); graphState.clearSelection();
(document.activeElement as HTMLElement)?.blur(); (document.activeElement as HTMLElement)?.blur();
}, },
}); });
@@ -605,7 +617,9 @@
description: "Select all nodes", description: "Select all nodes",
callback: () => { callback: () => {
if (!isBodyFocused()) return; if (!isBodyFocused()) return;
state.selectedNodes = new Set($nodes.keys()); for (const node of $nodes.keys()) {
graphState.selectedNodes.add(node);
}
}, },
}); });
@@ -654,38 +668,39 @@
callback: (event) => { callback: (event) => {
if (!isBodyFocused()) return; if (!isBodyFocused()) return;
manager.startUndoGroup(); manager.startUndoGroup();
if (state.activeNodeId !== -1) { if (graphState.activeNodeId !== -1) {
const node = manager.getNode(state.activeNodeId); const node = manager.getNode(graphState.activeNodeId);
if (node) { if (node) {
manager.removeNode(node, { restoreEdges: event.ctrlKey }); manager.removeNode(node, { restoreEdges: event.ctrlKey });
state.activeNodeId = -1; graphState.activeNodeId = -1;
} }
} }
if (state.selectedNodes) { if (graphState.selectedNodes) {
for (const nodeId of state.selectedNodes) { for (const nodeId of graphState.selectedNodes) {
const node = manager.getNode(nodeId); const node = manager.getNode(nodeId);
if (node) { if (node) {
manager.removeNode(node, { restoreEdges: event.ctrlKey }); manager.removeNode(node, { restoreEdges: event.ctrlKey });
} }
} }
state.clearSelection(); graphState.clearSelection();
} }
manager.saveUndoGroup(); manager.saveUndoGroup();
}, },
}); });
function handleMouseUp(event: MouseEvent) { function handleMouseUp(event: MouseEvent) {
isPanning = false;
if (!mouseDown) return; if (!mouseDown) return;
const activeNode = manager.getNode(state.activeNodeId); const activeNode = manager.getNode(graphState.activeNodeId);
const clickedNodeId = getNodeIdFromEvent(event); const clickedNodeId = getNodeIdFromEvent(event);
if (clickedNodeId !== -1) { if (clickedNodeId !== -1) {
if (activeNode) { if (activeNode) {
if (!activeNode?.tmp?.isMoving && !event.ctrlKey && !event.shiftKey) { if (!activeNode?.tmp?.isMoving && !event.ctrlKey && !event.shiftKey) {
state.clearSelection(); graphState.activeNodeId = clickedNodeId;
state.activeNodeId = clickedNodeId; graphState.clearSelection();
} }
} }
} }
@@ -708,7 +723,7 @@
activeNode.position[1] = activeNode?.tmp?.y ?? activeNode.position[1]; activeNode.position[1] = activeNode?.tmp?.y ?? activeNode.position[1];
} }
const nodes = [ const nodes = [
...[...(state.selectedNodes?.values() || [])].map((id) => ...[...(graphState.selectedNodes?.values() || [])].map((id) =>
manager.getNode(id), manager.getNode(id),
), ),
] as NodeType[]; ] as NodeType[];
@@ -747,26 +762,26 @@
$edges = $edges; $edges = $edges;
}); });
manager.save(); manager.save();
} else if (state.hoveredSocket && state.activeSocket) { } else if (graphState.hoveredSocket && graphState.activeSocket) {
if ( if (
typeof state.hoveredSocket.index === "number" && typeof graphState.hoveredSocket.index === "number" &&
typeof state.activeSocket.index === "string" typeof graphState.activeSocket.index === "string"
) { ) {
manager.createEdge( manager.createEdge(
state.hoveredSocket.node, graphState.hoveredSocket.node,
state.hoveredSocket.index || 0, graphState.hoveredSocket.index || 0,
state.activeSocket.node, graphState.activeSocket.node,
state.activeSocket.index, graphState.activeSocket.index,
); );
} else if ( } else if (
typeof state.activeSocket.index == "number" && typeof graphState.activeSocket.index == "number" &&
typeof state.hoveredSocket.index === "string" typeof graphState.hoveredSocket.index === "string"
) { ) {
manager.createEdge( manager.createEdge(
state.activeSocket.node, graphState.activeSocket.node,
state.activeSocket.index || 0, graphState.activeSocket.index || 0,
state.hoveredSocket.node, graphState.hoveredSocket.node,
state.hoveredSocket.index, graphState.hoveredSocket.index,
); );
} }
manager.save(); manager.save();
@@ -780,22 +795,25 @@
cameraDown[1] === cameraPosition[1] && cameraDown[1] === cameraPosition[1] &&
isBodyFocused() isBodyFocused()
) { ) {
state.activeNodeId = -1; graphState.activeNodeId = -1;
state.clearSelection(); graphState.clearSelection();
} }
mouseDown = null; mouseDown = null;
boxSelection = false; boxSelection = false;
state.activeSocket = null; graphState.activeSocket = null;
state.possibleSockets = []; graphState.possibleSockets = [];
state.hoveredSocket = null; graphState.hoveredSocket = null;
addMenuPosition = null; addMenuPosition = null;
} }
let isPanning = false;
let isDragging = false; let isDragging = false;
let hoveredNodeId = -1;
function handleMouseLeave() { function handleMouseLeave() {
isDragging = false; isDragging = false;
isPanning = false;
} }
function handleDrop(event: DragEvent) { function handleDrop(event: DragEvent) {
@@ -865,16 +883,19 @@
function handleDragEnter(e: DragEvent) { function handleDragEnter(e: DragEvent) {
e.preventDefault(); e.preventDefault();
isDragging = true; isDragging = true;
isPanning = false;
} }
function handlerDragOver(e: DragEvent) { function handlerDragOver(e: DragEvent) {
isDragging = true;
e.preventDefault(); e.preventDefault();
isDragging = true;
isPanning = false;
} }
function handleDragEnd(e: DragEvent) { function handleDragEnd(e: DragEvent) {
isDragging = false;
e.preventDefault(); e.preventDefault();
isDragging = true;
isPanning = false;
} }
onMount(() => { onMount(() => {
@@ -893,6 +914,8 @@
on:wheel={handleMouseScroll} on:wheel={handleMouseScroll}
bind:this={wrapper} bind:this={wrapper}
class="graph-wrapper" class="graph-wrapper"
class:is-panning={isPanning}
class:is-hovering={hoveredNodeId !== -1}
aria-label="Graph" aria-label="Graph"
role="button" role="button"
tabindex="0" tabindex="0"
@@ -916,9 +939,6 @@
/> />
<label for="drop-zone"></label> <label for="drop-zone"></label>
{#if showHelp}
<HelpView registry={manager.registry} />
{/if}
<Canvas shadows={false} renderMode="on-demand" colorManagementEnabled={false}> <Canvas shadows={false} renderMode="on-demand" colorManagementEnabled={false}>
<Camera bind:camera position={cameraPosition} /> <Camera bind:camera position={cameraPosition} />
@@ -943,11 +963,12 @@
<AddMenu bind:position={addMenuPosition} graph={manager} /> <AddMenu bind:position={addMenuPosition} graph={manager} />
{/if} {/if}
{#if state.activeSocket} {#if graphState.activeSocket}
<FloatingEdge <FloatingEdge
z={cameraPosition[2]}
from={{ from={{
x: state.activeSocket.position[0], x: graphState.activeSocket.position[0],
y: state.activeSocket.position[1], y: graphState.activeSocket.position[1],
}} }}
to={{ x: mousePosition[0], y: mousePosition[1] }} to={{ x: mousePosition[0], y: mousePosition[1] }}
/> />
@@ -962,6 +983,10 @@
</Canvas> </Canvas>
</div> </div>
{#if showHelp}
<HelpView registry={manager.registry} />
{/if}
<style> <style>
.graph-wrapper { .graph-wrapper {
position: relative; position: relative;
@@ -969,6 +994,15 @@
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
height: 100%; height: 100%;
} }
.is-hovering {
cursor: pointer;
}
.is-panning {
cursor: grab;
}
input { input {
position: absolute; position: absolute;
z-index: 1; z-index: 1;

View File

@@ -6,10 +6,23 @@
import { getContext, onMount } from "svelte"; import { getContext, onMount } from "svelte";
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import { getGraphState } from "./state.svelte"; import { getGraphState } from "./state.svelte";
import { useThrelte } from "@threlte/core";
import { appSettings } from "$lib/settings/app-settings.svelte";
export let nodes: Writable<Map<number, NodeType>>; type Props = {
export let edges: Writable<EdgeType[]>; nodes: Writable<Map<number, NodeType>>;
export let cameraPosition = [0, 0, 4]; edges: Writable<EdgeType[]>;
cameraPosition: [number, number, number];
};
const { nodes, edges, cameraPosition = [0, 0, 4] }: Props = $props();
const { invalidate } = useThrelte();
$effect(() => {
appSettings.theme;
invalidate();
});
const graphState = getGraphState(); const graphState = getGraphState();
@@ -23,7 +36,6 @@
function getEdgePosition(edge: EdgeType) { function getEdgePosition(edge: EdgeType) {
const pos1 = getSocketPosition(edge[0], edge[1]); const pos1 = getSocketPosition(edge[0], edge[1]);
const pos2 = getSocketPosition(edge[2], edge[3]); const pos2 = getSocketPosition(edge[2], edge[3]);
return [pos1[0], pos1[1], pos2[0], pos2[1]]; return [pos1[0], pos1[1], pos2[0], pos2[1]];
} }
@@ -41,6 +53,7 @@
{@const pos = getEdgePosition(edge)} {@const pos = getEdgePosition(edge)}
{@const [x1, y1, x2, y2] = pos} {@const [x1, y1, x2, y2] = pos}
<Edge <Edge
z={cameraPosition[2]}
from={{ from={{
x: x1, x: x1,
y: y1, y: y1,

View File

@@ -3,19 +3,18 @@
import GraphEl from "./Graph.svelte"; import GraphEl from "./Graph.svelte";
import { GraphManager } from "../graph-manager.js"; import { GraphManager } from "../graph-manager.js";
import { setContext } from "svelte"; import { setContext } from "svelte";
import { type Writable } from "svelte/store";
import { debounce } from "$lib/helpers"; import { debounce } from "$lib/helpers";
import { createKeyMap } from "$lib/helpers/createKeyMap"; import { createKeyMap } from "$lib/helpers/createKeyMap";
import { GraphState } from "./state.svelte"; import { GraphState } from "./state.svelte";
const state = new GraphState(); const graphState = new GraphState();
setContext("graphState", state); setContext("graphState", graphState);
type Props = { type Props = {
graph: Graph; graph: Graph;
registry: NodeRegistry; registry: NodeRegistry;
settings?: Writable<Record<string, any>>; settings?: Record<string, any>;
activeNode?: Node; activeNode?: Node;
showGrid?: boolean; showGrid?: boolean;
@@ -41,33 +40,32 @@
}: Props = $props(); }: Props = $props();
export const keymap = createKeyMap([]); export const keymap = createKeyMap([]);
setContext("keymap", keymap);
export const manager = new GraphManager(registry); export const manager = new GraphManager(registry);
setContext("graphManager", manager); setContext("graphManager", manager);
$effect(() => { $effect(() => {
if (state.activeNodeId !== -1) { if (graphState.activeNodeId !== -1) {
activeNode = manager.getNode(state.activeNodeId); activeNode = manager.getNode(graphState.activeNodeId);
} else { } else if (activeNode) {
activeNode = undefined; activeNode = undefined;
} }
}); });
setContext("keymap", keymap);
const updateSettings = debounce((s) => { const updateSettings = debounce((s) => {
manager.setSettings(s); manager.setSettings(s);
}, 200); }, 200);
$effect(() => { $effect(() => {
if (settingTypes && settings) { if (settingTypes && settings) {
updateSettings($settings); updateSettings($state.snapshot(settings));
} }
}); });
manager.on("settings", (_settings) => { manager.on("settings", (_settings) => {
settingTypes = _settings.types; settingTypes = { ...settingTypes, ..._settings.types };
settings?.set(_settings.values); settings = _settings.values;
}); });
manager.on("result", (result) => onresult?.(result)); manager.on("result", (result) => onresult?.(result));

View File

@@ -0,0 +1,32 @@
import { appSettings } from "$lib/settings/app-settings.svelte";
import { Color, LinearSRGBColorSpace } from "three";
const variables = [
"layer-0",
"layer-1",
"layer-2",
"layer-3",
"outline",
"active",
"selected",
"edge",
] as const;
function getColor(variable: typeof variables[number]) {
const style = getComputedStyle(document.body.parentElement!);
let color = style.getPropertyValue(`--${variable}`);
return new Color().setStyle(color, LinearSRGBColorSpace);
}
export const colors = Object.fromEntries(variables.map(v => [v, getColor(v)])) as Record<typeof variables[number], Color>;
$effect.root(() => {
$effect(() => {
if (!appSettings.theme || !("getComputedStyle" in globalThis)) return;
const style = getComputedStyle(document.body.parentElement!);
for (const v of variables) {
const hex = style.getPropertyValue(`--${v}`);
colors[v].setStyle(hex, LinearSRGBColorSpace);
}
});
})

View File

@@ -1,48 +0,0 @@
import { readable } from "svelte/store";
import { Color } from "three";
const variables = [
"layer-0",
"layer-1",
"layer-2",
"layer-3",
"outline",
"active",
"selected",
"edge",
] as const;
const store = Object.fromEntries(variables.map(v => [v, new Color()])) as Record<typeof variables[number], Color>;
let lastStyle = "";
function updateColors() {
if (!("getComputedStyle" in globalThis)) return;
const style = getComputedStyle(document.body.parentElement!);
let hash = "";
for (const v of variables) {
let color = style.getPropertyValue(`--${v}`);
hash += color;
store[v].setStyle(color);
}
if (hash === lastStyle) return;
lastStyle = hash;
}
export const colors = readable(store, set => {
updateColors();
set(store);
setTimeout(() => {
updateColors();
set(store);
}, 1000);
window.onload = function () { updateColors(); set(store) };
document.body.addEventListener("transitionstart", () => {
updateColors();
set(store);
})
});

View File

@@ -1,26 +1,22 @@
import type { Socket } from "@nodes/types"; import type { Socket } from "@nodes/types";
import { getContext } from "svelte"; import { getContext } from "svelte";
import { SvelteSet } from 'svelte/reactivity';
export function getGraphState() { export function getGraphState() {
return getContext<GraphState>("graphState"); return getContext<GraphState>("graphState");
} }
export class GraphState { export class GraphState {
activeNodeId = $state(-1); activeNodeId = $state(-1);
selectedNodes = $state(new Set<number>()); selectedNodes = new SvelteSet<number>();
activeSocket = $state<Socket | null>(null); activeSocket = $state<Socket | null>(null);
hoveredSocket = $state<Socket | null>(null); hoveredSocket = $state<Socket | null>(null);
possibleSockets = $state<Socket[]>([]); possibleSockets = $state<Socket[]>([]);
possibleSocketIds = $derived(new Set( possibleSocketIds = $derived(new Set(
this.possibleSockets.map((s) => `${s.node.id}-${s.index}`), this.possibleSockets.map((s) => `${s.node.id}-${s.index}`),
)); ));
clearSelection() { clearSelection() {
this.selectedNodes = new Set(); this.selectedNodes.clear();
}
} }
}
export { colors } from "./colors";

View File

@@ -1,12 +1,14 @@
<script lang="ts"> <script lang="ts">
import type { Node } from "@nodes/types"; import type { Node } from "@nodes/types";
import { getContext, onMount } from "svelte"; import { getContext, onMount } from "svelte";
import { colors, getGraphState } from "../graph/state.svelte"; import { getGraphState } from "../graph/state.svelte";
import { T } from "@threlte/core"; import { T } from "@threlte/core";
import { Color, type Mesh } from "three"; import { type Mesh } from "three";
import NodeFrag from "./Node.frag"; import NodeFrag from "./Node.frag";
import NodeVert from "./Node.vert"; import NodeVert from "./Node.vert";
import NodeHtml from "./NodeHTML.svelte"; import NodeHtml from "./NodeHTML.svelte";
import { colors } from "../graph/colors.svelte";
import { appSettings } from "$lib/settings/app-settings.svelte";
const graphState = getGraphState(); const graphState = getGraphState();
@@ -18,7 +20,16 @@
const { node, inView, z }: Props = $props(); const { node, inView, z }: Props = $props();
const isActive = $derived(graphState.activeNodeId === node.id); const isActive = $derived(graphState.activeNodeId === node.id);
const isSelected = $derived(!!graphState.selectedNodes?.has(node.id)); const isSelected = $derived(graphState.selectedNodes.has(node.id));
let strokeColor = $state(colors.selected);
$effect(() => {
appSettings.theme;
strokeColor = isSelected
? colors.selected
: isActive
? colors.active
: colors.outline;
});
const updateNodePosition = const updateNodePosition =
getContext<(n: Node) => void>("updateNodePosition"); getContext<(n: Node) => void>("updateNodePosition");
@@ -30,16 +41,11 @@
const height = getNodeHeight?.(node.type); const height = getNodeHeight?.(node.type);
$effect(() => { $effect(() => {
if (node && meshRef) {
node.tmp = node.tmp || {}; node.tmp = node.tmp || {};
node.tmp.mesh = meshRef; node.tmp.mesh = meshRef;
updateNodePosition?.(node); updateNodePosition?.(node);
}
}); });
const colorBright = $colors["layer-2"];
const colorDark = $colors["layer-1"];
onMount(() => { onMount(() => {
node.tmp = node.tmp || {}; node.tmp = node.tmp || {};
node.tmp.mesh = meshRef; node.tmp.mesh = meshRef;
@@ -61,20 +67,14 @@
fragmentShader={NodeFrag} fragmentShader={NodeFrag}
transparent transparent
uniforms={{ uniforms={{
uColorBright: { value: new Color("#171717") }, uColorBright: { value: colors["layer-2"] },
uColorDark: { value: new Color("#151515") }, uColorDark: { value: colors["layer-1"] },
uStrokeColor: { value: new Color("#9d5f28") }, uStrokeColor: { value: colors.outline.clone() },
uStrokeWidth: { value: 1.0 }, uStrokeWidth: { value: 1.0 },
uWidth: { value: 20 }, uWidth: { value: 20 },
uHeight: { value: height }, uHeight: { value: height },
}} }}
uniforms.uColorBright.value={colorBright} uniforms.uStrokeColor.value={strokeColor.clone()}
uniforms.uColorDark.value={colorDark}
uniforms.uStrokeColor.value={isSelected
? $colors.selected
: isActive
? $colors.active
: $colors.outline}
uniforms.uStrokeWidth.value={(7 - z) / 3} uniforms.uStrokeWidth.value={(7 - z) / 3}
/> />
</T.Mesh> </T.Mesh>

View File

@@ -3,14 +3,26 @@
import NodeHeader from "./NodeHeader.svelte"; import NodeHeader from "./NodeHeader.svelte";
import NodeParameter from "./NodeParameter.svelte"; import NodeParameter from "./NodeParameter.svelte";
import { getContext, onMount } from "svelte"; import { getContext, onMount } from "svelte";
export let isActive = false;
export let isSelected = false;
export let inView = true;
export let z = 2;
let ref: HTMLDivElement; let ref: HTMLDivElement;
export let node: Node;
export let position = "absolute"; type Props = {
node: Node;
position?: "absolute" | "fixed" | "relative";
isActive?: boolean;
isSelected?: boolean;
inView?: boolean;
z?: number;
};
let {
node = $bindable(),
position = "absolute",
isActive = false,
isSelected = false,
inView = true,
z = 2,
}: Props = $props();
const zOffset = (node.tmp?.random || 0) * 0.5; const zOffset = (node.tmp?.random || 0) * 0.5;
const zLimit = 2 - zOffset; const zLimit = 2 - zOffset;
@@ -25,12 +37,6 @@
const updateNodePosition = const updateNodePosition =
getContext<(n: Node) => void>("updateNodePosition"); getContext<(n: Node) => void>("updateNodePosition");
$: if (node && ref) {
node.tmp = node.tmp || {};
node.tmp.ref = ref;
updateNodePosition?.(node);
}
onMount(() => { onMount(() => {
node.tmp = node.tmp || {}; node.tmp = node.tmp || {};
node.tmp.ref = ref; node.tmp.ref = ref;

View File

@@ -26,9 +26,9 @@
const aspectRatio = 0.25; const aspectRatio = 0.25;
const path = createNodePath({ const path = createNodePath({
depth: 5, depth: 5.5,
height: 29, height: 34,
y: 50, y: 49,
cornerTop, cornerTop,
rightBump, rightBump,
aspectRatio, aspectRatio,
@@ -42,9 +42,9 @@
aspectRatio, aspectRatio,
}); });
const pathHover = createNodePath({ const pathHover = createNodePath({
depth: 9, depth: 8.5,
height: 50, height: 50,
y: 50, y: 49,
cornerTop, cornerTop,
rightBump, rightBump,
aspectRatio, aspectRatio,
@@ -103,12 +103,12 @@
svg { svg {
position: absolute; position: absolute;
top: 0; top: 1px;
left: 0; left: 1px;
z-index: -1; z-index: -1;
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: calc(100% - 2px);
height: 100%; height: calc(100% - 1px);
overflow: visible; overflow: visible;
} }

View File

@@ -19,7 +19,18 @@
const graph = getGraphManager(); const graph = getGraphManager();
let value = $state(node?.props?.[id] ?? input.value); function getDefaultValue() {
if (node?.props?.[id] !== undefined) return node?.props?.[id] as number;
if ("value" in input && input?.value !== undefined)
return input?.value as number;
if (input.type === "boolean") return 0;
if (input.type === "float") return 0.5;
if (input.type === "integer") return 0;
if (input.type === "select") return 0;
return 0;
}
let value = $state(getDefaultValue());
$effect(() => { $effect(() => {
if (value !== undefined && node?.props?.[id] !== value) { if (value !== undefined && node?.props?.[id] !== value) {

View File

@@ -53,14 +53,14 @@
const path = createNodePath({ const path = createNodePath({
depth: 7, depth: 7,
height: 20, height: 20,
y: 51, y: 50.5,
cornerBottom, cornerBottom,
leftBump, leftBump,
aspectRatio, aspectRatio,
}); });
const pathDisabled = createNodePath({ const pathDisabled = createNodePath({
depth: 4.5, depth: 6,
height: 14, height: 18,
y: 50.5, y: 50.5,
cornerBottom, cornerBottom,
leftBump, leftBump,
@@ -172,11 +172,11 @@
svg { svg {
position: absolute; position: absolute;
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: calc(100% - 2px);
height: 100%; height: 100%;
overflow: visible; overflow: visible;
top: 0; top: 0;
left: 0; left: 1px;
z-index: -1; z-index: -1;
} }

View File

@@ -18,7 +18,6 @@ export function createKeyMap(keys: Shortcut[]) {
const store = writable(new Map(keys.map(k => [getShortcutId(k), k]))); const store = writable(new Map(keys.map(k => [getShortcutId(k), k])));
return { return {
handleKeyboardEvent: (event: KeyboardEvent) => { handleKeyboardEvent: (event: KeyboardEvent) => {
const activeElement = document.activeElement as HTMLElement; const activeElement = document.activeElement as HTMLElement;

View File

@@ -23,7 +23,7 @@
<div class="wrapper"> <div class="wrapper">
{#if !activeUser} {#if !activeUser}
{#await registry.fetchUsers()} {#await registry.fetchUsers()}
<div>Loading...</div> <div>Loading Users...</div>
{:then users} {:then users}
{#each users as user} {#each users as user}
<button <button
@@ -37,7 +37,7 @@
{/await} {/await}
{:else if !activeCollection} {:else if !activeCollection}
{#await registry.fetchUser(activeUser)} {#await registry.fetchUser(activeUser)}
<div>Loading...</div> <div>Loading User...</div>
{:then user} {:then user}
{#each user.collections as collection} {#each user.collections as collection}
<button <button
@@ -53,11 +53,11 @@
{/await} {/await}
{:else if !activeNode} {:else if !activeNode}
{#await registry.fetchCollection(`${activeUser}/${activeCollection}`)} {#await registry.fetchCollection(`${activeUser}/${activeCollection}`)}
<div>Loading...</div> <div>Loading Collection...</div>
{:then collection} {:then collection}
{#each collection.nodes as node} {#each collection.nodes as node}
{#await registry.fetchNodeDefinition(node.id)} {#await registry.fetchNodeDefinition(node.id)}
<div>Loading... {node.id}</div> <div>Loading Node... {node.id}</div>
{:then node} {:then node}
{#if node} {#if node}
<DraggableNode {node} /> <DraggableNode {node} />

View File

@@ -11,9 +11,9 @@
fps: false, fps: false,
}); });
$: vertices = $store?.at(-1)?.["total-vertices"][0] || 0; $: vertices = $store?.at(-1)?.["total-vertices"]?.[0] || 0;
$: faces = $store?.at(-1)?.["total-faces"][0] || 0; $: faces = $store?.at(-1)?.["total-faces"]?.[0] || 0;
$: runtime = $store?.at(-1)?.["runtime"][0] || 0; $: runtime = $store?.at(-1)?.["runtime"]?.[0] || 0;
function getPoints(data: PerformanceData, key: string) { function getPoints(data: PerformanceData, key: string) {
return data?.map((run) => run[key]?.[0] || 0) || []; return data?.map((run) => run[key]?.[0] || 0) || [];

View File

@@ -9,21 +9,28 @@
Box3, Box3,
Mesh, Mesh,
MeshBasicMaterial, MeshBasicMaterial,
Color,
} from "three"; } from "three";
import { AppSettings } from "../settings/app-settings"; import { appSettings } from "../settings/app-settings.svelte";
import Camera from "./Camera.svelte"; import Camera from "./Camera.svelte";
import { colors } from "$lib/graph-interface/graph/colors.svelte";
const { renderStage, invalidate: _invalidate } = useThrelte(); const { renderStage, invalidate: _invalidate } = useThrelte();
export let fps: number[] = []; type Props = {
// let renderer = threlte.renderer; fps: number[];
// let rendererRender = renderer.render; lines: Vector3[][];
// renderer.render = function (scene, camera) { scene: Group;
// const a = performance.now(); centerCamera: boolean;
// rendererRender.call(renderer, scene, camera); };
// fps.push(performance.now() - a);
// fps = fps.slice(-100); let {
// }; lines,
centerCamera,
fps = $bindable(),
scene = $bindable(),
}: Props = $props();
useTask( useTask(
(delta) => { (delta) => {
fps.push(1 / delta); fps.push(1 / delta);
@@ -53,12 +60,8 @@
_invalidate(); _invalidate();
}; };
let geometries: BufferGeometry[] = []; let geometries = $state<BufferGeometry[]>();
export let lines: Vector3[][]; let center = $state(new Vector3(0, 4, 0));
export let scene: Group;
export let centerCamera: boolean = true;
let center = new Vector3(0, 4, 0);
function isMesh(child: Mesh | any): child is Mesh { function isMesh(child: Mesh | any): child is Mesh {
return child.isObject3D && "material" in child; return child.isObject3D && "material" in child;
@@ -68,14 +71,15 @@
return material.isMaterial && "matcap" in material; return material.isMaterial && "matcap" in material;
} }
$: if ($AppSettings && scene) { $effect(() => {
const wireframe = appSettings.debug.wireframe;
scene.traverse(function (child) { scene.traverse(function (child) {
if (isMesh(child) && isMatCapMaterial(child.material)) { if (isMesh(child) && isMatCapMaterial(child.material)) {
child.material.wireframe = $AppSettings.wireframe; child.material.wireframe = wireframe;
} }
}); });
invalidate(); _invalidate();
} });
function getPosition(geo: BufferGeometry, i: number) { function getPosition(geo: BufferGeometry, i: number) {
return [ return [
@@ -88,13 +92,18 @@
<Camera {center} {centerCamera} /> <Camera {center} {centerCamera} />
{#if $AppSettings.showGrid} {#if appSettings.showGrid}
<T.GridHelper args={[20, 20]} /> <T.GridHelper
args={[20, 20]}
colorGrid={colors["outline"]}
colorCenterLine={new Color("red")}
/>
{/if} {/if}
<T.Group> <T.Group>
{#if geometries}
{#each geometries as geo} {#each geometries as geo}
{#if $AppSettings.showIndices} {#if appSettings.debug.showIndices}
{#each geo.attributes.position.array as _, i} {#each geo.attributes.position.array as _, i}
{#if i % 3 === 0} {#if i % 3 === 0}
<Text fontSize={0.25} position={getPosition(geo, i)} /> <Text fontSize={0.25} position={getPosition(geo, i)} />
@@ -102,18 +111,19 @@
{/each} {/each}
{/if} {/if}
{#if $AppSettings.showVertices} {#if appSettings.debug.showVertices}
<T.Points visible={true}> <T.Points visible={true}>
<T is={geo} /> <T is={geo} />
<T.PointsMaterial size={0.25} /> <T.PointsMaterial size={0.25} />
</T.Points> </T.Points>
{/if} {/if}
{/each} {/each}
{/if}
<T.Group bind:ref={scene}></T.Group> <T.Group bind:ref={scene}></T.Group>
</T.Group> </T.Group>
{#if $AppSettings.showStemLines && lines} {#if appSettings.debug.showStemLines && lines}
{#each lines as line} {#each lines as line}
<T.Mesh> <T.Mesh>
<MeshLineGeometry points={line} /> <MeshLineGeometry points={line} />

View File

@@ -2,12 +2,10 @@
import { Canvas } from "@threlte/core"; import { Canvas } from "@threlte/core";
import Scene from "./Scene.svelte"; import Scene from "./Scene.svelte";
import { Vector3 } from "three"; import { Vector3 } from "three";
import { decodeFloat, splitNestedArray } from "@nodes/utils"; import { decodeFloat, splitNestedArray } from "@nodes/utils";
import type { PerformanceStore } from "@nodes/utils"; import type { PerformanceStore } from "@nodes/utils";
import { AppSettings } from "$lib/settings/app-settings"; import { appSettings } from "$lib/settings/app-settings.svelte";
import SmallPerformanceViewer from "$lib/performance/SmallPerformanceViewer.svelte"; import SmallPerformanceViewer from "$lib/performance/SmallPerformanceViewer.svelte";
import { MeshMatcapMaterial, TextureLoader, type Group } from "three"; import { MeshMatcapMaterial, TextureLoader, type Group } from "three";
import { import {
createGeometryPool, createGeometryPool,
@@ -22,9 +20,11 @@
matcap, matcap,
}); });
let sceneComponent = $state<ReturnType<typeof Scene>>();
let fps = $state<number[]>([]);
let geometryPool: ReturnType<typeof createGeometryPool>; let geometryPool: ReturnType<typeof createGeometryPool>;
let instancePool: ReturnType<typeof createInstancedGeometryPool>; let instancePool: ReturnType<typeof createInstancedGeometryPool>;
export function updateGeometries(inputs: Int32Array[], group: Group) { export function updateGeometries(inputs: Int32Array[], group: Group) {
geometryPool = geometryPool || createGeometryPool(group, material); geometryPool = geometryPool || createGeometryPool(group, material);
instancePool = instancePool || createInstancedGeometryPool(group, material); instancePool = instancePool || createInstancedGeometryPool(group, material);
@@ -38,14 +38,15 @@
}; };
} }
export let centerCamera: boolean = true; type Props = {
export let perf: PerformanceStore; scene: Group;
export let scene: Group; centerCamera: boolean;
let fps: number[] = []; perf: PerformanceStore;
};
let lines: Vector3[][] = []; let { scene = $bindable(), centerCamera, perf }: Props = $props();
let invalidate: () => void; let lines = $state<Vector3[][]>([]);
function createLineGeometryFromEncodedData(encodedData: Int32Array) { function createLineGeometryFromEncodedData(encodedData: Int32Array) {
const positions: Vector3[] = []; const positions: Vector3[] = [];
@@ -63,12 +64,12 @@
} }
export const update = function update(result: Int32Array) { export const update = function update(result: Int32Array) {
perf?.addPoint("split-result"); perf.addPoint("split-result");
const inputs = splitNestedArray(result); const inputs = splitNestedArray(result);
perf?.endPoint(); perf.endPoint();
if ($AppSettings.showStemLines) { if (appSettings.debug.showStemLines) {
perf?.addPoint("create-lines"); perf.addPoint("create-lines");
lines = inputs lines = inputs
.map((input) => { .map((input) => {
if (input[0] === 0) { if (input[0] === 0) {
@@ -79,21 +80,27 @@
perf.endPoint(); perf.endPoint();
} }
perf?.addPoint("update-geometries"); perf.addPoint("update-geometries");
const { totalVertices, totalFaces } = updateGeometries(inputs, scene); const { totalVertices, totalFaces } = updateGeometries(inputs, scene);
perf?.endPoint(); perf.endPoint();
perf?.addPoint("total-vertices", totalVertices); perf.addPoint("total-vertices", totalVertices);
perf?.addPoint("total-faces", totalFaces); perf.addPoint("total-faces", totalFaces);
invalidate(); sceneComponent?.invalidate();
}; };
</script> </script>
{#if $AppSettings.showPerformancePanel} {#if appSettings.debug.showPerformancePanel}
<SmallPerformanceViewer {fps} store={perf} /> <SmallPerformanceViewer {fps} store={perf} />
{/if} {/if}
<Canvas> <Canvas>
<Scene bind:scene bind:invalidate {lines} {centerCamera} bind:fps /> <Scene
bind:this={sceneComponent}
{lines}
{centerCamera}
bind:scene
bind:fps
/>
</Canvas> </Canvas>

View File

@@ -1,6 +1,5 @@
import { fastHashArrayBuffer } from "@nodes/utils"; import { fastHashArrayBuffer } from "@nodes/utils";
import { BufferAttribute, BufferGeometry, Float32BufferAttribute, Group, InstancedMesh, Material, Matrix4, Mesh } from "three" import { BufferAttribute, BufferGeometry, Float32BufferAttribute, Group, InstancedMesh, Material, Matrix4, Mesh } from "three";
function fastArrayHash(arr: ArrayBuffer) { function fastArrayHash(arr: ArrayBuffer) {
let ints = new Uint8Array(arr); let ints = new Uint8Array(arr);
@@ -108,7 +107,6 @@ export function createGeometryPool(parentScene: Group, material: Material) {
scene.add(mesh); scene.add(mesh);
meshes.push(mesh); meshes.push(mesh);
} }
} }

View File

@@ -1,6 +1,5 @@
import type { Graph, NodeRegistry, NodeDefinition, RuntimeExecutor, NodeInput } from "@nodes/types"; import type { Graph, NodeDefinition, NodeInput, NodeRegistry, RuntimeExecutor, SyncCache } from "@nodes/types";
import { concatEncodedArrays, encodeFloat, fastHashArrayBuffer, createLogger, type PerformanceStore } from "@nodes/utils" import { concatEncodedArrays, createLogger, encodeFloat, fastHashArrayBuffer, type PerformanceStore } from "@nodes/utils";
import type { SyncCache } from "@nodes/types";
const log = createLogger("runtime-executor"); const log = createLogger("runtime-executor");
log.mute() log.mute()
@@ -9,6 +8,7 @@ function getValue(input: NodeInput, value?: unknown) {
if (value === undefined && "value" in input) { if (value === undefined && "value" in input) {
value = input.value value = input.value
} }
if (input.type === "float") { if (input.type === "float") {
return encodeFloat(value as number); return encodeFloat(value as number);
} }

View File

@@ -5,9 +5,6 @@ import type { Graph, RuntimeExecutor } from "@nodes/types";
export class WorkerRuntimeExecutor implements RuntimeExecutor { export class WorkerRuntimeExecutor implements RuntimeExecutor {
private worker = new ComlinkWorker<typeof import('./worker-runtime-executor-backend.ts')>(new URL(`./worker-runtime-executor-backend.ts`, import.meta.url)); private worker = new ComlinkWorker<typeof import('./worker-runtime-executor-backend.ts')>(new URL(`./worker-runtime-executor-backend.ts`, import.meta.url));
constructor() {
console.log(import.meta.url)
}
async execute(graph: Graph, settings: Record<string, unknown>) { async execute(graph: Graph, settings: Record<string, unknown>) {
return this.worker.executeGraph(graph, settings); return this.worker.executeGraph(graph, settings);
} }

View File

@@ -0,0 +1,177 @@
<script module lang="ts">
let openSections = localState<Record<string, boolean>>("open-details", {});
</script>
<script lang="ts">
import NestedSettings from "./NestedSettings.svelte";
import { localState } from "$lib/helpers/localState.svelte";
import type { NodeInput } from "@nodes/types";
import Input from "@nodes/ui";
type Button = { type: "button"; label?: string };
type InputType = NodeInput | Button;
interface Nested {
[key: string]: (Nested & { title?: string }) | InputType;
}
type SettingsType = Record<string, Nested>;
type SettingsValue = Record<
string,
Record<string, unknown> | string | number | boolean | number[]
>;
type Props = {
id: string;
key?: string;
value: SettingsValue;
type: SettingsType;
depth?: number;
};
let { id, key = "", value = $bindable(), type, depth = 0 }: Props = $props();
function isNodeInput(v: InputType | Nested): v is InputType {
return v && "type" in v;
}
function getDefaultValue() {
if (key === "") return;
if (key === "title") return;
if (Array.isArray(type[key]?.options)) {
if (value?.[key] !== undefined) {
return type[key]?.options?.indexOf(value?.[key]);
} else {
return 0;
}
}
if (value?.[key] !== undefined) return value?.[key];
if (type[key]?.value !== undefined) return type[key]?.value;
if (isNodeInput(type[key])) {
if (type[key].type === "boolean") return 0;
if (type[key].type === "float") return 0.5;
if (type[key].type === "integer") return 0;
if (type[key].type === "select") return 0;
}
return 0;
}
let internalValue = $state(getDefaultValue());
let open = $state(openSections[id]);
if (depth > 0 && !isNodeInput(type[key])) {
$effect(() => {
if (open !== undefined) {
openSections[id] = open;
}
});
}
$effect(() => {
if (key === "" || internalValue === undefined) return;
if (
isNodeInput(type[key]) &&
Array.isArray(type[key]?.options) &&
typeof internalValue === "number"
) {
value[key] = type[key].options?.[internalValue];
} else {
value[key] = internalValue;
}
});
</script>
{#if key && isNodeInput(type?.[key])}
<div class="input input-{type[key].type}" class:first-level={depth === 1}>
{#if type[key].type === "button"}
<button onclick={() => console.log(type[key])}>
{type[key].label || key}
</button>
{:else}
<label for={id}>{type[key].label || key}</label>
<Input {id} input={type[key]} bind:value={internalValue} />
{/if}
</div>
{:else if depth === 0}
{#each Object.keys(type ?? {}).filter((key) => key !== "title") as childKey}
<NestedSettings
id={`${id}.${childKey}`}
key={childKey}
{value}
{type}
depth={depth + 1}
/>
{/each}
<hr />
{:else if key && type?.[key]}
{#if depth > 0}
<hr />
{/if}
<details bind:open>
<summary><p>{type[key]?.title || key}</p></summary>
<div class="content">
{#each Object.keys(type[key]).filter((key) => key !== "title") as childKey}
<NestedSettings
id={`${id}.${childKey}`}
key={childKey}
value={value[key] as SettingsValue}
type={type[key] as SettingsType}
depth={depth + 1}
/>
{/each}
</div>
</details>
{/if}
<style>
summary {
cursor: pointer;
user-select: none;
margin-bottom: 1em;
}
summary > p {
display: inline;
padding-left: 6px;
}
details {
padding: 1em;
padding-bottom: 0;
padding-left: 21px;
}
.input {
margin-top: 15px;
margin-bottom: 15px;
display: flex;
flex-direction: column;
gap: 10px;
padding-left: 20px;
}
.input-boolean {
display: flex;
flex-direction: row;
align-items: center;
}
.input-boolean > label {
order: 2;
}
.first-level.input {
padding-left: 1em;
padding-right: 1em;
padding-bottom: 1px;
}
hr {
position: absolute;
margin: 0;
left: 0;
right: 0;
border: none;
border-bottom: solid thin var(--outline);
}
</style>

View File

@@ -39,6 +39,7 @@ export const AppSettingTypes = {
} }
}, },
debug: { debug: {
title: "Debug",
wireframe: { wireframe: {
type: "boolean", type: "boolean",
label: "Wireframe", label: "Wireframe",
@@ -79,7 +80,8 @@ export const AppSettingTypes = {
amount: { amount: {
type: "integer", type: "integer",
min: 2, min: 2,
max: 15 max: 15,
value: 4
}, },
loadGrid: { loadGrid: {
type: "button", type: "button",
@@ -103,16 +105,26 @@ export const AppSettingTypes = {
} }
}, },
} }
} as const } as const;
type IsInputDefinition<T> = T extends NodeInput ? T : never; type IsInputDefinition<T> = T extends NodeInput ? T : never;
type HasTitle = { title: string }; type HasTitle = { title: string };
type Widen<T> = T extends boolean
? boolean
: T extends number
? number
: T extends string
? string
: T;
type ExtractSettingsValues<T> = { type ExtractSettingsValues<T> = {
[K in keyof T]: T[K] extends HasTitle -readonly [K in keyof T]: T[K] extends HasTitle
? ExtractSettingsValues<Omit<T[K], 'title'>> ? ExtractSettingsValues<Omit<T[K], 'title'>>
: T[K] extends IsInputDefinition<T[K]> : T[K] extends IsInputDefinition<T[K]>
? T[K] extends { value: any } ? T[K] extends { value: infer V }
? T[K]['value'] ? Widen<V>
: never : never
: T[K] extends Record<string, any> : T[K] extends Record<string, any>
? ExtractSettingsValues<T[K]> ? ExtractSettingsValues<T[K]>
@@ -138,8 +150,8 @@ export const appSettings = localState("app-settings", settingsToStore(AppSetting
$effect.root(() => { $effect.root(() => {
$effect(() => { $effect(() => {
const { theme } = $state.snapshot(appSettings); const theme = appSettings.theme;
const classes = document.body.parentElement?.classList; const classes = document.documentElement.classList;
const newClassName = `theme-${theme}`; const newClassName = `theme-${theme}`;
if (classes) { if (classes) {
for (const className of classes) { for (const className of classes) {
@@ -148,6 +160,6 @@ $effect.root(() => {
} }
} }
} }
document.body?.parentElement?.classList.add(newClassName); document.documentElement.classList.add(newClassName);
}); });
}); });

View File

@@ -1,136 +0,0 @@
import localStore from "$lib/helpers/localStore";
export const AppSettings = localStore("node.settings", {
theme: 0,
showGrid: true,
showNodeGrid: true,
snapToGrid: true,
showHelp: false,
wireframe: false,
showIndices: false,
showVertices: false,
showPerformancePanel: false,
showBenchmarkPanel: false,
centerCamera: true,
showStemLines: false,
useWorker: true,
amount: 5
});
const themes = ["dark", "light", "catppuccin", "solarized", "high-contrast", "nord", "dracula"];
AppSettings.subscribe((value) => {
const classes = document.body.parentElement?.classList;
const newClassName = `theme-${themes[value.theme]}`;
if (classes) {
for (const className of classes) {
if (className.startsWith("theme-") && className !== newClassName) {
classes.remove(className);
}
}
}
document.body?.parentElement?.classList.add(newClassName);
});
export const AppSettingTypes = {
theme: {
type: "select",
options: themes,
label: "Theme",
value: themes[0],
},
showGrid: {
type: "boolean",
label: "Show Grid",
value: true,
},
centerCamera: {
type: "boolean",
label: "Center Camera",
value: true
},
nodeInterface: {
__title: "Node Interface",
showNodeGrid: {
type: "boolean",
label: "Show Grid",
value: true
},
snapToGrid: {
type: "boolean",
label: "Snap to Grid",
value: true
},
showHelp: {
type: "boolean",
label: "Show Help",
value: false
}
},
debug: {
wireframe: {
type: "boolean",
label: "Wireframe",
value: false,
},
useWorker: {
type: "boolean",
label: "Execute runtime in worker",
value: true,
},
showIndices: {
type: "boolean",
label: "Show Indices",
value: false,
},
showPerformancePanel: {
type: "boolean",
label: "Show Performance Panel",
value: false,
},
showBenchmarkPanel: {
type: "boolean",
label: "Show Benchmark Panel",
value: false,
},
showVertices: {
type: "boolean",
label: "Show Vertices",
value: false,
},
showStemLines: {
type: "boolean",
label: "Show Stem Lines",
value: false,
},
stressTest: {
__title: "Stress Test",
amount: {
type: "integer",
min: 2,
max: 15
},
loadGrid: {
type: "button",
label: "Load Grid"
},
loadTree: {
type: "button",
label: "Load Tree"
},
lottaFaces: {
type: "button",
label: "Load 'lots of faces'"
},
lottaNodes: {
type: "button",
label: "Load 'lots of nodes'"
},
lottaNodesAndFaces: {
type: "button",
label: "Load 'lots of nodes and faces'"
}
},
}
}

View File

@@ -0,0 +1,13 @@
import type { NodeInput } from "@nodes/types";
type Button = { type: "button"; label?: string };
type InputType = NodeInput | Button;
export interface SettingsType {
[key: string]: (SettingsType & { title?: string }) | InputType;
}
export type SettingsStore = {
[key: string]: SettingsStore | string | number | boolean
};

View File

@@ -1,89 +0,0 @@
<script lang="ts">
import type { Node, NodeInput } from "@nodes/types";
import NestedSettings from "./NestedSettings.svelte";
import { writable } from "svelte/store";
import type { GraphManager } from "$lib/graph-interface/graph-manager";
export let manager: GraphManager;
export let node: Node | undefined;
function filterInputs(inputs: Record<string, NodeInput>) {
return Object.fromEntries(
Object.entries(inputs)
.filter(([_key, value]) => {
return value.hidden === true;
})
.map(([key, value]) => {
//@ts-ignore
value.__node_type = node?.tmp?.type.id;
//@ts-ignore
value.__node_input = key;
return [key, value];
}),
);
}
function createStore(
props: Node["props"],
inputs: Record<string, NodeInput>,
) {
const store: Record<string, unknown> = {};
Object.keys(inputs).forEach((key) => {
if (props) {
//@ts-ignore
store[key] = props[key] || inputs[key].value;
}
});
return writable(store);
}
let nodeDefinition: Record<string, NodeInput> | undefined;
$: nodeDefinition = node?.tmp?.type
? filterInputs(node.tmp.type.inputs)
: undefined;
$: store = node ? createStore(node.props, nodeDefinition) : undefined;
let lastPropsHash = "";
function updateNode() {
if (!node || !$store) return;
let needsUpdate = false;
Object.keys($store).forEach((_key: string) => {
node.props = node.props || {};
const key = _key as keyof typeof $store;
if (node && $store) {
needsUpdate = true;
node.props[key] = $store[key];
}
});
let propsHash = JSON.stringify(node.props);
if (propsHash === lastPropsHash) {
return;
}
lastPropsHash = propsHash;
// console.log(needsUpdate, node.props, $store);
if (needsUpdate) {
manager.execute();
}
}
$: if (store && $store) {
updateNode();
}
</script>
{#if node}
{#key node.id}
{#if nodeDefinition && store && Object.keys(nodeDefinition).length > 0}
<NestedSettings
id="activeNodeSettings"
settings={nodeDefinition}
{store}
/>
{:else}
<p class="mx-4">Active Node has no Settings</p>
{/if}
{/key}
{:else}
<p class="mx-4">No active node</p>
{/if}

View File

@@ -1,40 +0,0 @@
<script lang="ts">
import type { NodeInput } from "@nodes/types";
import NestedSettings from "./NestedSettings.svelte";
import type { Writable } from "svelte/store";
interface Nested {
[key: string]: NodeInput | Nested;
}
export let type: Record<string, NodeInput>;
export let store: Writable<Record<string, any>>;
function constructNested(type: Record<string, NodeInput>) {
const nested: Nested = {};
for (const key in type) {
const parts = key.split(".");
let current = nested;
for (let i = 0; i < parts.length; i++) {
if (i === parts.length - 1) {
current[parts[i]] = type[key];
} else {
current[parts[i]] = current[parts[i]] || {};
current = current[parts[i]] as Nested;
}
}
}
return nested;
}
$: settings = constructNested({
randomSeed: { type: "boolean", value: false },
...type,
});
</script>
{#key settings}
<NestedSettings id="graph-settings" {settings} {store} />
{/key}

View File

@@ -1,60 +0,0 @@
<script lang="ts">
import type { createKeyMap } from "$lib/helpers/createKeyMap";
import { ShortCut } from "@nodes/ui";
export let keymap: ReturnType<typeof createKeyMap>;
const keys = keymap?.keys;
export let title = "Keymap";
</script>
<div class="wrapper">
<h3>{title}</h3>
<section>
{#each $keys as key}
{#if key.description}
<div class="command-wrapper">
<ShortCut
alt={key.alt}
ctrl={key.ctrl}
shift={key.shift}
key={key.key}
/>
</div>
<p>{key.description}</p>
{/if}
{/each}
</section>
</div>
<style>
.wrapper {
padding: 1em;
display: flex;
flex-direction: column;
gap: 1em;
}
section {
display: grid;
grid-template-columns: min-content 1fr;
gap: 1em;
}
h3 {
margin: 0;
}
.command-wrapper {
display: flex;
justify-content: right;
align-items: center;
}
p {
font-size: 0.9em;
margin: 0;
display: flex;
align-items: center;
}
</style>

View File

@@ -1,147 +0,0 @@
<script lang="ts">
import NestedSettings from "./NestedSettings.svelte";
import {localState} from "$lib/helpers/localState.svelte";
import type { NodeInput } from "@nodes/types";
import Input from "@nodes/ui";
type Button = { type: "button"; label?: string };
type InputType = NodeInput | Button;
interface Nested {
[key: string]: (Nested & { title?: string }) | InputType;
}
type Props = {
id: string;
key?: string;
value: Record<string, unknown> | string | number | boolean;
type: Nested;
depth?: number;
};
let { id, key = "", value = $bindable(), type, depth = 0 }: Props = $props();
function isNodeInput(v: InputType | Nested): v is InputType {
return v && "type" in v;
}
let internalValue = $state(Array.isArray(type?.[key]?.options) ? type[key]?.options?.indexOf(value?.[key]) : value?.[key]);
let openSections = localState("open-details", {});
let open = $state(openSections[id]);
if(depth > 0 && !isNodeInput(type[key])){
$effect(() => {
if(open !== undefined){}
openSections[id] = open;
});
}
$effect(() => {
if(key === "" || internalValue === undefined) return;
if(isNodeInput(type[key]) && Array.isArray(type[key]?.options) && typeof internalValue === "number"){
value[key] = type[key].options?.[internalValue];
}else{
value[key] = internalValue;
}
})
</script>
{#if key && isNodeInput(type?.[key]) }
<div class="input input-{type[key].type}">
{#if type[key].type === "button"}
<button onclick={() => console.log(type[key])}>
{type[key].label || key}
</button>
{:else}
<label for={id}>{type[key].label || key}</label>
<Input id={id} input={type[key]} bind:value={internalValue} />
{/if}
</div>
{:else}
{#if depth === 0}
{#each Object.keys(type).filter((key) => key !== "title") as childKey}
<NestedSettings
id={`${id}.${childKey}`}
key={childKey}
value={value as Record<string, unknown>}
type={type as Nested}
depth={depth + 1}
/>
{/each}
{#if depth > 0}
<hr />
{/if}
{:else if key && type?.[key]}
{#if depth > 0}
<hr />
{/if}
<details bind:open>
<summary>{type[key]?.title||key}</summary>
<div class="content">
{#each Object.keys(type[key]).filter((key) => key !== "title") as childKey}
<NestedSettings
id={`${id}.${childKey}`}
key={childKey}
value={value[key] as Record<string, unknown>}
type={type[key] as Nested}
depth={depth + 1}
/>
{/each}
</div>
</details>
{/if}
{/if}
<style>
summary {
cursor: pointer;
user-select: none;
margin-bottom: 1em;
}
details {
padding: 1em;
padding-bottom: 0;
}
.input {
margin-top: 15px;
margin-bottom: 15px;
display: flex;
flex-direction: column;
gap: 10px;
padding-left: 14px;
}
.input-boolean {
display: flex;
flex-direction: row;
align-items: center;
}
.input-boolean > label {
order: 2;
}
.first-level > .input {
padding-right: 1rem;
}
.first-level {
border-bottom: solid thin var(--outline);
}
.first-level > details {
border: none;
}
hr {
position: absolute;
margin: 0;
left: 0;
right: 0;
border: none;
border-bottom: solid thin var(--outline);
}
</style>

View File

@@ -0,0 +1,82 @@
<script lang="ts">
import type { Node, NodeInput } from "@nodes/types";
import NestedSettings from "$lib/settings/NestedSettings.svelte";
import type { GraphManager } from "$lib/graph-interface/graph-manager";
type Props = {
manager: GraphManager;
node: Node;
};
const { manager, node }: Props = $props();
const nodeDefinition = filterInputs(node.tmp?.type?.inputs);
function filterInputs(inputs?: Record<string, NodeInput>) {
const _inputs = $state.snapshot(inputs);
return Object.fromEntries(
Object.entries(structuredClone(_inputs ?? {}))
.filter(([_key, value]) => {
return value.hidden === true;
})
.map(([key, value]) => {
//@ts-ignore
value.__node_type = node?.tmp?.type.id;
//@ts-ignore
value.__node_input = key;
return [key, value];
}),
);
}
type Store = Record<string, number | number[]>;
let store = $state<Store>(createStore(node?.props, nodeDefinition));
function createStore(
props: Node["props"],
inputs: Record<string, NodeInput>,
): Store {
const store: Store = {};
Object.keys(inputs).forEach((key) => {
if (props) {
//@ts-ignore
store[key] = props[key] || inputs[key].value;
}
});
return store;
}
let lastPropsHash = "";
function updateNode() {
if (!node || !store) return;
let needsUpdate = false;
Object.keys(store).forEach((_key: string) => {
node.props = node.props || {};
const key = _key as keyof typeof store;
if (node && store) {
needsUpdate = true;
node.props[key] = store[key];
}
});
let propsHash = JSON.stringify(node.props);
if (propsHash === lastPropsHash) {
return;
}
lastPropsHash = propsHash;
if (needsUpdate) {
manager.execute();
}
}
$effect(() => {
if (store) {
updateNode();
}
});
</script>
<NestedSettings
id="activeNodeSettings"
bind:value={store}
type={nodeDefinition}
/>

View File

@@ -0,0 +1,24 @@
<script lang="ts">
import type { Node } from "@nodes/types";
import type { GraphManager } from "$lib/graph-interface/graph-manager";
import ActiveNodeSelected from "./ActiveNodeSelected.svelte";
type Props = {
manager: GraphManager;
node: Node | undefined;
};
const { manager, node }: Props = $props();
</script>
{#if node}
{#key node.id}
{#if node}
<ActiveNodeSelected {manager} {node} />
{:else}
<p class="mx-4">Active Node has no Settings</p>
{/if}
{/key}
{:else}
<p class="mx-4">No active node</p>
{/if}

View File

@@ -3,7 +3,6 @@
import type { OBJExporter } from "three/addons/exporters/OBJExporter.js"; import type { OBJExporter } from "three/addons/exporters/OBJExporter.js";
import type { GLTFExporter } from "three/addons/exporters/GLTFExporter.js"; import type { GLTFExporter } from "three/addons/exporters/GLTFExporter.js";
import FileSaver from "file-saver"; import FileSaver from "file-saver";
import { appSettings } from "../app-settings.svelte";
// Download // Download
const download = ( const download = (
@@ -52,8 +51,6 @@
// download .obj file // download .obj file
download(result, "plant", "text/plain", "obj"); download(result, "plant", "text/plain", "obj");
} }
</script> </script>
<div class="p-2"> <div class="p-2">

View File

@@ -0,0 +1,66 @@
<script lang="ts">
import type { createKeyMap } from "$lib/helpers/createKeyMap";
import { ShortCut } from "@nodes/ui";
import { get } from "svelte/store";
type Props = {
keymaps: {
keymap: ReturnType<typeof createKeyMap>;
title: string;
}[];
};
let { keymaps }: Props = $props();
console.log({ keymaps });
</script>
<table class="wrapper">
<tbody>
{#each keymaps as keymap}
<tr>
<td colspan="2">
<h3>{keymap.title}</h3>
</td>
</tr>
{#each get(keymap.keymap?.keys) as key}
<tr>
{#if key.description}
<td class="command-wrapper">
<ShortCut
alt={key.alt}
ctrl={key.ctrl}
shift={key.shift}
key={key.key}
/>
</td>
<td>{key.description}</td>
{/if}
</tr>
{/each}
{/each}
</tbody>
</table>
<style>
.wrapper {
padding: 1em;
}
h3 {
margin: 0;
}
.command-wrapper {
display: flex;
justify-content: right;
align-items: center;
}
td {
font-size: 0.9em;
margin: 0;
padding: 7px;
padding-left: 0;
align-items: center;
}
</style>

View File

@@ -4,23 +4,20 @@
import * as templates from "$lib/graph-templates"; import * as templates from "$lib/graph-templates";
import type { Graph, Node } from "@nodes/types"; import type { Graph, Node } from "@nodes/types";
import Viewer from "$lib/result-viewer/Viewer.svelte"; import Viewer from "$lib/result-viewer/Viewer.svelte";
import Settings from "$lib/settings/Settings.svelte";
import { AppSettingTypes, AppSettings } from "$lib/settings/app-settings";
import { import {
appSettings as _appSettings, appSettings,
AppSettingTypes as _AppSettingTypes, AppSettingTypes,
} from "$lib/settings/app-settings.svelte"; } from "$lib/settings/app-settings.svelte";
import { writable } from "svelte/store"; import Keymap from "$lib/sidebar/panels/Keymap.svelte";
import Keymap from "$lib/settings/panels/Keymap.svelte"; import Sidebar from "$lib/sidebar/Sidebar.svelte";
import { createKeyMap } from "$lib/helpers/createKeyMap"; import { createKeyMap } from "$lib/helpers/createKeyMap";
import NodeStore from "$lib/node-store/NodeStore.svelte"; import NodeStore from "$lib/node-store/NodeStore.svelte";
import ActiveNodeSettings from "$lib/settings/panels/ActiveNodeSettings.svelte"; import ActiveNodeSettings from "$lib/sidebar/panels/ActiveNodeSettings.svelte";
import PerformanceViewer from "$lib/performance/PerformanceViewer.svelte"; import PerformanceViewer from "$lib/performance/PerformanceViewer.svelte";
import Panel from "$lib/settings/Panel.svelte"; import Panel from "$lib/sidebar/Panel.svelte";
import GraphSettings from "$lib/settings/panels/GraphSettings.svelte"; import NestedSettings from "$lib/settings/NestedSettings.svelte";
import NestedSettings from "$lib/settings/panels/NestedSettings.svelte";
import type { Group } from "three"; import type { Group } from "three";
import ExportSettings from "$lib/settings/panels/ExportSettings.svelte"; import ExportSettings from "$lib/sidebar/panels/ExportSettings.svelte";
import { import {
MemoryRuntimeCache, MemoryRuntimeCache,
WorkerRuntimeExecutor, WorkerRuntimeExecutor,
@@ -28,37 +25,42 @@
} from "$lib/runtime"; } from "$lib/runtime";
import { IndexDBCache, RemoteNodeRegistry } from "@nodes/registry"; import { IndexDBCache, RemoteNodeRegistry } from "@nodes/registry";
import { createPerformanceStore } from "@nodes/utils"; import { createPerformanceStore } from "@nodes/utils";
import BenchmarkPanel from "$lib/settings/panels/BenchmarkPanel.svelte"; import BenchmarkPanel from "$lib/sidebar/panels/BenchmarkPanel.svelte";
import { debounceAsyncFunction } from "$lib/helpers"; import { debounceAsyncFunction } from "$lib/helpers";
import { onMount } from "svelte";
let performanceStore = createPerformanceStore(); let performanceStore = createPerformanceStore();
const registryCache = new IndexDBCache("node-registry"); const registryCache = new IndexDBCache("node-registry");
const nodeRegistry = new RemoteNodeRegistry(""); const nodeRegistry = new RemoteNodeRegistry(
nodeRegistry.cache = registryCache; "https://node-store.app.max-richter.dev",
registryCache,
);
const workerRuntime = new WorkerRuntimeExecutor(); const workerRuntime = new WorkerRuntimeExecutor();
const runtimeCache = new MemoryRuntimeCache(); const runtimeCache = new MemoryRuntimeCache();
const memoryRuntime = new MemoryRuntimeExecutor(nodeRegistry, runtimeCache); const memoryRuntime = new MemoryRuntimeExecutor(nodeRegistry, runtimeCache);
memoryRuntime.perf = performanceStore; memoryRuntime.perf = performanceStore;
$: runtime = $AppSettings.useWorker ? workerRuntime : memoryRuntime; const runtime = $derived(
appSettings.debug.useWorker ? workerRuntime : memoryRuntime,
);
let activeNode: Node | undefined; let activeNode = $state<Node | undefined>(undefined);
let scene: Group; let scene = $state<Group>(null!);
let updateViewerResult: (result: Int32Array) => void;
let graph = localStorage.getItem("graph") let graph = localStorage.getItem("graph")
? JSON.parse(localStorage.getItem("graph")!) ? JSON.parse(localStorage.getItem("graph")!)
: templates.defaultPlant; : templates.defaultPlant;
let graphInterface: ReturnType<typeof GraphInterface>; let graphInterface = $state<ReturnType<typeof GraphInterface>>(null!);
$: manager = graphInterface?.manager; let viewerComponent = $state<ReturnType<typeof Viewer>>();
$: managerStatus = manager?.status; const manager = $derived(graphInterface?.manager);
$: keymap = graphInterface?.keymap; const managerStatus = $derived(manager?.status);
async function randomGenerate() { async function randomGenerate() {
if (!manager) return;
const g = manager.serialize(); const g = manager.serialize();
const s = { ...$graphSettings, randomSeed: true }; const s = { ...graphSettings, randomSeed: true };
await handleUpdate(g, s); await handleUpdate(g, s);
} }
@@ -69,18 +71,20 @@
callback: randomGenerate, callback: randomGenerate,
}, },
]); ]);
let graphSettings = writable<Record<string, any>>({}); let graphSettings = $state<Record<string, any>>({});
let graphSettingTypes = {}; let graphSettingTypes = $state({
randomSeed: { type: "boolean", value: false },
});
const handleUpdate = debounceAsyncFunction( const handleUpdate = debounceAsyncFunction(
async (g: Graph, s: Record<string, any>) => { async (g: Graph, s: Record<string, any> = graphSettings) => {
performanceStore.startRun(); performanceStore.startRun();
try { try {
let a = performance.now(); let a = performance.now();
const graphResult = await runtime.execute(g, s); const graphResult = await runtime.execute(g, $state.snapshot(s));
let b = performance.now(); let b = performance.now();
if ($AppSettings.useWorker) { if (appSettings.debug.useWorker) {
let perfData = await runtime.getPerformanceData(); let perfData = await runtime.getPerformanceData();
let lastRun = perfData?.at(-1); let lastRun = perfData?.at(-1);
if (lastRun?.total) { if (lastRun?.total) {
@@ -94,7 +98,7 @@
} }
} }
updateViewerResult(graphResult); viewerComponent?.update(graphResult);
} catch (error) { } catch (error) {
console.log("errors", error); console.log("errors", error);
} finally { } finally {
@@ -103,32 +107,35 @@
}, },
); );
$: if (AppSettings) { // $ if (AppSettings) {
//@ts-ignore // //@ts-ignore
AppSettingTypes.debug.stressTest.loadGrid.callback = () => { // AppSettingTypes.debug.stressTest.loadGrid.callback = () => {
graph = templates.grid($AppSettings.amount, $AppSettings.amount); // graph = templates.grid($AppSettings.amount, $AppSettings.amount);
}; // };
//@ts-ignore // //@ts-ignore
AppSettingTypes.debug.stressTest.loadTree.callback = () => { // AppSettingTypes.debug.stressTest.loadTree.callback = () => {
graph = templates.tree($AppSettings.amount); // graph = templates.tree($AppSettings.amount);
}; // };
//@ts-ignore // //@ts-ignore
AppSettingTypes.debug.stressTest.lottaFaces.callback = () => { // AppSettingTypes.debug.stressTest.lottaFaces.callback = () => {
graph = templates.lottaFaces; // graph = templates.lottaFaces;
}; // };
//@ts-ignore // //@ts-ignore
AppSettingTypes.debug.stressTest.lottaNodes.callback = () => { // AppSettingTypes.debug.stressTest.lottaNodes.callback = () => {
graph = templates.lottaNodes; // graph = templates.lottaNodes;
}; // };
//@ts-ignore // //@ts-ignore
AppSettingTypes.debug.stressTest.lottaNodesAndFaces.callback = () => { // AppSettingTypes.debug.stressTest.lottaNodesAndFaces.callback = () => {
graph = templates.lottaNodesAndFaces; // graph = templates.lottaNodesAndFaces;
}; // };
} // }
function handleSave(graph: Graph) { function handleSave(graph: Graph) {
localStorage.setItem("graph", JSON.stringify(graph)); localStorage.setItem("graph", JSON.stringify(graph));
} }
onMount(() => {
handleUpdate(graph);
});
</script> </script>
<svelte:document on:keydown={applicationKeymap.handleKeyboardEvent} /> <svelte:document on:keydown={applicationKeymap.handleKeyboardEvent} />
@@ -137,10 +144,10 @@
<Grid.Row> <Grid.Row>
<Grid.Cell> <Grid.Cell>
<Viewer <Viewer
perf={performanceStore}
bind:scene bind:scene
bind:update={updateViewerResult} bind:this={viewerComponent}
centerCamera={$AppSettings.centerCamera} perf={performanceStore}
centerCamera={appSettings.centerCamera}
/> />
</Grid.Cell> </Grid.Cell>
<Grid.Cell> <Grid.Cell>
@@ -149,21 +156,21 @@
bind:this={graphInterface} bind:this={graphInterface}
{graph} {graph}
registry={nodeRegistry} registry={nodeRegistry}
showGrid={appSettings.nodeInterface.showNodeGrid}
snapToGrid={appSettings.nodeInterface.snapToGrid}
bind:activeNode bind:activeNode
showGrid={$AppSettings.showNodeGrid} bind:showHelp={appSettings.nodeInterface.showHelp}
snapToGrid={$AppSettings.snapToGrid}
bind:showHelp={$AppSettings.showHelp}
bind:settings={graphSettings} bind:settings={graphSettings}
bind:settingTypes={graphSettingTypes} bind:settingTypes={graphSettingTypes}
onresult={(result) => handleUpdate(result, $graphSettings)} onresult={(result) => handleUpdate(result)}
onsave={(graph) => handleSave(graph)} onsave={(graph) => handleSave(graph)}
/> />
<Settings> <Sidebar>
<Panel id="general" title="General" icon="i-tabler-settings"> <Panel id="general" title="General" icon="i-tabler-settings">
<NestedSettings <NestedSettings
id="general" id="general"
value={_appSettings} value={appSettings}
type={_AppSettingTypes} type={AppSettingTypes}
/> />
</Panel> </Panel>
<Panel <Panel
@@ -171,10 +178,12 @@
title="Keyboard Shortcuts" title="Keyboard Shortcuts"
icon="i-tabler-keyboard" icon="i-tabler-keyboard"
> >
<Keymap title="Application" keymap={applicationKeymap} /> <Keymap
{#if keymap} keymaps={[
<Keymap title="Node-Editor" {keymap} /> { keymap: applicationKeymap, title: "Application" },
{/if} { keymap: graphInterface.keymap, title: "Node-Editor" },
]}
/>
</Panel> </Panel>
<Panel id="exports" title="Exporter" icon="i-tabler-package-export"> <Panel id="exports" title="Exporter" icon="i-tabler-package-export">
<ExportSettings {scene} /> <ExportSettings {scene} />
@@ -191,7 +200,7 @@
id="performance" id="performance"
title="Performance" title="Performance"
classes="text-red-400" classes="text-red-400"
hidden={!$AppSettings.showPerformancePanel} hidden={!appSettings.debug.showPerformancePanel}
icon="i-tabler-brand-speedtest" icon="i-tabler-brand-speedtest"
> >
{#if $performanceStore} {#if $performanceStore}
@@ -202,7 +211,7 @@
id="benchmark" id="benchmark"
title="Benchmark" title="Benchmark"
classes="text-red-400" classes="text-red-400"
hidden={!$AppSettings.showBenchmarkPanel} hidden={!appSettings.debug.showBenchmarkPanel}
icon="i-tabler-graph" icon="i-tabler-graph"
> >
<BenchmarkPanel run={randomGenerate} /> <BenchmarkPanel run={randomGenerate} />
@@ -213,9 +222,11 @@
classes="text-blue-400" classes="text-blue-400"
icon="i-custom-graph" icon="i-custom-graph"
> >
{#if Object.keys(graphSettingTypes).length > 0} <NestedSettings
<GraphSettings type={graphSettingTypes} store={graphSettings} /> id="graph-settings"
{/if} type={graphSettingTypes}
bind:value={graphSettings}
/>
</Panel> </Panel>
<Panel <Panel
id="active-node" id="active-node"
@@ -225,7 +236,7 @@
> >
<ActiveNodeSettings {manager} node={activeNode} /> <ActiveNodeSettings {manager} node={activeNode} />
</Panel> </Panel>
</Settings> </Sidebar>
{/key} {/key}
</Grid.Cell> </Grid.Cell>
</Grid.Row> </Grid.Row>

View File

@@ -17,15 +17,15 @@
"type": "float", "type": "float",
"hidden": true, "hidden": true,
"min": 0, "min": 0,
"value": 0.5, "max": 1,
"max": 1 "value": 0.5
}, },
"depth": { "depth": {
"type": "integer", "type": "integer",
"min": 1, "min": 1,
"max": 10, "max": 10,
"value": 1, "hidden": true,
"hidden": true "value": 1
} }
} }
} }

View File

@@ -8,11 +8,5 @@
"build:deploy": "pnpm build", "build:deploy": "pnpm build",
"dev": "pnpm -r --filter 'app' --filter './packages/node-registry' dev" "dev": "pnpm -r --filter 'app' --filter './packages/node-registry' dev"
}, },
"packageManager": "pnpm@9.11.0+sha512.0a203ffaed5a3f63242cd064c8fb5892366c103e328079318f78062f24ea8c9d50bc6a47aa3567cabefd824d170e78fa2745ed1f16b132e16436146b7688f19b", "packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c"
"dependencies": {
"@types/pg": "^8.11.10",
"drizzle-kit": "^0.30.1",
"drizzle-orm": "^0.38.2",
"pg": "^8.13.1"
}
} }

View File

@@ -13,7 +13,7 @@ export class RemoteNodeRegistry implements NodeRegistry {
fetch: typeof fetch = globalThis.fetch.bind(globalThis); fetch: typeof fetch = globalThis.fetch.bind(globalThis);
constructor(private url: string) { } constructor(private url: string, private cache?: AsyncCache<ArrayBuffer>) { }
async fetchUsers() { async fetchUsers() {
const response = await this.fetch(`${this.url}/nodes/users.json`); const response = await this.fetch(`${this.url}/nodes/users.json`);
@@ -24,7 +24,7 @@ export class RemoteNodeRegistry implements NodeRegistry {
} }
async fetchUser(userId: `${string}`) { async fetchUser(userId: `${string}`) {
const response = await this.fetch(`${this.url}/nodes/${userId}.json`); const response = await this.fetch(`${this.url}/user/${userId}.json`);
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to load user ${userId}`); throw new Error(`Failed to load user ${userId}`);
} }

View File

@@ -0,0 +1,19 @@
{
"name": "store-client",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"generate": "openapi-ts -i ../../store/openapi.json -o src/client -c @hey-api/client-fetch"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@hey-api/client-fetch": "^0.5.6"
},
"devDependencies": {
"@hey-api/openapi-ts": "^0.60.0"
}
}

View File

@@ -0,0 +1,3 @@
// This file is auto-generated by @hey-api/openapi-ts
export * from './sdk.gen';
export * from './types.gen';

View File

@@ -0,0 +1,55 @@
// This file is auto-generated by @hey-api/openapi-ts
import { createClient, createConfig, type OptionsLegacyParser } from '@hey-api/client-fetch';
import type { GetV1NodesByUserJsonData, GetV1NodesByUserJsonError, GetV1NodesByUserJsonResponse, GetV1NodesByUserBySystemJsonData, GetV1NodesByUserBySystemJsonError, GetV1NodesByUserBySystemJsonResponse, GetV1NodesByUserBySystemByNodeIdby-.+jsonData, GetV1NodesByUserBySystemByNodeIdby-.+jsonError, GetV1NodesByUserBySystemByNodeIdby-.+jsonResponse, GetV1NodesByUserBySystemByNodeIdby-.+WasmData, GetV1NodesByUserBySystemByNodeIdby-.+WasmError, GetV1NodesByUserBySystemByNodeIdby-.+WasmResponse, PostV1NodesError, PostV1NodesResponse, GetV1UsersUsersJsonError, GetV1UsersUsersJsonResponse, GetV1UsersByUserIdJsonData, GetV1UsersByUserIdJsonError, GetV1UsersByUserIdJsonResponse } from './types.gen';
export const client = createClient(createConfig());
export const getV1NodesByUserJson = <ThrowOnError extends boolean = false>(options: OptionsLegacyParser<GetV1NodesByUserJsonData, ThrowOnError>) => {
return (options?.client ?? client).get<GetV1NodesByUserJsonResponse, GetV1NodesByUserJsonError, ThrowOnError>({
...options,
url: '/v1/nodes/{user}.json'
});
};
export const getV1NodesByUserBySystemJson = <ThrowOnError extends boolean = false>(options: OptionsLegacyParser<GetV1NodesByUserBySystemJsonData, ThrowOnError>) => {
return (options?.client ?? client).get<GetV1NodesByUserBySystemJsonResponse, GetV1NodesByUserBySystemJsonError, ThrowOnError>({
...options,
url: '/v1/nodes/{user}/{system}.json'
});
};
export const getV1NodesByUserBySystemByNodeIdby-.+Json = <ThrowOnError extends boolean = false>(options: OptionsLegacyParser<GetV1NodesByUserBySystemByNodeIdby-.+jsonData, ThrowOnError>) => {
return (options?.client ?? client).get<GetV1NodesByUserBySystemByNodeIdby-.+jsonResponse, GetV1NodesByUserBySystemByNodeIdby-.+jsonError, ThrowOnError>({
...options,
url: '/v1/nodes/{user}/{system}/{nodeId}{.+\\.json}'
});
};
export const getV1NodesByUserBySystemByNodeIdby-.+Wasm = <ThrowOnError extends boolean = false>(options: OptionsLegacyParser<GetV1NodesByUserBySystemByNodeIdby-.+WasmData, ThrowOnError>) => {
return (options?.client ?? client).get<GetV1NodesByUserBySystemByNodeIdby-.+WasmResponse, GetV1NodesByUserBySystemByNodeIdby-.+WasmError, ThrowOnError>({
...options,
url: '/v1/nodes/{user}/{system}/{nodeId}{.+\\.wasm}'
});
};
export const postV1Nodes = <ThrowOnError extends boolean = false>(options?: OptionsLegacyParser<unknown, ThrowOnError>) => {
return (options?.client ?? client).post<PostV1NodesResponse, PostV1NodesError, ThrowOnError>({
...options,
url: '/v1/nodes'
});
};
export const getV1UsersUsersJson = <ThrowOnError extends boolean = false>(options?: OptionsLegacyParser<unknown, ThrowOnError>) => {
return (options?.client ?? client).get<GetV1UsersUsersJsonResponse, GetV1UsersUsersJsonError, ThrowOnError>({
...options,
url: '/v1/users/users.json'
});
};
export const getV1UsersByUserIdJson = <ThrowOnError extends boolean = false>(options?: OptionsLegacyParser<GetV1UsersByUserIdJsonData, ThrowOnError>) => {
return (options?.client ?? client).get<GetV1UsersByUserIdJsonResponse, GetV1UsersByUserIdJsonError, ThrowOnError>({
...options,
url: '/v1/users/{userId}.json'
});
};

View File

@@ -0,0 +1,173 @@
// This file is auto-generated by @hey-api/openapi-ts
export type NodeDefinition = {
id: string;
inputs?: {
[key: string]: NodeInput;
};
outputs?: Array<(string)>;
meta?: {
description?: string;
title?: string;
};
};
export type NodeInput = {
internal?: boolean;
external?: boolean;
setting?: string;
label?: string;
description?: string;
accepts?: Array<(string)>;
hidden?: boolean;
type: 'seed';
value?: number;
} | {
internal?: boolean;
external?: boolean;
setting?: string;
label?: string;
description?: string;
accepts?: Array<(string)>;
hidden?: boolean;
type: 'boolean';
value?: boolean;
} | {
internal?: boolean;
external?: boolean;
setting?: string;
label?: string;
description?: string;
accepts?: Array<(string)>;
hidden?: boolean;
type: 'float';
element?: 'slider';
value?: number;
min?: number;
max?: number;
step?: number;
} | {
internal?: boolean;
external?: boolean;
setting?: string;
label?: string;
description?: string;
accepts?: Array<(string)>;
hidden?: boolean;
type: 'integer';
element?: 'slider';
value?: number;
min?: number;
max?: number;
} | {
internal?: boolean;
external?: boolean;
setting?: string;
label?: string;
description?: string;
accepts?: Array<(string)>;
hidden?: boolean;
type: 'select';
options?: Array<(string)>;
value?: number;
} | {
internal?: boolean;
external?: boolean;
setting?: string;
label?: string;
description?: string;
accepts?: Array<(string)>;
hidden?: boolean;
type: 'vec3';
value?: Array<(number)>;
} | {
internal?: boolean;
external?: boolean;
setting?: string;
label?: string;
description?: string;
accepts?: Array<(string)>;
hidden?: boolean;
type: 'geometry';
} | {
internal?: boolean;
external?: boolean;
setting?: string;
label?: string;
description?: string;
accepts?: Array<(string)>;
hidden?: boolean;
type: 'path';
};
export type type = 'seed';
export type element = 'slider';
export type User = {
id: string;
name: string;
};
export type GetV1NodesByUserJsonData = {
path: {
user: string;
};
};
export type GetV1NodesByUserJsonResponse = (Array<NodeDefinition>);
export type GetV1NodesByUserJsonError = unknown;
export type GetV1NodesByUserBySystemJsonData = {
path: {
system?: string;
user: string;
};
};
export type GetV1NodesByUserBySystemJsonResponse = (Array<NodeDefinition>);
export type GetV1NodesByUserBySystemJsonError = unknown;
export type GetV1NodesByUserBySystemByNodeIdby-.+jsonData = {
path: {
nodeId: string;
system: string;
user: string;
};
};
export type GetV1NodesByUserBySystemByNodeIdby-.+jsonResponse = (NodeDefinition);
export type GetV1NodesByUserBySystemByNodeIdby-.+jsonError = unknown;
export type GetV1NodesByUserBySystemByNodeIdby-.+WasmData = {
path: {
nodeId: string;
system: string;
user: string;
};
};
export type GetV1NodesByUserBySystemByNodeIdby-.+WasmResponse = (unknown);
export type GetV1NodesByUserBySystemByNodeIdby-.+WasmError = unknown;
export type PostV1NodesResponse = (NodeDefinition);
export type PostV1NodesError = unknown;
export type GetV1UsersUsersJsonResponse = (Array<User>);
export type GetV1UsersUsersJsonError = unknown;
export type GetV1UsersByUserIdJsonData = {
path?: {
userId?: string;
};
};
export type GetV1UsersByUserIdJsonResponse = (User);
export type GetV1UsersByUserIdJsonError = unknown;

View File

@@ -34,6 +34,6 @@
} }
.content { .content {
padding-left: 12px; /* padding-left: 12px; */
} }
</style> </style>

3642
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

13
store/Dockerfile Normal file
View File

@@ -0,0 +1,13 @@
FROM denoland/deno:alpine
ARG GIT_REVISION
ENV DENO_DEPLOYMENT_ID=${GIT_REVISION}
WORKDIR /app
COPY . .
RUN deno cache src/server.ts
EXPOSE 8000
CMD ["task", "run"]

View File

@@ -44,7 +44,8 @@ for await (const dir of dirs) {
async function postNode(node: Node) { async function postNode(node: Node) {
const wasmContent = await Deno.readFile(node.path); const wasmContent = await Deno.readFile(node.path);
const url = `http://localhost:8000/v1/nodes`; const url = `http://localhost:8000/nodes`;
// const url = "https://node-store.app.max-richter.dev/nodes";
const res = await fetch(url, { const res = await fetch(url, {
method: "POST", method: "POST",
@@ -55,7 +56,7 @@ async function postNode(node: Node) {
console.log(`Uploaded ${node.id}`); console.log(`Uploaded ${node.id}`);
} else { } else {
const text = await res.text(); const text = await res.text();
console.log(`Failed to upload ${node.id}: ${text}`); console.log(`Failed to upload ${node.id}: ${res.status} ${text}`);
} }
} }

View File

@@ -9,7 +9,7 @@ services:
volumes: volumes:
- .:/app - .:/app
- deno-cache:/deno-dir/ - deno-cache:/deno-dir/
command: run --allow-net --allow-env --allow-read --watch src/main.ts command: task dev
depends_on: depends_on:
- db - db

View File

@@ -1,6 +1,7 @@
{ {
"tasks": { "tasks": {
"dev": "deno run --watch main.ts", "dev": "deno run -A --watch src/server.ts",
"run": "deno run -A src/server.ts",
"test": "deno run vitest", "test": "deno run vitest",
"drizzle": "podman-compose exec app deno --env -A --node-modules-dir npm:drizzle-kit", "drizzle": "podman-compose exec app deno --env -A --node-modules-dir npm:drizzle-kit",
"upload": "deno run --allow-read --allow-net bin/upload.ts" "upload": "deno run --allow-read --allow-net bin/upload.ts"
@@ -11,6 +12,7 @@
"@hono/zod-openapi": "npm:@hono/zod-openapi@^0.18.3", "@hono/zod-openapi": "npm:@hono/zod-openapi@^0.18.3",
"@std/assert": "jsr:@std/assert@1", "@std/assert": "jsr:@std/assert@1",
"@types/pg": "npm:@types/pg@^8.11.10", "@types/pg": "npm:@types/pg@^8.11.10",
"drizzle-kit": "npm:drizzle-kit@^0.30.1",
"drizzle-orm": "npm:drizzle-orm@^0.38.2", "drizzle-orm": "npm:drizzle-orm@^0.38.2",
"hono": "npm:hono@^4.6.14", "hono": "npm:hono@^4.6.14",
"pg": "npm:pg@^8.13.1", "pg": "npm:pg@^8.13.1",

2
store/deno.lock generated
View File

@@ -15,6 +15,7 @@
"npm:@types/node@*": "22.5.4", "npm:@types/node@*": "22.5.4",
"npm:@types/pg@^8.11.10": "8.11.10", "npm:@types/pg@^8.11.10": "8.11.10",
"npm:drizzle-kit@*": "0.30.1_esbuild@0.19.12", "npm:drizzle-kit@*": "0.30.1_esbuild@0.19.12",
"npm:drizzle-kit@~0.30.1": "0.30.1_esbuild@0.19.12",
"npm:drizzle-orm@~0.38.2": "0.38.2_@types+pg@8.11.10_pg@8.13.1", "npm:drizzle-orm@~0.38.2": "0.38.2_@types+pg@8.11.10_pg@8.13.1",
"npm:hono@^4.6.14": "4.6.14", "npm:hono@^4.6.14": "4.6.14",
"npm:pg@^8.13.1": "8.13.1", "npm:pg@^8.13.1": "8.13.1",
@@ -871,6 +872,7 @@
"npm:@hono/swagger-ui@0.5", "npm:@hono/swagger-ui@0.5",
"npm:@hono/zod-openapi@~0.18.3", "npm:@hono/zod-openapi@~0.18.3",
"npm:@types/pg@^8.11.10", "npm:@types/pg@^8.11.10",
"npm:drizzle-kit@~0.30.1",
"npm:drizzle-orm@~0.38.2", "npm:drizzle-orm@~0.38.2",
"npm:hono@^4.6.14", "npm:hono@^4.6.14",
"npm:pg@^8.13.1", "npm:pg@^8.13.1",

View File

@@ -1,10 +0,0 @@
CREATE TABLE "nodes" (
"id" serial NOT NULL,
"content" "bytea" NOT NULL,
"definition" json NOT NULL
);
--> statement-breakpoint
CREATE TABLE "users" (
"id" serial PRIMARY KEY NOT NULL,
"name" text
);

View File

@@ -0,0 +1,25 @@
CREATE TABLE "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" text NOT NULL,
CONSTRAINT "users_name_unique" UNIQUE("name")
);
--> statement-breakpoint
CREATE TABLE "nodes" (
"id" serial PRIMARY KEY NOT NULL,
"userId" varchar NOT NULL,
"createdAt" timestamp DEFAULT now(),
"systemId" varchar NOT NULL,
"nodeId" varchar NOT NULL,
"content" "bytea" NOT NULL,
"definition" json NOT NULL,
"hash" varchar(16) NOT NULL,
"previous" varchar(16),
CONSTRAINT "nodes_hash_unique" UNIQUE("hash")
);
--> statement-breakpoint
ALTER TABLE "nodes" ADD CONSTRAINT "nodes_userId_users_name_fk" FOREIGN KEY ("userId") REFERENCES "public"."users"("name") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "nodes" ADD CONSTRAINT "node_previous_fk" FOREIGN KEY ("previous") REFERENCES "public"."nodes"("hash") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "user_id_idx" ON "nodes" USING btree ("userId");--> statement-breakpoint
CREATE INDEX "system_id_idx" ON "nodes" USING btree ("systemId");--> statement-breakpoint
CREATE INDEX "node_id_idx" ON "nodes" USING btree ("nodeId");--> statement-breakpoint
CREATE INDEX "hash_idx" ON "nodes" USING btree ("hash");

View File

@@ -1,9 +1,43 @@
{ {
"id": "53dea8d7-01be-4983-ac75-9de9c9a7f592", "id": "15ad729d-5756-4c06-87ed-cb8b721201f9",
"prevId": "00000000-0000-0000-0000-000000000000", "prevId": "00000000-0000-0000-0000-000000000000",
"version": "7", "version": "7",
"dialect": "postgresql", "dialect": "postgresql",
"tables": { "tables": {
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"users_name_unique": {
"name": "users_name_unique",
"nullsNotDistinct": false,
"columns": [
"name"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.nodes": { "public.nodes": {
"name": "nodes", "name": "nodes",
"schema": "", "schema": "",
@@ -11,6 +45,31 @@
"id": { "id": {
"name": "id", "name": "id",
"type": "serial", "type": "serial",
"primaryKey": true,
"notNull": true
},
"userId": {
"name": "userId",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"systemId": {
"name": "systemId",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"nodeId": {
"name": "nodeId",
"type": "varchar",
"primaryKey": false, "primaryKey": false,
"notNull": true "notNull": true
}, },
@@ -25,37 +84,120 @@
"type": "json", "type": "json",
"primaryKey": false, "primaryKey": false,
"notNull": true "notNull": true
}
}, },
"indexes": {}, "hash": {
"foreignKeys": {}, "name": "hash",
"compositePrimaryKeys": {}, "type": "varchar(16)",
"uniqueConstraints": {}, "primaryKey": false,
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true "notNull": true
}, },
"name": { "previous": {
"name": "name", "name": "previous",
"type": "text", "type": "varchar(16)",
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": false
} }
}, },
"indexes": {}, "indexes": {
"foreignKeys": {}, "user_id_idx": {
"name": "user_id_idx",
"columns": [
{
"expression": "userId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"system_id_idx": {
"name": "system_id_idx",
"columns": [
{
"expression": "systemId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"node_id_idx": {
"name": "node_id_idx",
"columns": [
{
"expression": "nodeId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"hash_idx": {
"name": "hash_idx",
"columns": [
{
"expression": "hash",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"nodes_userId_users_name_fk": {
"name": "nodes_userId_users_name_fk",
"tableFrom": "nodes",
"tableTo": "users",
"columnsFrom": [
"userId"
],
"columnsTo": [
"name"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"node_previous_fk": {
"name": "node_previous_fk",
"tableFrom": "nodes",
"tableTo": "nodes",
"columnsFrom": [
"previous"
],
"columnsTo": [
"hash"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {}, "compositePrimaryKeys": {},
"uniqueConstraints": {}, "uniqueConstraints": {
"nodes_hash_unique": {
"name": "nodes_hash_unique",
"nullsNotDistinct": false,
"columns": [
"hash"
]
}
},
"policies": {}, "policies": {},
"checkConstraints": {}, "checkConstraints": {},
"isRLSEnabled": false "isRLSEnabled": false

View File

@@ -5,8 +5,8 @@
{ {
"idx": 0, "idx": 0,
"version": "7", "version": "7",
"when": 1734446124519, "when": 1734703963242,
"tag": "0000_dark_squirrel_girl", "tag": "0000_known_kid_colt",
"breakpoints": true "breakpoints": true
} }
] ]

1
store/openapi.json Normal file

File diff suppressed because one or more lines are too long

View File

@@ -2,14 +2,21 @@ import { drizzle } from "drizzle-orm/node-postgres";
import pg from "pg"; import pg from "pg";
import * as schema from "./schema.ts"; import * as schema from "./schema.ts";
import { migrate } from "drizzle-orm/node-postgres/migrator";
// Use pg driver. // Use pg driver.
const { Pool } = pg; const { Pool } = pg;
// Instantiate Drizzle client with pg driver and schema. // Instantiate Drizzle client with pg driver and schema.
export const db = drizzle({ export const db = drizzle({
client: new Pool({ client: new Pool({
max: 20,
connectionString: Deno.env.get("DATABASE_URL"), connectionString: Deno.env.get("DATABASE_URL"),
}), }),
schema, schema,
}); });
export async function migrateDb() {
await migrate(db, { migrationsFolder: "drizzle" });
console.log("Database migrated");
}

View File

@@ -1,2 +1,2 @@
export * from "../routes/user/user.schema.ts"; export * from "../routes/user/user.schema.ts";
export * from "../routes/node/schemas/node.schema.ts"; export * from "../routes/node/node.schema.ts";

View File

@@ -1,25 +0,0 @@
import { OpenAPIHono } from "@hono/zod-openapi";
import { router } from "./routes/router.ts";
import { createUser } from "./routes/user/user.service.ts";
import { swaggerUI } from "@hono/swagger-ui";
import { logger } from "hono/logger";
import { cors } from "hono/cors";
await createUser("max");
const app = new OpenAPIHono();
app.use("/v1/*", cors());
app.use(logger());
app.route("v1", router);
app.doc("/openapi.json", {
openapi: "3.0.0",
info: {
version: "1.0.0",
title: "Nodarium API",
},
});
app.get("/ui", swaggerUI({ url: "/openapi.json" }));
Deno.serve(app.fetch);

View File

@@ -0,0 +1,33 @@
import { StatusCode } from "hono";
export class CustomError extends Error {
constructor(public status: StatusCode, message: string) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
export class NodeNotFoundError extends CustomError {
constructor() {
super(404, "Node not found");
}
}
export class InvalidNodeDefinitionError extends CustomError {
constructor() {
super(400, "Invalid node definition");
}
}
export class WorkerTimeoutError extends CustomError {
constructor() {
super(500, "Worker timed out");
}
}
export class UnknownWorkerResponseError extends CustomError {
constructor() {
super(500, "Unknown worker response");
}
}

View File

@@ -1,172 +1,352 @@
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi"; import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
import { HTTPException } from "hono/http-exception"; import { HTTPException } from "hono/http-exception";
import { idRegex, NodeDefinitionSchema } from "./schemas/types.ts"; import { idRegex, NodeDefinitionSchema } from "./validations/types.ts";
import * as service from "./node.service.ts"; import * as service from "./node.service.ts";
import { bodyLimit } from "hono/body-limit"; import { bodyLimit } from "hono/body-limit";
import { ZodSchema } from "zod";
import { CustomError } from "./errors.ts";
const nodeRouter = new OpenAPIHono(); const nodeRouter = new OpenAPIHono();
const SingleParam = (name: string) => const createParamSchema = (name: string) =>
z z
.string() .string()
.min(3) .min(3)
.max(20) .max(20)
.refine( .refine(
(value) => idRegex.test(value), (value) => idRegex.test(value),
"Name should contain only alphabets", `${name} must contain only letters, numbers, "-", or "_"`,
) )
.openapi({ param: { name, in: "path" } }); .openapi({ param: { name, in: "path" } });
const ParamsSchema = z.object({ const createResponseSchema = <T extends ZodSchema>(
user: SingleParam("user"), description: string,
system: SingleParam("system"), schema: T,
nodeId: SingleParam("nodeId"), ) => ({
200: {
content: { "application/json": { schema } },
description,
},
}); });
const getUserNodesRoute = createRoute({ async function getNodeByVersion(
user: string,
system: string,
nodeId: string,
hash?: string,
) {
console.log("Get Node by Version", { user, system, nodeId, hash });
if (hash) {
if (nodeId.includes("wasm")) {
return await service.getNodeVersionWasm(
user,
system,
nodeId.replace(".wasm", ""),
hash,
);
} else {
const wasmContent = await service.getNodeVersion(
user,
system,
nodeId,
hash,
);
return wasmContent;
}
} else {
if (nodeId.includes(".wasm")) {
const [id, version] = nodeId.replace(/\.wasm$/, "").split("@");
console.log({ user, system, id, version });
if (version) {
return service.getNodeVersionWasm(user, system, id, version);
} else {
return service.getNodeWasmById(user, system, id);
}
} else {
const [id, version] = nodeId.replace(/\.json$/, "").split("@");
if (!version) {
return service.getNodeDefinitionById(user, system, id);
} else {
return await service.getNodeVersion(user, system, id, version);
}
}
}
}
nodeRouter.openapi(
createRoute({
method: "post",
path: "/",
responses: createResponseSchema(
"Create a single node",
NodeDefinitionSchema,
),
middleware: [
bodyLimit({
maxSize: 128 * 1024, // 128 KB
onError: (c) => c.text("Node content too large", 413),
}),
],
}),
async (c) => {
const buffer = await c.req.arrayBuffer();
const bytes = new Uint8Array(buffer);
try {
const node = await service.createNode(buffer, bytes);
return c.json(node);
} catch (error) {
if (error instanceof CustomError) {
throw new HTTPException(error.status, { message: error.message });
}
throw new HTTPException(500, { message: "Internal server error" });
}
},
);
nodeRouter.openapi(
createRoute({
method: "get", method: "get",
path: "/{user}.json", path: "/{user}.json",
request: { request: {
params: z.object({ params: z.object({
user: SingleParam("user"), user: createParamSchema("user").optional(),
}), }),
}, },
responses: { responses: createResponseSchema(
200: { "Retrieve nodes for a user",
content: { z.array(NodeDefinitionSchema),
"application/json": { ),
schema: z.array(NodeDefinitionSchema), }),
}, async (c) => {
}, const user = c.req.param("user.json").replace(/\.json$/, "");
description: "Retrieve a single node definition", try {
}, const nodes = await service.getNodeDefinitionsByUser(user);
},
});
nodeRouter.openapi(getUserNodesRoute, async (c) => {
const userId = c.req.param("user.json").replace(/\.json$/, "");
const nodes = await service.getNodeDefinitionsByUser(userId);
return c.json(nodes); return c.json(nodes);
}); } catch (error) {
if (error instanceof CustomError) {
throw new HTTPException(error.status, { message: error.message });
}
throw new HTTPException(500, { message: "Internal server error" });
}
},
);
const getNodeCollectionRoute = createRoute({ nodeRouter.openapi(
createRoute({
method: "get", method: "get",
path: "/{user}/{system}.json", path: "/{user}/{system}.json",
request: { request: {
params: z.object({ params: z.object({
user: SingleParam("user"), user: createParamSchema("user"),
system: SingleParam("system").optional(), system: createParamSchema("system").optional(),
}), }),
}, },
responses: { responses: createResponseSchema(
200: { "Retrieve nodes for a system",
content: { z.array(NodeDefinitionSchema),
"application/json": { ),
schema: z.array(NodeDefinitionSchema), }),
}, async (c) => {
},
description: "Retrieve a single node definition",
},
},
});
nodeRouter.openapi(getNodeCollectionRoute, async (c) => {
const { user } = c.req.valid("param"); const { user } = c.req.valid("param");
const nodeSystemId = c.req.param("system.json").replace(/\.json$/, ""); const system = c.req.param("system.json").replace(/\.json$/, "");
console.log("Get Nodes by System", { user, system });
const nodes = await service.getNodesBySystem(user, nodeSystemId); try {
return c.json(nodes); const nodes = await service.getNodesBySystem(user, system);
return c.json({
id: `${user}/${system}`,
nodes: nodes.map((n) => ({ id: n.id.split("@")[0] })),
}); });
} catch (error) {
const getNodeDefinitionRoute = createRoute({ if (error instanceof CustomError) {
method: "get", throw new HTTPException(error.status, { message: error.message });
path: "/{user}/{system}/{nodeId}{.+\\.json}", }
request: { throw new HTTPException(500, { message: "Internal server error" });
params: ParamsSchema, }
}, },
responses: {
200: {
content: {
"application/json": {
schema: NodeDefinitionSchema,
},
},
description: "Retrieve a single node definition",
},
},
});
nodeRouter.openapi(getNodeDefinitionRoute, async (c) => {
const { user, system, nodeId } = c.req.valid("param");
const node = await service.getNodeDefinitionById(
user,
system,
nodeId.replace(/\.json$/, ""),
); );
if (!node) { nodeRouter.openapi(
throw new HTTPException(404); createRoute({
method: "get",
path: "/{user}/{system}/{nodeId}.json",
request: {
params: z.object({
user: createParamSchema("user"),
system: createParamSchema("system"),
nodeId: createParamSchema("nodeId").optional(),
}),
},
responses: createResponseSchema(
"Retrieve a single node definition",
NodeDefinitionSchema,
),
}),
async (c) => {
const { user, system } = c.req.valid("param");
const nodeId = c.req.param("nodeId.json").replace(/\.json$/, "");
console.log("Get Node by Id", { user, system, nodeId });
try {
const res = await getNodeByVersion(user, system, nodeId);
if (res instanceof ArrayBuffer) {
c.header("Content-Type", "application/wasm");
return c.body(res);
} else {
return c.json(res);
} }
} catch (error) {
if (error instanceof CustomError) {
throw new HTTPException(error.status, { message: error.message });
}
throw new HTTPException(500, { message: "Internal server error" });
}
},
);
nodeRouter.openapi(
createRoute({
method: "get",
path: "/{user}/{system}/{nodeId}@{version}.json",
request: {
params: z.object({
user: createParamSchema("user"),
system: createParamSchema("system"),
nodeId: createParamSchema("nodeId"),
version: createParamSchema("version").optional(),
}),
},
responses: createResponseSchema(
"Retrieve a single node definition",
NodeDefinitionSchema,
),
}),
async (c) => {
const { user, system, nodeId } = c.req.valid("param");
const hash = c.req.param("version.json");
try {
const res = await getNodeByVersion(user, system, nodeId, hash);
if (res instanceof ArrayBuffer) {
c.header("Content-Type", "application/wasm");
return c.body(res);
} else {
return c.json(res);
}
} catch (error) {
if (error instanceof CustomError) {
throw new HTTPException(error.status, { message: error.message });
}
throw new HTTPException(500, { message: "Internal server error" });
}
},
);
nodeRouter.openapi(
createRoute({
method: "get",
path: "/{user}/{system}/{nodeId}/versions.json",
request: {
params: z.object({
user: createParamSchema("user"),
system: createParamSchema("system"),
nodeId: createParamSchema("nodeId"),
}),
},
responses: createResponseSchema(
"Retrieve a single node definition",
z.array(NodeDefinitionSchema),
),
}),
async (c) => {
const { user, system, nodeId } = c.req.valid("param");
try {
const node = await service.getNodeVersions(user, system, nodeId);
return c.json(node); return c.json(node);
}); } catch (error) {
if (error instanceof CustomError) {
const getNodeWasmRoute = createRoute({ throw new HTTPException(error.status, { message: error.message });
method: "get", }
path: "/{user}/{system}/{nodeId}{.+\\.wasm}", throw new HTTPException(500, { message: "Internal server error" });
request: { }
params: ParamsSchema,
}, },
responses: {
200: {
content: {
"application/wasm": {
schema: z.any(),
},
},
description: "Retrieve a single node",
},
},
});
nodeRouter.openapi(getNodeWasmRoute, async (c) => {
const { user, system, nodeId } = c.req.valid("param");
const wasmContent = await service.getNodeWasmById(
user,
system,
nodeId.replace(/\.wasm/, ""),
); );
c.header("Content-Type", "application/wasm"); nodeRouter.openapi(
createRoute({
return c.body(wasmContent); method: "get",
}); path: "/{user}/{system}/{nodeId}.wasm",
request: {
const createNodeRoute = createRoute({ params: z.object({
method: "post", user: createParamSchema("user"),
path: "/", system: createParamSchema("system"),
nodeId: createParamSchema("nodeId").optional(),
}),
},
responses: { responses: {
200: { 200: {
content: { content: { "application/wasm": { schema: z.any() } },
"application/json": { description: "Retrieve a node's WASM file",
schema: NodeDefinitionSchema,
}, },
}, },
description: "Create a single node",
},
},
middleware: [
bodyLimit({
maxSize: 128 * 1024, // 128kb
onError: (c) => {
return c.text("Node content too large", 413);
},
}), }),
], async (c) => {
}); const { user, system } = c.req.valid("param");
nodeRouter.openapi(createNodeRoute, async (c) => { const nodeId = c.req.param("nodeId.wasm");
const buffer = await c.req.arrayBuffer(); console.log("Get NodeWasm by Id", { user, system, nodeId });
const bytes = await (await c.req.blob()).bytes(); try {
const node = await service.createNode(buffer, bytes); const res = await getNodeByVersion(user, system, nodeId);
return c.json(node); if (res instanceof ArrayBuffer) {
}); c.header("Content-Type", "application/wasm");
return c.body(res);
} else {
return c.json(res);
}
} catch (error) {
if (error instanceof CustomError) {
throw new HTTPException(error.status, { message: error.message });
}
throw new HTTPException(500, { message: "Internal server error" });
}
},
);
nodeRouter.openapi(
createRoute({
method: "get",
path: "/{user}/{system}/{nodeId}@{version}.wasm",
request: {
params: z.object({
user: createParamSchema("user"),
system: createParamSchema("system"),
nodeId: createParamSchema("nodeId"),
version: createParamSchema("version").optional(),
}),
},
responses: {
200: {
content: { "application/wasm": { schema: z.any() } },
description: "Retrieve a node's WASM file",
},
},
}),
async (c) => {
const { user, system, nodeId } = c.req.valid("param");
const hash = c.req.param("version.wasm");
try {
const res = await getNodeByVersion(user, system, nodeId, hash);
if (res instanceof ArrayBuffer) {
c.header("Content-Type", "application/wasm");
return c.body(res);
} else {
return c.json(res);
}
} catch (error) {
if (error instanceof CustomError) {
throw new HTTPException(error.status, { message: error.message });
}
throw new HTTPException(500, { message: "Internal server error" });
}
},
);
export { nodeRouter }; export { nodeRouter };

View File

@@ -0,0 +1,43 @@
import {
customType,
foreignKey,
index,
json,
pgTable,
serial,
timestamp,
varchar,
} from "drizzle-orm/pg-core";
import { usersTable } from "../user/user.schema.ts";
import { NodeDefinition } from "./validations/types.ts";
const bytea = customType<{
data: ArrayBuffer;
default: false;
}>({
dataType() {
return "bytea";
},
});
export const nodeTable = pgTable("nodes", {
id: serial().primaryKey(),
userId: varchar().notNull().references(() => usersTable.name),
createdAt: timestamp().defaultNow(),
systemId: varchar().notNull(),
nodeId: varchar().notNull(),
content: bytea().notNull(),
definition: json().notNull().$type<NodeDefinition>(),
hash: varchar({ length: 16 }).notNull().unique(),
previous: varchar({ length: 16 }),
}, (table) => [
foreignKey({
columns: [table.previous],
foreignColumns: [table.hash],
name: "node_previous_fk",
}),
index("user_id_idx").on(table.userId),
index("system_id_idx").on(table.systemId),
index("node_id_idx").on(table.nodeId),
index("hash_idx").on(table.hash),
]);

View File

@@ -1,9 +1,10 @@
import { db } from "../../db/db.ts"; import { db } from "../../db/db.ts";
import { nodeTable } from "./schemas/node.schema.ts"; import { nodeTable } from "./node.schema.ts";
import { NodeDefinition, NodeDefinitionSchema } from "./schemas/types.ts"; import { NodeDefinition, NodeDefinitionSchema } from "./validations/types.ts";
import { and, eq } from "drizzle-orm"; import { and, asc, eq } from "drizzle-orm";
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import { WorkerMessage } from "./worker/types.ts"; import { extractDefinition } from "./worker/index.ts";
import { InvalidNodeDefinitionError, NodeNotFoundError } from "./errors.ts";
export type CreateNodeDTO = { export type CreateNodeDTO = {
id: string; id: string;
@@ -15,75 +16,55 @@ export type CreateNodeDTO = {
function getNodeHash(content: Uint8Array) { function getNodeHash(content: Uint8Array) {
const hash = createHash("sha256"); const hash = createHash("sha256");
hash.update(content); hash.update(content);
return hash.digest("hex").slice(0, 8); return hash.digest("hex").slice(0, 16);
}
function extractDefinition(content: ArrayBuffer): Promise<NodeDefinition> {
const worker = new Worker(
new URL("./worker/node.worker.ts", import.meta.url).href,
{
type: "module",
},
) as Worker & {
postMessage: (message: WorkerMessage) => void;
};
return new Promise((res, rej) => {
worker.postMessage({ action: "extract-definition", content });
setTimeout(() => {
worker.terminate();
rej(new Error("Worker timeout out"));
}, 100);
worker.onmessage = function (e) {
switch (e.data.action) {
case "result":
res(e.data.result);
break;
case "error":
console.log("Worker error", e.data.error);
rej(e.data.result);
break;
default:
rej(new Error("Unknown worker response"));
}
};
});
} }
export async function createNode( export async function createNode(
wasmBuffer: ArrayBuffer, wasmBuffer: ArrayBuffer,
content: Uint8Array, content: Uint8Array,
): Promise<NodeDefinition> { ): Promise<NodeDefinition> {
try {
const def = await extractDefinition(wasmBuffer); const def = await extractDefinition(wasmBuffer);
const [userId, systemId, nodeId] = def.id.split("/"); const [userId, systemId, nodeId] = def.id.split("/");
const hash = getNodeHash(content);
const node: typeof nodeTable.$inferInsert = { const node: typeof nodeTable.$inferInsert = {
userId, userId,
systemId, systemId,
nodeId, nodeId,
definition: def, definition: def,
hash: getNodeHash(content), hash,
content: content, content: content,
}; };
await db.insert(nodeTable).values(node); const previousNode = await db
console.log("new node created!"); .select({ hash: nodeTable.hash })
return def; .from(nodeTable)
} catch (error) { .orderBy(asc(nodeTable.createdAt))
console.log("Creation Error", { error }); .limit(1);
throw error;
} if (previousNode[0]) {
node.previous = previousNode[0].hash;
} }
export function getNodeDefinitionsByUser(userName: string) { await db.insert(nodeTable).values(node);
return db.select({ definition: nodeTable.definition }).from(nodeTable) return def;
.where( }
and(
eq(nodeTable.userId, userName), export async function getNodeDefinitionsByUser(userName: string) {
), const nodes = await db
); .select({
definition: nodeTable.definition,
hash: nodeTable.hash,
})
.from(nodeTable)
.where(and(eq(nodeTable.userId, userName)));
return nodes.map((n) => ({
...n.definition,
// id: n.definition.id + "@" + n.hash,
}));
} }
export async function getNodesBySystem( export async function getNodesBySystem(
@@ -91,16 +72,26 @@ export async function getNodesBySystem(
systemId: string, systemId: string,
): Promise<NodeDefinition[]> { ): Promise<NodeDefinition[]> {
const nodes = await db const nodes = await db
.select() .selectDistinctOn(
[nodeTable.userId, nodeTable.systemId, nodeTable.nodeId],
{ definition: nodeTable.definition, hash: nodeTable.hash },
)
.from(nodeTable) .from(nodeTable)
.where( .where(
and(eq(nodeTable.systemId, systemId), eq(nodeTable.userId, username)), and(eq(nodeTable.systemId, systemId), eq(nodeTable.userId, username)),
); )
.orderBy(nodeTable.userId, nodeTable.systemId, nodeTable.nodeId);
const definitions = nodes const definitions = nodes
.map((node) => NodeDefinitionSchema.safeParse(node.definition)) .map(
.filter((v) => v.success) (node) =>
.map((v) => v.data); [NodeDefinitionSchema.safeParse(node.definition), node.hash] as const,
)
.filter(([v]) => v.success)
.map(([v, hash]) => ({
...v.data,
// id: v?.data?.id + "@" + hash,
}));
return definitions; return definitions;
} }
@@ -110,17 +101,21 @@ export async function getNodeWasmById(
systemId: string, systemId: string,
nodeId: string, nodeId: string,
) { ) {
const node = await db.select({ content: nodeTable.content }).from(nodeTable) const node = await db
.select({ content: nodeTable.content })
.from(nodeTable)
.where( .where(
and( and(
eq(nodeTable.userId, userName), eq(nodeTable.userId, userName),
eq(nodeTable.systemId, systemId), eq(nodeTable.systemId, systemId),
eq(nodeTable.nodeId, nodeId), eq(nodeTable.nodeId, nodeId),
), ),
).limit(1); )
.orderBy(asc(nodeTable.createdAt))
.limit(1);
if (!node[0]) { if (!node[0]) {
throw new Error("Node not found"); throw new NodeNotFoundError();
} }
return node[0].content; return node[0].content;
@@ -131,25 +126,116 @@ export async function getNodeDefinitionById(
systemId: string, systemId: string,
nodeId: string, nodeId: string,
) { ) {
const node = await db.select({ definition: nodeTable.definition }).from( const node = await db
nodeTable, .select({
).where( definition: nodeTable.definition,
hash: nodeTable.hash,
})
.from(nodeTable)
.where(
and( and(
eq(nodeTable.userId, userName), eq(nodeTable.userId, userName),
eq(nodeTable.systemId, systemId), eq(nodeTable.systemId, systemId),
eq(nodeTable.nodeId, nodeId), eq(nodeTable.nodeId, nodeId),
), ),
).limit(1); )
.orderBy(asc(nodeTable.createdAt))
.limit(1);
if (!node[0]) { if (!node[0]) {
return; throw new NodeNotFoundError();
} }
const definition = NodeDefinitionSchema.safeParse(node[0]?.definition); const definition = NodeDefinitionSchema.safeParse(node[0]?.definition);
if (!definition.data) { if (!definition.success) {
throw new Error("Invalid definition"); throw new InvalidNodeDefinitionError();
} }
return definition.data; return {
...definition.data,
// id: definition.data.id + "@" + node[0].hash
};
}
export async function getNodeVersions(
user: string,
system: string,
nodeId: string,
) {
const nodes = await db
.select({
definition: nodeTable.definition,
hash: nodeTable.hash,
})
.from(nodeTable)
.where(
and(
eq(nodeTable.userId, user),
eq(nodeTable.systemId, system),
eq(nodeTable.nodeId, nodeId),
),
)
.orderBy(asc(nodeTable.createdAt));
return nodes.map((node) => ({
...node.definition,
// id: node.definition.id + "@" + node.hash,
}));
}
export async function getNodeVersion(
user: string,
system: string,
nodeId: string,
hash: string,
) {
const nodes = await db
.select({
definition: nodeTable.definition,
})
.from(nodeTable)
.where(
and(
eq(nodeTable.userId, user),
eq(nodeTable.systemId, system),
eq(nodeTable.nodeId, nodeId),
eq(nodeTable.hash, hash),
),
)
.limit(1);
if (nodes.length === 0) {
throw new NodeNotFoundError();
}
return nodes[0].definition;
}
export async function getNodeVersionWasm(
user: string,
system: string,
nodeId: string,
hash: string,
) {
const node = await db
.select({
content: nodeTable.content,
})
.from(nodeTable)
.where(
and(
eq(nodeTable.userId, user),
eq(nodeTable.systemId, system),
eq(nodeTable.nodeId, nodeId),
eq(nodeTable.hash, hash),
),
)
.limit(1);
if (node.length === 0) {
throw new NodeNotFoundError();
}
return node[0].content;
} }

View File

@@ -1,41 +0,0 @@
import {
customType,
integer,
json,
pgTable,
serial,
varchar,
} from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm/relations";
import { usersTable } from "../../user/user.schema.ts";
const bytea = customType<{
data: ArrayBuffer;
default: false;
}>({
dataType() {
return "bytea";
},
});
export const nodeTable = pgTable("nodes", {
id: serial().primaryKey(),
userId: varchar().notNull(),
systemId: varchar().notNull(),
nodeId: varchar().notNull(),
content: bytea().notNull(),
definition: json().notNull(),
hash: varchar({ length: 8 }).notNull(),
previous: integer(),
});
export const nodeRelations = relations(nodeTable, ({ one }) => ({
userId: one(usersTable, {
fields: [nodeTable.userId],
references: [usersTable.id],
}),
previous: one(nodeTable, {
fields: [nodeTable.previous],
references: [nodeTable.id],
}),
}));

View File

@@ -0,0 +1,36 @@
import { UnknownWorkerResponseError, WorkerTimeoutError } from "../errors.ts";
import { NodeDefinition } from "../validations/types.ts";
import { WorkerMessage } from "./messages.ts";
export function extractDefinition(
content: ArrayBuffer,
): Promise<NodeDefinition> {
const worker = new Worker(
new URL("./node.worker.ts", import.meta.url).href,
{
type: "module",
},
) as Worker & {
postMessage: (message: WorkerMessage) => void;
};
return new Promise((res, rej) => {
worker.postMessage({ action: "extract-definition", content });
setTimeout(() => {
worker.terminate();
rej(new WorkerTimeoutError());
}, 100);
worker.onmessage = function (e) {
switch (e.data.action) {
case "result":
res(e.data.result);
break;
case "error":
rej(e.data.error);
break;
default:
rej(new UnknownWorkerResponseError());
}
};
});
}

View File

@@ -1,4 +1,4 @@
import { NodeDefinition } from "../schemas/types.ts"; import { NodeDefinition } from "../validations/types.ts";
type ExtractDefinitionMessage = { type ExtractDefinitionMessage = {
action: "extract-definition"; action: "extract-definition";

View File

@@ -1,7 +1,7 @@
/// <reference lib="webworker" /> /// <reference lib="webworker" />
import { NodeDefinitionSchema } from "../schemas/types.ts"; import { NodeDefinitionSchema } from "../validations/types.ts";
import { WorkerMessage } from "./types.ts"; import { WorkerMessage } from "./messages.ts";
import { createWasmWrapper } from "./utils.ts"; import { createWasmWrapper } from "./utils.ts";
const workerSelf = self as DedicatedWorkerGlobalScope & { const workerSelf = self as DedicatedWorkerGlobalScope & {

View File

@@ -1,5 +1,5 @@
// @ts-nocheck: Nocheck // @ts-nocheck: Nocheck
import { NodeDefinition } from "../schemas/types.ts"; import { NodeDefinition } from "../validations/types.ts";
const cachedTextDecoder = new TextDecoder("utf-8", { const cachedTextDecoder = new TextDecoder("utf-8", {
ignoreBOM: true, ignoreBOM: true,

View File

@@ -1,10 +0,0 @@
import { OpenAPIHono } from "@hono/zod-openapi";
import { nodeRouter } from "./node/node.controller.ts";
import { userRouter } from "./user/user.controller.ts";
const router = new OpenAPIHono();
router.route("nodes", nodeRouter);
router.route("users", userRouter);
export { router };

View File

@@ -1,7 +1,8 @@
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi"; import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
import { UserSchema, usersTable } from "./user.schema.ts"; import { usersTable } from "./user.schema.ts";
import { db } from "../../db/db.ts"; import { db } from "../../db/db.ts";
import { findUserByName } from "./user.service.ts"; import { findUserByName } from "./user.service.ts";
import { UserSchema } from "./user.validation.ts";
const userRouter = new OpenAPIHono(); const userRouter = new OpenAPIHono();

View File

@@ -1,14 +1,6 @@
import { pgTable, text, uuid } from "drizzle-orm/pg-core"; import { pgTable, text, uuid } from "drizzle-orm/pg-core";
import { z } from "@hono/zod-openapi";
export const usersTable = pgTable("users", { export const usersTable = pgTable("users", {
id: uuid().primaryKey().defaultRandom(), id: uuid().primaryKey().defaultRandom(),
name: text().unique().notNull(), name: text().unique().notNull(),
}); });
export const UserSchema = z
.object({
id: z.string().uuid(),
name: z.string().min(1), // Non-null text with a unique constraint (enforced at the database level)
})
.openapi("User");

View File

@@ -0,0 +1,8 @@
import { z } from "@hono/zod-openapi";
export const UserSchema = z
.object({
id: z.string().uuid(),
name: z.string().min(1),
})
.openapi("User");

37
store/src/server.ts Normal file
View File

@@ -0,0 +1,37 @@
import { createUser } from "./routes/user/user.service.ts";
import { swaggerUI } from "@hono/swagger-ui";
import { logger } from "hono/logger";
import { cors } from "hono/cors";
import { OpenAPIHono } from "@hono/zod-openapi";
import { nodeRouter } from "./routes/node/node.controller.ts";
import { userRouter } from "./routes/user/user.controller.ts";
import { migrateDb } from "./db/db.ts";
const router = new OpenAPIHono();
router.use(logger());
router.use(cors());
router.route("nodes", nodeRouter);
router.route("users", userRouter);
router.doc("/openapi.json", {
openapi: "3.0.0",
info: {
version: "1.0.0",
title: "Nodarium API",
},
});
router.get("/ui", swaggerUI({ url: "/openapi.json" }));
Deno.serve(router.fetch);
async function init() {
await migrateDb();
await createUser("max");
const openapi = await router.request("/openapi.json");
const json = await openapi.text();
Deno.writeTextFile("openapi.json", json);
}
await init();