48 Commits

Author SHA1 Message Date
0894141d3e fix: correctly load nodes from cache/fetch
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 4m9s
2025-11-26 11:09:19 +01:00
925167d9f2 feat: setup antialising on grids 2025-11-26 11:08:57 +01:00
Max Richter
9c4554a1f0 chore: log some more stuff during registry
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m31s
2025-11-24 22:42:08 +01:00
Max Richter
67a104ff84 chore: add some more logs
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m34s
2025-11-24 22:25:01 +01:00
Max Richter
1212c28152 feat: enable some debug logs
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m45s
2025-11-24 22:09:54 +01:00
Max Richter
cfcb447784 feat: update some more components to svelte 5
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m48s
2025-11-24 21:11:16 +01:00
Max Richter
d64877666b fix: 120 type errors
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m47s
2025-11-24 00:10:38 +01:00
Max Richter
0fa1b64d49 feat: update ci to cache pnpm and rust
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m40s
2025-11-23 19:40:50 +01:00
Max Richter
6ca1ff2a34 chore: move some more components to svelte 5
Some checks failed
Deploy to GitHub Pages / build_site (push) Has been cancelled
2025-11-23 19:35:33 +01:00
Max Richter
716df245ab fix: trying to correctly setup sftp
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m40s
2025-11-23 16:09:57 +01:00
Max Richter
2e76202c63 fix: trying to correctly setup sftp 2025-11-23 16:07:26 +01:00
Max Richter
7818148b12 chore: pnpm up -r latest
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 4m6s
2025-11-23 16:01:59 +01:00
Max Richter
566b287550 chore: upgrade ci docker image
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 2m30s
2025-11-23 15:39:16 +01:00
Max Richter
62d3f58d86 chore: update pnpm to latest 2025-11-23 15:19:27 +01:00
Max Richter
c868818ba2 feat: use local node registry again
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 2m56s
2025-11-23 15:15:50 +01:00
Max Richter
64ea7ac349 chore: make some things a bit more typesafe 2025-11-23 15:15:38 +01:00
Max Richter
2dcd797762 chore: pnpm upgrade 2025-11-23 15:15:15 +01:00
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
972fd39da2 feat: disable remote node registry
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m59s
2024-12-18 18:19:46 +01:00
00a9d5b532 feat: add analytics
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m3s
2024-12-18 18:17:10 +01:00
c7d1c28c83 fix: disable story building (for now)
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m13s
2024-12-18 18:14:56 +01:00
43d525f1d9 fix(ci): build scripts
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 1m29s
2024-12-18 18:12:06 +01:00
48175aade0 chore: update lockfile
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 1m31s
2024-12-18 18:07:21 +01:00
6d6ca6f888 feat: some update
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 7s
2024-12-18 18:06:48 +01:00
0c9a9269c4 feat: some shit
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 6s
2024-12-18 17:26:24 +01:00
9d4d67f086 feat: initial backend store prototype
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 13s
2024-12-17 18:15:21 +01:00
109 changed files with 7252 additions and 4829 deletions

View File

@@ -2,7 +2,7 @@ name: Deploy to GitHub Pages
on:
push:
branches: 'main'
branches: "main"
jobs:
build_site:
@@ -13,7 +13,7 @@ jobs:
uses: actions/checkout@v4
- name: Install dependencies
run: pnpm install
run: pnpm install --frozen-lockfile
- name: build
run: pnpm run build:deploy
@@ -23,7 +23,7 @@ jobs:
echo "$SSH_PRIVATE_KEY" > /tmp/id_rsa
chmod 600 /tmp/id_rsa
mkdir -p ~/.config/rclone
echo "[sftp-remote]\ntype = sftp\nhost = ${SSH_HOST}\nuser = ${SSH_USER}\nport = ${SSH_PORT}\nkey_file = /tmp/id_rsa" > ~/.config/rclone/rclone.conf
echo -e "[sftp-remote]\ntype = sftp\nhost = ${SSH_HOST}\nuser = ${SSH_USER}\nport = ${SSH_PORT}\nkey_file = /tmp/id_rsa" > ~/.config/rclone/rclone.conf
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_HOST: ${{ vars.SSH_HOST }}

View File

@@ -1,18 +1,21 @@
FROM node:21
FROM node:24-alpine
# IMAGE CUSTOMISATIONS
RUN apk add --no-cache --update curl rclone g++
# Install rust
# https://github.com/rust-lang/rustup/issues/1085
RUN RUSTUP_URL="https://sh.rustup.rs" \
&& curl --silent --show-error --location --fail --retry 3 --proto '=https' --tlsv1.2 --output /tmp/rustup-linux-install.sh $RUSTUP_URL \
&& bash /tmp/rustup-linux-install.sh -y \
&& curl https://rclone.org/install.sh --output /tmp/rclone-install.sh \
&& bash /tmp/rclone-install.sh
&& sh /tmp/rustup-linux-install.sh -y
ENV PATH=/root/.cargo/bin:$PATH
ENV RUSTUP_HOME=/usr/local/rustup \
CARGO_HOME=/usr/local/cargo \
PATH=/usr/local/cargo/bin:$PATH
RUN rustup target add wasm32-unknown-unknown \
RUN curl --silent --show-error --location --fail --retry 3 \
--proto '=https' --tlsv1.2 \
--output /tmp/rustup-init.sh https://sh.rustup.rs \
&& sh /tmp/rustup-init.sh -y --no-modify-path --profile minimal \
&& rm /tmp/rustup-init.sh \
&& rustup default stable \
&& rustup target add wasm32-unknown-unknown \
&& cargo install wasm-pack \
&& npm i -g pnpm

View File

@@ -13,34 +13,34 @@
"@nodes/registry": "link:../packages/registry",
"@nodes/ui": "link:../packages/ui",
"@nodes/utils": "link:../packages/utils",
"@sveltejs/kit": "^2.7.4",
"@threlte/core": "8.0.0-next.23",
"@threlte/extras": "9.0.0-next.33",
"@types/three": "^0.169.0",
"@unocss/reset": "^0.63.6",
"comlink": "^4.4.1",
"@sveltejs/kit": "^2.49.0",
"@threlte/core": "8.3.0",
"@threlte/extras": "9.7.0",
"@types/three": "^0.181.0",
"@unocss/reset": "^66.5.9",
"comlink": "^4.4.2",
"file-saver": "^2.0.5",
"idb": "^8.0.0",
"jsondiffpatch": "^0.6.0",
"three": "^0.170.0"
"idb": "^8.0.3",
"jsondiffpatch": "^0.7.3",
"three": "^0.181.2"
},
"devDependencies": {
"@iconify-json/tabler": "^1.2.7",
"@iconify-json/tabler": "^1.2.23",
"@nodes/types": "link:../packages/types",
"@sveltejs/adapter-static": "^3.0.6",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@tsconfig/svelte": "^5.0.4",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tsconfig/svelte": "^5.0.6",
"@types/file-saver": "^2.0.7",
"@unocss/preset-icons": "^0.63.6",
"svelte": "^5.1.9",
"svelte-check": "^4.0.5",
"@unocss/preset-icons": "^66.5.9",
"svelte": "^5.43.14",
"svelte-check": "^4.3.4",
"tslib": "^2.8.1",
"typescript": "^5.6.3",
"unocss": "^0.63.6",
"vite": "^5.4.10",
"vite-plugin-comlink": "^5.1.0",
"vite-plugin-glsl": "^1.3.0",
"vite-plugin-wasm": "^3.3.0",
"vitest": "^2.1.4"
"typescript": "^5.9.3",
"unocss": "^66.5.9",
"vite": "^7.2.4",
"vite-plugin-comlink": "^5.3.0",
"vite-plugin-glsl": "^1.5.4",
"vite-plugin-wasm": "^3.5.0",
"vitest": "^4.0.13"
}
}

View File

@@ -5,6 +5,7 @@
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/svelte.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script defer src="https://umami.max-richter.dev/script.js" data-website-id="585c442b-0524-4874-8955-f9853b44b17e"></script>
%sveltekit.head%
<title>Nodes</title>
<script>

View File

@@ -1,33 +1,39 @@
<script lang="ts">
import type { GraphManager } from "./graph-manager.js";
import type { GraphManager } from "./graph-manager.svelte";
import { HTML } from "@threlte/extras";
import { onMount } from "svelte";
import type { NodeType } from "@nodes/types";
export let position: [x: number, y: number] | null;
type Props = {
position: [x: number, y: number] | null;
graph: GraphManager;
};
export let graph: GraphManager;
let { position = $bindable(), graph }: Props = $props();
let input: HTMLInputElement;
let value: string = "";
let activeNodeId: string = "";
let value = $state<string>();
let activeNodeId = $state<NodeType>();
const allNodes = graph.getNodeDefinitions();
function filterNodes() {
return allNodes.filter((node) => node.id.includes(value));
return allNodes.filter((node) => node.id.includes(value ?? ""));
}
$: nodes = value === "" ? allNodes : filterNodes();
$: if (nodes) {
if (activeNodeId === "") {
activeNodeId = nodes[0].id;
} else if (nodes.length) {
const node = nodes.find((node) => node.id === activeNodeId);
if (!node) {
const nodes = $derived(value === "" ? allNodes : filterNodes());
$effect(() => {
if (nodes) {
if (activeNodeId === undefined) {
activeNodeId = nodes[0].id;
} else if (nodes.length) {
const node = nodes.find((node) => node.id === activeNodeId);
if (!node) {
activeNodeId = nodes[0].id;
}
}
}
}
});
function handleKeyDown(event: KeyboardEvent) {
event.stopImmediatePropagation();
@@ -51,7 +57,7 @@
if (event.key === "Enter") {
if (activeNodeId && position) {
graph.createNode({ type: activeNodeId, position });
graph.createNode({ type: activeNodeId, position, props: {} });
position = null;
}
return;
@@ -74,7 +80,7 @@
role="searchbox"
placeholder="Search..."
disabled={false}
on:keydown={handleKeyDown}
onkeydown={handleKeyDown}
bind:value
bind:this={input}
/>
@@ -87,7 +93,7 @@
role="treeitem"
tabindex="0"
aria-selected={node.id === activeNodeId}
on:keydown={(event) => {
onkeydown={(event) => {
if (event.key === "Enter") {
if (position) {
graph.createNode({ type: node.id, position, props: {} });
@@ -95,17 +101,17 @@
}
}
}}
on:mousedown={() => {
onmousedown={() => {
if (position) {
graph.createNode({ type: node.id, position, props: {} });
position = null;
}
}}
on:focus={() => {
onfocus={() => {
activeNodeId = node.id;
}}
class:selected={node.id === activeNodeId}
on:mouseover={() => {
onmouseover={() => {
activeNodeId = node.id;
}}
>

View File

@@ -1,4 +1,6 @@
precision highp float;
// For WebGL1 make sure this extension is enabled in your material:
// #extension GL_OES_standard_derivatives : enable
varying vec2 vUv;
@@ -10,33 +12,45 @@ uniform vec2 zoomLimits;
uniform vec3 backgroundColor;
uniform vec3 lineColor;
// Anti-aliased step: threshold in the same units as `value`
float aaStep(float threshold, float value, float deriv) {
float w = deriv * 0.5; // ~one pixel
return smoothstep(threshold - w, threshold + w, value);
}
float grid(float x, float y, float divisions, float thickness) {
x = fract(x * divisions);
x = min(x, 1.0 - x);
// Continuous grid coordinates
float gx = x * divisions;
float gy = y * divisions;
float xdelta = fwidth(x);
x = smoothstep(x - xdelta, x + xdelta, thickness);
// Distance to nearest grid line (0 at the line)
float fx = fract(gx);
fx = min(fx, 1.0 - fx);
float fy = fract(gy);
fy = min(fy, 1.0 - fy);
y = fract(y * divisions);
y = min(y, 1.0 - y);
// Derivatives in screen space use the continuous coords here
float dx = fwidth(gx);
float dy = fwidth(gy);
float ydelta = fwidth(y);
y = smoothstep(y - ydelta, y + ydelta, thickness);
// Keep the original semantics: thickness is the threshold in the [0, 0.5] distance domain
float lineX = 1.0 - aaStep(thickness, fx, dx);
float lineY = 1.0 - aaStep(thickness, fy, dy);
return clamp(x + y, 0.0, 1.0);
return clamp(lineX + lineY, 0.0, 1.0);
}
float circle_grid(float x, float y, float divisions, float circleRadius) {
float gridX = mod(x + divisions * 0.5, divisions) - divisions * 0.5;
float gridY = mod(y + divisions * 0.5, divisions) - divisions * 0.5;
float gridX = mod(x + divisions/2.0, divisions) - divisions / 2.0;
float gridY = mod(y + divisions/2.0, divisions) - divisions / 2.0;
vec2 g = vec2(gridX, gridY);
float d = length(g);
// Calculate the distance from the center of the grid
float gridDistance = length(vec2(gridX, gridY));
// Use smoothstep to create a smooth transition at the edges of the circle
float circle = 1.0 - smoothstep(circleRadius - 0.5, circleRadius + 0.5, gridDistance);
// Screen-space derivative for AA on the circle edge
float w = fwidth(d);
float circle = 1.0 - smoothstep(circleRadius - w, circleRadius + w, d);
return circle;
}
@@ -56,44 +70,43 @@ void main(void) {
float minZ = zoomLimits.x;
float maxZ = zoomLimits.y;
float divisions = 0.1/cz;
float thickness = 0.05/cz;
float delta = 0.1 / 2.0;
float divisions = 0.1 / cz;
float thickness = 0.05 / cz;
float nz = (cz - minZ) / (maxZ - minZ);
float ux = (vUv.x-0.5) * width + cx*cz;
float uy = (vUv.y-0.5) * height - cy*cz;
float ux = (vUv.x - 0.5) * width + cx * cz;
float uy = (vUv.y - 0.5) * height - cy * cz;
//extra small grid
float m1 = grid(ux, uy, divisions*4.0, thickness*4.0) * 0.9;
float m2 = grid(ux, uy, divisions*16.0, thickness*16.0) * 0.5;
// extra small grid
float m1 = grid(ux, uy, divisions * 4.0, thickness * 4.0) * 0.9;
float m2 = grid(ux, uy, divisions * 16.0, thickness * 16.0) * 0.5;
float xsmall = max(m1, m2);
float s3 = circle_grid(ux, uy, cz/1.6, 1.0) * 0.5;
float s3 = circle_grid(ux, uy, cz / 1.6, 1.0) * 0.5;
xsmall = max(xsmall, s3);
// small grid
float c1 = grid(ux, uy, divisions, thickness) * 0.6;
float c2 = grid(ux, uy, divisions*2.0, thickness) * 0.5;
float c1 = grid(ux, uy, divisions, thickness) * 0.6;
float c2 = grid(ux, uy, divisions * 2.0, thickness * 2.0) * 0.5;
float small = max(c1, c2);
float s1 = circle_grid(ux, uy, cz*10.0, 2.0) * 0.5;
float s1 = circle_grid(ux, uy, cz * 10.0, 2.0) * 0.5;
small = max(small, s1);
// large grid
float c3 = grid(ux, uy, divisions/8.0, thickness/8.0) * 0.5;
float c4 = grid(ux, uy, divisions/2.0, thickness/4.0) * 0.4;
float c3 = grid(ux, uy, divisions / 8.0, thickness / 8.0) * 0.5;
float c4 = grid(ux, uy, divisions / 2.0, thickness / 4.0) * 0.4;
float large = max(c3, c4);
float s2 = circle_grid(ux, uy, cz*20.0, 1.0) * 0.4;
float s2 = circle_grid(ux, uy, cz * 20.0, 1.0) * 0.4;
large = max(large, s2);
float c = mix(large, small, min(nz*2.0+0.05, 1.0));
c = mix(c, xsmall, max(min((nz-0.3)/0.7, 1.0), 0.0));
float c = mix(large, small, min(nz * 2.0 + 0.05, 1.0));
c = mix(c, xsmall, clamp((nz - 0.3) / 0.7, 0.0, 1.0));
vec3 color = mix(backgroundColor, lineColor, c);
gl_FragColor = vec4(color, 1.0);
}

View File

@@ -1,21 +0,0 @@
<script lang="ts">
import type { Hst } from "@histoire/plugin-svelte";
export let Hst: Hst;
import Background from "./Background.svelte";
import { Canvas } from "@threlte/core";
import Camera from "../Camera.svelte";
let width = globalThis.innerWidth || 100;
let height = globalThis.innerHeight || 100;
let cameraPosition: [number, number, number] = [0, 1, 0];
</script>
<svelte:window bind:innerWidth={width} bind:innerHeight={height} />
<Hst.Story>
<Canvas shadows={false}>
<Camera bind:position={cameraPosition} />
<Background {cameraPosition} {width} {height} />
</Canvas>
</Hst.Story>

View File

@@ -3,7 +3,6 @@
import BackgroundVert from "./Background.vert";
import BackgroundFrag from "./Background.frag";
import { colors } from "../graph/colors.svelte";
import { Color } from "three";
import { appSettings } from "$lib/settings/app-settings.svelte";
type Props = {
@@ -42,10 +41,10 @@
value: [0, 1, 0],
},
backgroundColor: {
value: colors["layer-0"].clone(),
value: colors["layer-0"],
},
lineColor: {
value: colors["outline"].clone(),
value: colors["outline"],
},
zoomLimits: {
value: [2, 50],
@@ -55,9 +54,9 @@
},
}}
uniforms.camPos.value={cameraPosition}
uniforms.backgroundColor.value={appSettings.theme &&
colors["layer-0"].clone()}
uniforms.lineColor.value={appSettings.theme && colors["outline"].clone()}
uniforms.backgroundColor.value={appSettings.value.theme &&
colors["layer-0"]}
uniforms.lineColor.value={appSettings.value.theme && colors["outline"]}
uniforms.zoomLimits.value={[minZoom, maxZoom]}
uniforms.dimensions.value={[width, height]}
/>

View File

@@ -7,9 +7,9 @@
});
$effect.root(() => {
$effect(() => {
appSettings.theme;
appSettings.value.theme;
circleMaterial.color = colors.edge.clone().convertSRGBToLinear();
})
});
});
const lineCache = new Map<number, BufferGeometry>();
@@ -25,23 +25,25 @@
<script lang="ts">
import { T } from "@threlte/core";
import { MeshLineMaterial } from "@threlte/extras";
import { BufferGeometry, MeshBasicMaterial, Vector3 } from "three";
import { BufferGeometry, MeshBasicMaterial, Vector3 } from "three";
import { CubicBezierCurve } from "three/src/extras/curves/CubicBezierCurve.js";
import { Vector2 } from "three/src/math/Vector2.js";
import { createEdgeGeometry } from "./createEdgeGeometry.js";
import { appSettings } from "$lib/settings/app-settings.svelte";
import { appSettings } from "$lib/settings/app-settings.svelte";
type Props = {
from: { x: number; y: number };
to: { x: number; y: number };
z:number;
z: number;
};
const { from, to, z }: Props = $props();
let geometry: BufferGeometry|null = $state(null);
let geometry: BufferGeometry | null = $state(null);
const lineColor = $derived(appSettings.theme && colors.edge.clone().convertSRGBToLinear());
const lineColor = $derived(
appSettings.value.theme && colors.edge.clone().convertSRGBToLinear(),
);
let lastId: number | null = null;
@@ -81,8 +83,7 @@
geometry = createEdgeGeometry(points);
lineCache.set(curveId, geometry);
};
}
$effect(() => {
if (from || to) {
@@ -113,6 +114,9 @@
{#if geometry}
<T.Mesh position.x={from.x} position.z={from.y} position.y={0.1} {geometry}>
<MeshLineMaterial width={Math.max(z*0.0001,0.00001)} color={lineColor} />
<MeshLineMaterial
width={Math.max(z * 0.00012, 0.00003)}
color={lineColor}
/>
</T.Mesh>
{/if}

View File

@@ -6,7 +6,7 @@ export function createEdgeGeometry(points: Vector3[]) {
const length = points[0].distanceTo(points[points.length - 1]);
const startRadius = 10.5;
const startRadius = 8;
const constantWidth = 2;
const taperFraction = 0.8 / length;

View File

@@ -1,86 +1,103 @@
import type { Edge, Graph, Node, NodeInput, NodeRegistry, Socket, } from "@nodes/types";
import type {
Edge,
Graph,
Node,
NodeInput,
NodeRegistry,
NodeType,
Socket,
} from "@nodes/types";
import { fastHashString } from "@nodes/utils";
import { get, writable, type Writable } from "svelte/store";
import EventEmitter from "./helpers/EventEmitter.js";
import { createLogger } from "./helpers/index.js";
import throttle from "./helpers/throttle.js";
import { HistoryManager } from "./history-manager.js";
import { SvelteMap } from "svelte/reactivity";
import EventEmitter from "./helpers/EventEmitter";
import { createLogger } from "@nodes/utils";
import throttle from "$lib/helpers/throttle";
import { HistoryManager } from "./history-manager";
const logger = createLogger("graph-manager");
// logger.mute();
logger.mute();
const clone =
"structuredClone" in self
? self.structuredClone
: (args: any) => JSON.parse(JSON.stringify(args));
const clone = "structuredClone" in self ? self.structuredClone : (args: any) => JSON.parse(JSON.stringify(args));
function areSocketsCompatible(output: string | undefined, inputs: string | string[] | undefined) {
function areSocketsCompatible(
output: string | undefined,
inputs: string | (string | undefined)[] | undefined,
) {
if (Array.isArray(inputs) && output) {
return inputs.includes(output);
}
return inputs === output;
}
export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "settings": { types: Record<string, NodeInput>, values: Record<string, unknown> } }> {
status: Writable<"loading" | "idle" | "error"> = writable("loading");
export class GraphManager extends EventEmitter<{
save: Graph;
result: any;
settings: {
types: Record<string, NodeInput>;
values: Record<string, unknown>;
};
}> {
status = $state<"loading" | "idle" | "error">();
loaded = false;
graph: Graph = { id: 0, nodes: [], edges: [] };
id = writable(0);
id = $state(0);
private _nodes: Map<number, Node> = new Map();
nodes: Writable<Map<number, Node>> = writable(new Map());
nodes = new SvelteMap<number, Node>();
private _edges: Edge[] = [];
edges: Writable<Edge[]> = writable([]);
edges = $state<Edge[]>([]);
settingTypes: Record<string, NodeInput> = {};
settings: Record<string, unknown> = {};
settings = $state<Record<string, unknown>>();
currentUndoGroup: number | null = null;
inputSockets: Writable<Set<string>> = writable(new Set());
inputSockets = $derived.by(() => {
const s = new Set<string>();
for (const edge of this.edges) {
s.add(`${edge[2].id}-${edge[3]}`);
}
return s;
});
history: HistoryManager = new HistoryManager();
execute = throttle(() => {
console.log("Props", get(this.nodes).values().find(n => n.type === "max/plantarium/gravity")?.props);
if (this.loaded === false) return;
this.emit("result", this.serialize());
}, 10);
constructor(public registry: NodeRegistry) {
super();
this.nodes.subscribe((nodes) => {
this._nodes = nodes;
});
this.edges.subscribe((edges) => {
this._edges = edges;
const s = new Set<string>();
for (const edge of edges) {
s.add(`${edge[2].id}-${edge[3]}`);
}
this.inputSockets.set(s);
});
}
serialize(): Graph {
logger.group("serializing graph")
const nodes = Array.from(this._nodes.values()).map(node => ({
const nodes = Array.from(this.nodes.values()).map((node) => ({
id: node.id,
position: [...node.position],
type: node.type,
props: node.props,
})) as Node[];
const edges = this._edges.map(edge => [edge[0].id, edge[1], edge[2].id, edge[3]]) as Graph["edges"];
const serialized = { id: this.graph.id, settings: this.settings, nodes, edges };
logger.groupEnd();
return clone(serialized);
const edges = this.edges.map((edge) => [
edge[0].id,
edge[1],
edge[2].id,
edge[3],
]) as Graph["edges"];
const serialized = {
id: this.graph.id,
settings: this.settings,
nodes,
edges,
};
logger.log("serializing graph", serialized);
return clone($state.snapshot(serialized));
}
private lastSettingsHash = 0;
setSettings(settings: Record<string, unknown>) {
let hash = fastHashString(JSON.stringify(settings));
if (hash === this.lastSettingsHash) return;
this.lastSettingsHash = hash;
@@ -90,8 +107,6 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
this.execute();
}
getNodeDefinitions() {
return this.registry.getAllNodes();
}
@@ -105,7 +120,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
nodes.add(n);
const children = this.getChildrenOfNode(n);
const parents = this.getParentsOfNode(n);
const newNodes = [...children, ...parents].filter(n => !nodes.has(n));
const newNodes = [...children, ...parents].filter((n) => !nodes.has(n));
stack.push(...newNodes);
}
return [...nodes.values()];
@@ -117,9 +132,16 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
const children = node.tmp?.children || [];
for (const child of children) {
if (nodes.includes(child)) {
const edge = this._edges.find(e => e[0].id === node.id && e[2].id === child.id);
const edge = this.edges.find(
(e) => e[0].id === node.id && e[2].id === child.id,
);
if (edge) {
edges.push([edge[0].id, edge[1], edge[2].id, edge[3]] as [number, number, number, string]);
edges.push([edge[0].id, edge[1], edge[2].id, edge[3]] as [
number,
number,
number,
string,
]);
}
}
}
@@ -128,25 +150,26 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
return edges;
}
private _init(graph: Graph) {
const nodes = new Map(graph.nodes.map(node => {
const nodeType = this.registry.getNode(node.type);
if (nodeType) {
node.tmp = {
random: (Math.random() - 0.5) * 2,
type: nodeType
};
}
return [node.id, node]
}));
const nodes = new Map(
graph.nodes.map((node: Node) => {
const nodeType = this.registry.getNode(node.type);
if (nodeType) {
node.tmp = {
random: (Math.random() - 0.5) * 2,
type: nodeType,
};
}
return [node.id, node];
}),
);
const edges = graph.edges.map((edge) => {
const from = nodes.get(edge[0]);
const to = nodes.get(edge[2]);
if (!from || !to) {
throw new Error("Edge references non-existing node");
};
}
from.tmp = from.tmp || {};
from.tmp.children = from.tmp.children || [];
from.tmp.children.push(to);
@@ -154,32 +177,38 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
to.tmp.parents = to.tmp.parents || [];
to.tmp.parents.push(from);
return [from, edge[1], to, edge[3]] as Edge;
})
});
this.edges.set(edges);
this.nodes.set(nodes);
this.edges = [...edges];
this.nodes.clear();
for (const [id, node] of nodes) {
this.nodes.set(id, node);
}
this.execute();
}
async load(graph: Graph) {
const a = performance.now();
this.loaded = false;
this.graph = graph;
this.status.set("loading");
this.id.set(graph.id);
this.status = "loading";
this.id = graph.id;
const nodeIds = Array.from(new Set([...graph.nodes.map(n => n.type)]));
logger.info("loading graph", $state.snapshot(graph));
const nodeIds = Array.from(new Set([...graph.nodes.map((n) => n.type)]));
await this.registry.load(nodeIds);
logger.info("loaded node types", this.registry.getAllNodes());
for (const node of this.graph.nodes) {
const nodeType = this.registry.getNode(node.type);
if (!nodeType) {
logger.error(`Node type not found: ${node.type}`);
this.status.set("error");
this.status = "error";
return;
}
node.tmp = node.tmp || {};
@@ -187,9 +216,12 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
node.tmp.type = nodeType;
}
// load settings
const settingTypes: Record<string, NodeInput> = {};
const settingTypes: Record<
string,
// Optional metadata to map settings to specific nodes
NodeInput & { __node_type: string; __node_input: string }
> = {};
const settingValues = graph.settings || {};
const types = this.getNodeDefinitions();
for (const type of types) {
@@ -197,8 +229,15 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
for (const key in type.inputs) {
let settingId = type.inputs[key].setting;
if (settingId) {
settingTypes[settingId] = { __node_type: type.id, __node_input: key, ...type.inputs[key] };
if (settingValues[settingId] === undefined && "value" in type.inputs[key]) {
settingTypes[settingId] = {
__node_type: type.id,
__node_input: key,
...type.inputs[key],
};
if (
settingValues[settingId] === undefined &&
"value" in type.inputs[key]
) {
settingValues[settingId] = type.inputs[key].value;
}
}
@@ -214,28 +253,26 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
this.save();
this.status.set("idle");
this.status = "idle";
this.loaded = true;
logger.log(`Graph loaded in ${performance.now() - a}ms`);
setTimeout(() => this.execute(), 100);
}
getAllNodes() {
return Array.from(this._nodes.values());
return Array.from(this.nodes.values());
}
getNode(id: number) {
return this._nodes.get(id);
return this.nodes.get(id);
}
getNodeType(id: string) {
return this.registry.getNode(id);
}
async loadNode(id: string) {
async loadNode(id: NodeType) {
await this.registry.load([id]);
const nodeType = this.registry.getNode(id);
@@ -248,7 +285,11 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
let settingId = nodeType.inputs[key].setting;
if (settingId) {
settingTypes[settingId] = nodeType.inputs[key];
if (settingValues[settingId] === undefined && "value" in nodeType.inputs[key]) {
if (
settingValues &&
settingValues?.[settingId] === undefined &&
"value" in nodeType.inputs[key]
) {
settingValues[settingId] = nodeType.inputs[key].value;
}
}
@@ -256,6 +297,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
}
this.settings = settingValues;
console.log("GraphManager.setSettings", settingValues);
this.settingTypes = settingTypes;
this.emit("settings", { types: settingTypes, values: settingValues });
}
@@ -267,7 +309,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
const child = stack.pop();
if (!child) continue;
children.push(child);
stack.push(...child.tmp?.children || []);
stack.push(...(child.tmp?.children || []));
}
return children;
}
@@ -279,10 +321,10 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
const fromParents = this.getParentsOfNode(from);
if (toParents.includes(from)) {
const fromChildren = this.getChildrenOfNode(from);
return toParents.filter(n => fromChildren.includes(n));
return toParents.filter((n) => fromChildren.includes(n));
} else if (fromParents.includes(to)) {
const toChildren = this.getChildrenOfNode(to);
return fromParents.filter(n => toChildren.includes(n));
return fromParents.filter((n) => toChildren.includes(n));
} else {
// these two nodes are not connected
return;
@@ -290,46 +332,41 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
}
removeNode(node: Node, { restoreEdges = false } = {}) {
const edgesToNode = this._edges.filter((edge) => edge[2].id === node.id);
const edgesFromNode = this._edges.filter((edge) => edge[0].id === node.id);
const edgesToNode = this.edges.filter((edge) => edge[2].id === node.id);
const edgesFromNode = this.edges.filter((edge) => edge[0].id === node.id);
for (const edge of [...edgesToNode, ...edgesFromNode]) {
this.removeEdge(edge, { applyDeletion: false });
}
if (restoreEdges) {
const outputSockets = edgesToNode.map(e => [e[0], e[1]] as const);
const inputSockets = edgesFromNode.map(e => [e[2], e[3]] as const);
const outputSockets = edgesToNode.map((e) => [e[0], e[1]] as const);
const inputSockets = edgesFromNode.map((e) => [e[2], e[3]] as const);
for (const [to, toSocket] of inputSockets) {
for (const [from, fromSocket] of outputSockets) {
const outputType = from.tmp?.type?.outputs?.[fromSocket];
const inputType = to?.tmp?.type?.inputs?.[toSocket]?.type;
if (outputType === inputType) {
this.createEdge(from, fromSocket, to, toSocket, { applyUpdate: false });
this.createEdge(from, fromSocket, to, toSocket, {
applyUpdate: false,
});
continue;
}
}
}
}
this.edges.set(this._edges);
this.nodes.update((nodes) => {
nodes.delete(node.id);
return nodes;
});
this.execute()
this.nodes.delete(node.id);
this.execute();
this.save();
}
createNodeId() {
const max = Math.max(...this._nodes.keys());
const max = Math.max(0, ...this.nodes.keys());
return max + 1;
}
createGraph(nodes: Node[], edges: [number, number, number, string][]) {
// map old ids to new ids
const idMap = new Map<number, number>();
@@ -345,9 +382,9 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
return { ...node, id, tmp: { type } };
});
const _edges = edges.map(edge => {
const from = nodes.find(n => n.id === idMap.get(edge[0]));
const to = nodes.find(n => n.id === idMap.get(edge[2]));
const _edges = edges.map((edge) => {
const from = nodes.find((n) => n.id === idMap.get(edge[0]));
const to = nodes.find((n) => n.id === idMap.get(edge[2]));
if (!from || !to) {
throw new Error("Edge references non-existing node");
@@ -365,45 +402,60 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
});
for (const node of nodes) {
this._nodes.set(node.id, node);
this.nodes.set(node.id, node);
}
this._edges.push(..._edges);
this.edges.push(..._edges);
this.nodes.set(this._nodes);
this.edges.set(this._edges);
this.save();
return nodes;
}
createNode({ type, position, props = {} }: { type: Node["type"], position: Node["position"], props: Node["props"] }) {
createNode({
type,
position,
props = {},
}: {
type: Node["type"];
position: Node["position"];
props: Node["props"];
}) {
const nodeType = this.registry.getNode(type);
if (!nodeType) {
logger.error(`Node type not found: ${type}`);
return;
}
const node: Node = { id: this.createNodeId(), type, position, tmp: { type: nodeType }, props };
const node: Node = {
id: this.createNodeId(),
type,
position,
tmp: { type: nodeType },
props,
};
this.nodes.update((nodes) => {
nodes.set(node.id, node);
return nodes;
});
this.nodes.set(node.id, node);
this.save();
}
createEdge(from: Node, fromSocket: number, to: Node, toSocket: string, { applyUpdate = true } = {}) {
createEdge(
from: Node,
fromSocket: number,
to: Node,
toSocket: string,
{ applyUpdate = true } = {},
) {
const existingEdges = this.getEdgesToNode(to);
// check if this exact edge already exists
const existingEdge = existingEdges.find(e => e[0].id === from.id && e[1] === fromSocket && e[3] === toSocket);
const existingEdge = existingEdges.find(
(e) => e[0].id === from.id && e[1] === fromSocket && e[3] === toSocket,
);
if (existingEdge) {
logger.error("Edge already exists", existingEdge);
return;
};
}
// check if socket types match
const fromSocketType = from.tmp?.type?.outputs?.[fromSocket];
@@ -413,19 +465,23 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
}
if (!areSocketsCompatible(fromSocketType, toSocketType)) {
logger.error(`Socket types do not match: ${fromSocketType} !== ${toSocketType}`);
logger.error(
`Socket types do not match: ${fromSocketType} !== ${toSocketType}`,
);
return;
}
const edgeToBeReplaced = this._edges.find(e => e[2].id === to.id && e[3] === toSocket);
const edgeToBeReplaced = this.edges.find(
(e) => e[2].id === to.id && e[3] === toSocket,
);
if (edgeToBeReplaced) {
this.removeEdge(edgeToBeReplaced, { applyDeletion: false });
}
if (applyUpdate) {
this._edges.push([from, fromSocket, to, toSocket]);
this.edges.push([from, fromSocket, to, toSocket]);
} else {
this._edges.push([from, fromSocket, to, toSocket]);
this.edges.push([from, fromSocket, to, toSocket]);
}
from.tmp = from.tmp || {};
@@ -437,7 +493,6 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
to.tmp.parents.push(from);
if (applyUpdate) {
this.edges.set(this._edges);
this.save();
}
this.execute();
@@ -451,14 +506,12 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
}
}
redo() {
const nextState = this.history.redo();
if (nextState) {
this._init(nextState);
this.emit("save", this.serialize());
}
}
startUndoGroup() {
@@ -483,30 +536,30 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
const stack = node.tmp?.parents?.slice(0);
while (stack?.length) {
if (parents.length > 1000000) {
logger.warn("Infinite loop detected")
logger.warn("Infinite loop detected");
break;
}
const parent = stack.pop();
if (!parent) continue;
parents.push(parent);
stack.push(...parent.tmp?.parents || []);
stack.push(...(parent.tmp?.parents || []));
}
return parents.reverse();
}
getPossibleSockets({ node, index }: Socket): [Node, string | number][] {
const nodeType = node?.tmp?.type;
if (!nodeType) return [];
const sockets: [Node, string | number][] = []
const sockets: [Node, string | number][] = [];
// if index is a string, we are an input looking for outputs
if (typeof index === "string") {
// filter out self and child nodes
const children = new Set(this.getChildrenOfNode(node).map(n => n.id));
const nodes = this.getAllNodes().filter(n => n.id !== node.id && !children.has(n.id));
const children = new Set(this.getChildrenOfNode(node).map((n) => n.id));
const nodes = this.getAllNodes().filter(
(n) => n.id !== node.id && !children.has(n.id),
);
const ownType = nodeType?.inputs?.[index].type;
@@ -520,16 +573,21 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
}
}
}
} else if (typeof index === "number") {
// if index is a number, we are an output looking for inputs
// filter out self and parent nodes
const parents = new Set(this.getParentsOfNode(node).map(n => n.id));
const nodes = this.getAllNodes().filter(n => n.id !== node.id && !parents.has(n.id));
const parents = new Set(this.getParentsOfNode(node).map((n) => n.id));
const nodes = this.getAllNodes().filter(
(n) => n.id !== node.id && !parents.has(n.id),
);
// get edges from this socket
const edges = new Map(this.getEdgesFromNode(node).filter(e => e[1] === index).map(e => [e[2].id, e[3]]));
const edges = new Map(
this.getEdgesFromNode(node)
.filter((e) => e[1] === index)
.map((e) => [e[2].id, e[3]]),
);
const ownType = nodeType.outputs?.[index];
@@ -537,11 +595,13 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
const inputs = node?.tmp?.type?.inputs;
if (!inputs) continue;
for (const key in inputs) {
const otherType = [inputs[key].type];
otherType.push(...(inputs[key].accepts || []));
if (areSocketsCompatible(ownType, otherType) && edges.get(node.id) !== key) {
if (
areSocketsCompatible(ownType, otherType) &&
edges.get(node.id) !== key
) {
sockets.push([node, key]);
}
}
@@ -549,42 +609,48 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
}
return sockets;
}
removeEdge(edge: Edge, { applyDeletion = true }: { applyDeletion?: boolean } = {}) {
removeEdge(
edge: Edge,
{ applyDeletion = true }: { applyDeletion?: boolean } = {},
) {
const id0 = edge[0].id;
const sid0 = edge[1];
const id2 = edge[2].id;
const sid2 = edge[3];
const _edge = this._edges.find((e) => e[0].id === id0 && e[1] === sid0 && e[2].id === id2 && e[3] === sid2);
const _edge = this.edges.find(
(e) =>
e[0].id === id0 && e[1] === sid0 && e[2].id === id2 && e[3] === sid2,
);
if (!_edge) return;
edge[0].tmp = edge[0].tmp || {};
if (edge[0].tmp.children) {
edge[0].tmp.children = edge[0].tmp.children.filter(n => n.id !== id2);
edge[0].tmp.children = edge[0].tmp.children.filter(
(n: Node) => n.id !== id2,
);
}
edge[2].tmp = edge[2].tmp || {};
if (edge[2].tmp.parents) {
edge[2].tmp.parents = edge[2].tmp.parents.filter(n => n.id !== id0);
edge[2].tmp.parents = edge[2].tmp.parents.filter(
(n: Node) => n.id !== id0,
);
}
if (applyDeletion) {
this.edges.update((edges) => {
return edges.filter(e => e !== _edge);
});
this.edges = this.edges.filter((e) => e !== _edge);
this.execute();
this.save();
} else {
this._edges = this._edges.filter(e => e !== _edge);
this.edges = this.edges.filter((e) => e !== _edge);
}
}
getEdgesToNode(node: Node) {
return this._edges
return this.edges
.filter((edge) => edge[2].id === node.id)
.map((edge) => {
const from = this.getNode(edge[0].id);
@@ -596,7 +662,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
}
getEdgesFromNode(node: Node) {
return this._edges
return this.edges
.filter((edge) => edge[0].id === node.id)
.map((edge) => {
const from = this.getNode(edge[0].id);
@@ -606,7 +672,4 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
})
.filter(Boolean) as unknown as [Node, number, Node, string][];
}
}

View File

@@ -1,73 +1,67 @@
<script lang="ts">
import type { Node, NodeType, Socket } from "@nodes/types";
import { GraphSchema } from "@nodes/types";
import { getContext, onMount, setContext } from "svelte";
import type { OrthographicCamera } from "three";
import { createKeyMap } from "../../helpers/createKeyMap";
import AddMenu from "../AddMenu.svelte";
import Background from "../background/Background.svelte";
import BoxSelection from "../BoxSelection.svelte";
import Camera from "../Camera.svelte";
import FloatingEdge from "../edges/FloatingEdge.svelte";
import {
animate,
lerp,
snapToGrid as snapPointToGrid,
} from "../helpers/index.js";
import type { OrthographicCamera } from "three";
import Background from "../background/Background.svelte";
import { getContext, onMount, setContext } from "svelte";
import Camera from "../Camera.svelte";
import GraphView from "./GraphView.svelte";
import type { Node, NodeId, Node as NodeType, Socket } from "@nodes/types";
import { GraphSchema } from "@nodes/types";
import FloatingEdge from "../edges/FloatingEdge.svelte";
import { getGraphState } from "./state.svelte";
import { createKeyMap } from "../../helpers/createKeyMap";
import BoxSelection from "../BoxSelection.svelte";
import AddMenu from "../AddMenu.svelte";
import HelpView from "../HelpView.svelte";
import FileSaver from "file-saver";
import { Canvas } from "@threlte/core";
import { getGraphManager } from "./context.js";
import FileSaver from "file-saver";
import HelpView from "../HelpView.svelte";
import { getGraphManager } from "./context";
const graph = getGraphManager();
const graphState = getGraphState();
export let snapToGrid = true;
export let showGrid = true;
export let showHelp = false;
const {
snapToGrid = $bindable(true),
showGrid = $bindable(true),
showHelp = $bindable(false),
} = $props();
const keymap = getContext<ReturnType<typeof createKeyMap>>("keymap");
const manager = getGraphManager();
let wrapper = $state<HTMLDivElement>(null!);
const status = manager.status;
const nodes = manager.nodes;
const edges = manager.edges;
const rect: DOMRect = $derived(
wrapper ? wrapper.getBoundingClientRect() : new DOMRect(0, 0, 0, 0),
);
let width = $derived(rect?.width ?? 100);
let height = $derived(rect?.height ?? 100);
let wrapper: HTMLDivElement;
let rect: DOMRect;
$: rect =
wrapper && width
? wrapper.getBoundingClientRect()
: ({ x: 0, y: 0, width: 0, height: 0 } as DOMRect);
let camera: OrthographicCamera;
let camera = $state<OrthographicCamera>(null!);
const minZoom = 1;
const maxZoom = 40;
let mousePosition = [0, 0];
let mouseDown: null | [number, number] = null;
let mousePosition = $state([0, 0]);
let mouseDown = $state<[number, number] | null>(null);
let mouseDownId = -1;
let boxSelection = false;
let boxSelection = $state(false);
const cameraDown = [0, 0];
let cameraPosition: [number, number, number] = [0, 0, 4];
let addMenuPosition: [number, number] | null = null;
let cameraPosition: [number, number, number] = $state([0, 0, 4]);
let addMenuPosition = $state<[number, number] | null>(null);
let clipboard: null | {
nodes: Node[];
edges: [number, number, number, string][];
} = null;
let width = rect?.width ?? 100;
let height = rect?.height ?? 100;
let cameraBounds = [-1000, 1000, -1000, 1000];
$: cameraBounds = [
const cameraBounds = $derived([
cameraPosition[0] - width / cameraPosition[2] / 2,
cameraPosition[0] + width / cameraPosition[2] / 2,
cameraPosition[1] - height / cameraPosition[2] / 2,
cameraPosition[1] + height / cameraPosition[2] / 2,
];
]);
function setCameraTransform(
x = cameraPosition[0],
y = cameraPosition[1],
@@ -82,7 +76,7 @@
localStorage.setItem("cameraPosition", JSON.stringify(cameraPosition));
}
function updateNodePosition(node: NodeType) {
function updateNodePosition(node: Node) {
if (node?.tmp?.ref && node?.tmp?.mesh) {
if (node.tmp["x"] !== undefined && node.tmp["y"] !== undefined) {
node.tmp.ref.style.setProperty("--nx", `${node.tmp.x * 10}px`);
@@ -96,6 +90,7 @@
delete node.tmp.x;
delete node.tmp.y;
}
graph.edges = [...graph.edges];
} else {
node.tmp.ref.style.setProperty("--nx", `${node.position[0] * 10}px`);
node.tmp.ref.style.setProperty("--ny", `${node.position[1] * 10}px`);
@@ -112,7 +107,7 @@
if (nodeTypeId in nodeHeightCache) {
return nodeHeightCache[nodeTypeId];
}
const node = manager.getNodeType(nodeTypeId);
const node = graph.getNodeType(nodeTypeId);
if (!node?.inputs) {
return 5;
}
@@ -131,7 +126,7 @@
}
setContext("getNodeHeight", getNodeHeight);
setContext("isNodeInView", (node: NodeType) => {
setContext("isNodeInView", (node: Node) => {
const height = getNodeHeight(node.type);
const width = 20;
return (
@@ -162,7 +157,7 @@
// we are going to check if we clicked on a node by coordinates
if (clickedNodeId === -1) {
const [downX, downY] = projectScreenToWorld(mx, my);
for (const node of $nodes.values()) {
for (const node of graph.nodes.values()) {
const x = node.position[0];
const y = node.position[1];
const height = getNodeHeight(node.type);
@@ -183,13 +178,13 @@
// remove existing edge
if (typeof index === "string") {
const edges = manager.getEdgesToNode(node);
const edges = graph.getEdgesToNode(node);
for (const edge of edges) {
if (edge[3] === index) {
node = edge[0];
index = edge[1];
position = getSocketPosition(node, index);
manager.removeEdge(edge);
graph.removeEdge(edge);
break;
}
}
@@ -202,7 +197,7 @@
position,
};
graphState.possibleSockets = manager
graphState.possibleSockets = graph
.getPossibleSockets(graphState.activeSocket)
.map(([node, index]) => {
return {
@@ -227,7 +222,7 @@
}
function getSocketPosition(
node: NodeType,
node: Node,
index: string | number,
): [number, number] {
if (typeof index === "number") {
@@ -294,7 +289,7 @@
const x2 = Math.max(mouseD[0], mousePosition[0]);
const y1 = Math.min(mouseD[1], mousePosition[1]);
const y2 = Math.max(mouseD[1], mousePosition[1]);
for (const node of $nodes.values()) {
for (const node of graph.nodes.values()) {
if (!node?.tmp) continue;
const x = node.position[0];
const y = node.position[1];
@@ -310,7 +305,7 @@
// here we are handling dragging of nodes
if (graphState.activeNodeId !== -1 && mouseDownId !== -1) {
const node = manager.getNode(graphState.activeNodeId);
const node = graph.getNode(graphState.activeNodeId);
if (!node || event.buttons !== 1) return;
node.tmp = node.tmp || {};
@@ -341,7 +336,7 @@
if (graphState.selectedNodes?.size) {
for (const nodeId of graphState.selectedNodes) {
const n = manager.getNode(nodeId);
const n = graph.getNode(nodeId);
if (!n?.tmp) continue;
n.tmp.x = (n?.tmp?.downX || 0) - vecX;
n.tmp.y = (n?.tmp?.downY || 0) - vecY;
@@ -354,7 +349,6 @@
updateNodePosition(node);
$edges = $edges;
return;
}
@@ -436,10 +430,10 @@
graphState.activeNodeId = clickedNodeId;
// select the node
} else if (event.shiftKey) {
const activeNode = manager.getNode(graphState.activeNodeId);
const newNode = manager.getNode(clickedNodeId);
const activeNode = graph.getNode(graphState.activeNodeId);
const newNode = graph.getNode(clickedNodeId);
if (activeNode && newNode) {
const edge = manager.getNodesBetween(activeNode, newNode);
const edge = graph.getNodesBetween(activeNode, newNode);
if (edge) {
graphState.selectedNodes.clear();
for (const node of edge) {
@@ -456,7 +450,7 @@
boxSelection = true;
}
const node = manager.getNode(graphState.activeNodeId);
const node = graph.getNode(graphState.activeNodeId);
if (!node) return;
node.tmp = node.tmp || {};
node.tmp.downX = node.position[0];
@@ -464,7 +458,7 @@
if (graphState.selectedNodes) {
for (const nodeId of graphState.selectedNodes) {
const n = manager.getNode(nodeId);
const n = graph.getNode(nodeId);
if (!n) continue;
n.tmp = n.tmp || {};
n.tmp.downX = n.position[0];
@@ -480,10 +474,10 @@
graphState.activeNodeId,
...(graphState.selectedNodes?.values() || []),
]
.map((id) => manager.getNode(id))
.map((id) => graph.getNode(id))
.filter(Boolean) as Node[];
const _edges = manager.getEdgesBetweenNodes(_nodes);
const _edges = graph.getEdgesBetweenNodes(_nodes);
_nodes = _nodes.map((_node) => {
const node = globalThis.structuredClone({
@@ -514,7 +508,7 @@
})
.filter(Boolean) as Node[];
const newNodes = manager.createGraph(_nodes, clipboard.edges);
const newNodes = graph.createGraph(_nodes, clipboard.edges);
graphState.selectedNodes.clear();
for (const node of newNodes) {
graphState.selectedNodes.add(node.id);
@@ -527,9 +521,9 @@
key: "l",
description: "Select linked nodes",
callback: () => {
const activeNode = manager.getNode(graphState.activeNodeId);
const activeNode = graph.getNode(graphState.activeNodeId);
if (activeNode) {
const nodes = manager.getLinkedNodes(activeNode);
const nodes = graph.getLinkedNodes(activeNode);
graphState.selectedNodes.clear();
for (const node of nodes) {
graphState.selectedNodes.add(node.id);
@@ -542,7 +536,8 @@
key: "?",
description: "Toggle Help",
callback: () => {
showHelp = !showHelp;
// TODO: fix this
// showHelp = !showHelp;
},
});
@@ -586,12 +581,12 @@
if (!isBodyFocused()) return;
const average = [0, 0];
for (const node of $nodes.values()) {
for (const node of graph.nodes.values()) {
average[0] += node.position[0];
average[1] += node.position[1];
}
average[0] = average[0] ? average[0] / $nodes.size : 0;
average[1] = average[1] ? average[1] / $nodes.size : 0;
average[0] = average[0] ? average[0] / graph.nodes.size : 0;
average[1] = average[1] ? average[1] / graph.nodes.size : 0;
const camX = cameraPosition[0];
const camY = cameraPosition[1];
@@ -617,7 +612,7 @@
description: "Select all nodes",
callback: () => {
if (!isBodyFocused()) return;
for (const node of $nodes.keys()) {
for (const node of graph.nodes.keys()) {
graphState.selectedNodes.add(node);
}
},
@@ -629,8 +624,8 @@
description: "Undo",
callback: () => {
if (!isBodyFocused()) return;
manager.undo();
for (const node of $nodes.values()) {
graph.undo();
for (const node of graph.nodes.values()) {
updateNodePosition(node);
}
},
@@ -641,8 +636,8 @@
ctrl: true,
description: "Redo",
callback: () => {
manager.redo();
for (const node of $nodes.values()) {
graph.redo();
for (const node of graph.nodes.values()) {
updateNodePosition(node);
}
},
@@ -654,7 +649,7 @@
description: "Save",
preventDefault: true,
callback: () => {
const state = manager.serialize();
const state = graph.serialize();
const blob = new Blob([JSON.stringify(state)], {
type: "application/json;charset=utf-8",
});
@@ -667,24 +662,24 @@
description: "Delete selected nodes",
callback: (event) => {
if (!isBodyFocused()) return;
manager.startUndoGroup();
graph.startUndoGroup();
if (graphState.activeNodeId !== -1) {
const node = manager.getNode(graphState.activeNodeId);
const node = graph.getNode(graphState.activeNodeId);
if (node) {
manager.removeNode(node, { restoreEdges: event.ctrlKey });
graph.removeNode(node, { restoreEdges: event.ctrlKey });
graphState.activeNodeId = -1;
}
}
if (graphState.selectedNodes) {
for (const nodeId of graphState.selectedNodes) {
const node = manager.getNode(nodeId);
const node = graph.getNode(nodeId);
if (node) {
manager.removeNode(node, { restoreEdges: event.ctrlKey });
graph.removeNode(node, { restoreEdges: event.ctrlKey });
}
}
graphState.clearSelection();
}
manager.saveUndoGroup();
graph.saveUndoGroup();
},
});
@@ -692,7 +687,7 @@
isPanning = false;
if (!mouseDown) return;
const activeNode = manager.getNode(graphState.activeNodeId);
const activeNode = graph.getNode(graphState.activeNodeId);
const clickedNodeId = getNodeIdFromEvent(event);
@@ -724,9 +719,9 @@
}
const nodes = [
...[...(graphState.selectedNodes?.values() || [])].map((id) =>
manager.getNode(id),
graph.getNode(id),
),
] as NodeType[];
] as Node[];
const vec = [
activeNode.position[0] - (activeNode?.tmp.x || 0),
@@ -758,16 +753,14 @@
}
}
}
$edges = $edges;
});
manager.save();
graph.save();
} else if (graphState.hoveredSocket && graphState.activeSocket) {
if (
typeof graphState.hoveredSocket.index === "number" &&
typeof graphState.activeSocket.index === "string"
) {
manager.createEdge(
graph.createEdge(
graphState.hoveredSocket.node,
graphState.hoveredSocket.index || 0,
graphState.activeSocket.node,
@@ -777,14 +770,14 @@
typeof graphState.activeSocket.index == "number" &&
typeof graphState.hoveredSocket.index === "string"
) {
manager.createEdge(
graph.createEdge(
graphState.activeSocket.node,
graphState.activeSocket.index || 0,
graphState.hoveredSocket.node,
graphState.hoveredSocket.index,
);
}
manager.save();
graph.save();
}
// check if camera moved
@@ -807,9 +800,9 @@
addMenuPosition = null;
}
let isPanning = false;
let isDragging = false;
let hoveredNodeId = -1;
let isPanning = $state(false);
let isDragging = $state(false);
let hoveredNodeId = $state(-1);
function handleMouseLeave() {
isDragging = false;
@@ -820,7 +813,7 @@
event.preventDefault();
isDragging = false;
if (!event.dataTransfer) return;
const nodeId = event.dataTransfer.getData("data/node-id") as NodeId;
const nodeId = event.dataTransfer.getData("data/node-id") as NodeType;
let mx = event.clientX - rect.x;
let my = event.clientY - rect.y;
@@ -841,8 +834,8 @@
}
const pos = projectScreenToWorld(mx, my);
manager.registry.load([nodeId]).then(() => {
manager.createNode({
graph.registry.load([nodeId]).then(() => {
graph.createNode({
type: nodeId,
props,
position: pos,
@@ -856,9 +849,9 @@
reader.onload = async (e) => {
const buffer = e.target?.result;
if (buffer?.constructor === ArrayBuffer) {
const nodeType = await manager.registry.register(buffer);
const nodeType = await graph.registry.register(buffer);
manager.createNode({
graph.createNode({
type: nodeType.id,
props: {},
position: projectScreenToWorld(mx, my),
@@ -869,10 +862,10 @@
} else if (file.type === "application/json") {
const reader = new FileReader();
reader.onload = (e) => {
const buffer = e.target?.result as Buffer;
const buffer = e.target?.result as ArrayBuffer;
if (buffer) {
const state = GraphSchema.parse(JSON.parse(buffer.toString()));
manager.load(state);
graph.load(state);
}
};
reader.readAsText(file);
@@ -908,10 +901,10 @@
});
</script>
<svelte:window on:mousemove={handleMouseMove} on:mouseup={handleMouseUp} />
<svelte:window onmousemove={handleMouseMove} onmouseup={handleMouseUp} />
<div
on:wheel={handleMouseScroll}
onwheel={handleMouseScroll}
bind:this={wrapper}
class="graph-wrapper"
class:is-panning={isPanning}
@@ -921,21 +914,21 @@
tabindex="0"
bind:clientWidth={width}
bind:clientHeight={height}
on:dragenter={handleDragEnter}
on:dragover={handlerDragOver}
on:dragexit={handleDragEnd}
on:drop={handleDrop}
on:mouseleave={handleMouseLeave}
on:keydown={keymap.handleKeyboardEvent}
on:mousedown={handleMouseDown}
ondragenter={handleDragEnter}
ondragover={handlerDragOver}
ondragexit={handleDragEnd}
ondrop={handleDrop}
onmouseleave={handleMouseLeave}
onkeydown={keymap.handleKeyboardEvent}
onmousedown={handleMouseDown}
>
<input
type="file"
accept="application/wasm,application/json"
id="drop-zone"
disabled={!isDragging}
on:dragend={handleDragEnd}
on:dragleave={handleDragEnd}
ondragend={handleDragEnd}
ondragleave={handleDragEnd}
/>
<label for="drop-zone"></label>
@@ -958,9 +951,9 @@
/>
{/if}
{#if $status === "idle"}
{#if graph.status === "idle"}
{#if addMenuPosition}
<AddMenu bind:position={addMenuPosition} graph={manager} />
<AddMenu bind:position={addMenuPosition} {graph} />
{/if}
{#if graphState.activeSocket}
@@ -974,17 +967,17 @@
/>
{/if}
<GraphView {nodes} {edges} {cameraPosition} />
{:else if $status === "loading"}
<GraphView nodes={graph.nodes} edges={graph.edges} {cameraPosition} />
{:else if graph.status === "loading"}
<span>Loading</span>
{:else if $status === "error"}
{:else if graph.status === "error"}
<span>Error</span>
{/if}
</Canvas>
</div>
{#if showHelp}
<HelpView registry={manager.registry} />
<HelpView registry={graph.registry} />
{/if}
<style>

View File

@@ -4,14 +4,13 @@
import Edge from "../edges/Edge.svelte";
import Node from "../node/Node.svelte";
import { getContext, onMount } from "svelte";
import type { Writable } from "svelte/store";
import { getGraphState } from "./state.svelte";
import { useThrelte } from "@threlte/core";
import { appSettings } from "$lib/settings/app-settings.svelte";
type Props = {
nodes: Writable<Map<number, NodeType>>;
edges: Writable<EdgeType[]>;
nodes: Map<number, NodeType>;
edges: EdgeType[];
cameraPosition: [number, number, number];
};
@@ -20,7 +19,7 @@
const { invalidate } = useThrelte();
$effect(() => {
appSettings.theme;
appSettings.value.theme;
invalidate();
});
@@ -33,14 +32,24 @@
"getSocketPosition",
);
function getEdgePosition(edge: EdgeType) {
const pos1 = getSocketPosition(edge[0], edge[1]);
const pos2 = getSocketPosition(edge[2], edge[3]);
return [pos1[0], pos1[1], pos2[0], pos2[1]];
}
const edgePositions = $derived(
edges.map((edge) => {
const fromNode = nodes.get(edge[0].id);
const toNode = nodes.get(edge[2].id);
// This check is important because nodes might not be there during some transitions.
if (!fromNode || !toNode) {
return [0, 0, 0, 0];
}
const pos1 = getSocketPosition(fromNode, edge[1]);
const pos2 = getSocketPosition(toNode, edge[3]);
return [pos1[0], pos1[1], pos2[0], pos2[1]];
}),
);
onMount(() => {
for (const node of $nodes.values()) {
for (const node of nodes.values()) {
if (node?.tmp?.ref) {
node.tmp.ref.style.setProperty("--nx", `${node.position[0] * 10}px`);
node.tmp.ref.style.setProperty("--ny", `${node.position[1] * 10}px`);
@@ -49,9 +58,8 @@
});
</script>
{#each $edges as edge (`${edge[0].id}-${edge[1]}-${edge[2].id}-${edge[3]}`)}
{@const pos = getEdgePosition(edge)}
{@const [x1, y1, x2, y2] = pos}
{#each edgePositions as edge (`${edge.join("-")}`)}
{@const [x1, y1, x2, y2] = edge}
<Edge
z={cameraPosition[2]}
from={{
@@ -74,7 +82,7 @@
style:transform={`scale(${cameraPosition[2] * 0.1})`}
class:hovering-sockets={graphState.activeSocket}
>
{#each $nodes.values() as node (node.id)}
{#each nodes.values() as node (node.id)}
<Node
{node}
inView={cameraPosition && isNodeInView(node)}

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import type { Graph, Node, NodeRegistry } from "@nodes/types";
import GraphEl from "./Graph.svelte";
import { GraphManager } from "../graph-manager.js";
import { GraphManager } from "../graph-manager.svelte";
import { setContext } from "svelte";
import { debounce } from "$lib/helpers";
import { createKeyMap } from "$lib/helpers/createKeyMap";
@@ -48,23 +48,23 @@
$effect(() => {
if (graphState.activeNodeId !== -1) {
activeNode = manager.getNode(graphState.activeNodeId);
} else {
} else if (activeNode) {
activeNode = undefined;
}
});
const updateSettings = debounce((s) => {
const updateSettings = debounce((s: Record<string, any>) => {
manager.setSettings(s);
}, 200);
$effect(() => {
if (settingTypes && settings) {
updateSettings($state.snapshot(settings));
updateSettings(settings);
}
});
manager.on("settings", (_settings) => {
settingTypes = _settings.types;
settingTypes = { ...settingTypes, ..._settings.types };
settings = _settings.values;
});

View File

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

View File

@@ -1,4 +1,4 @@
import type { GraphManager } from "../graph-manager.js";
import type { GraphManager } from "../graph-manager.svelte";
import { getContext } from "svelte";
export function getGraphManager(): GraphManager {

View File

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

View File

@@ -1,15 +1,15 @@
import throttle from './throttle.js';
import throttle from "$lib/helpers/throttle";
type EventMap = Record<string, unknown>;
type EventKey<T extends EventMap> = string & keyof T;
type EventReceiver<T> = (params: T, stuff?: Record<string, unknown>) => unknown;
export default class EventEmitter<T extends EventMap = { [key: string]: unknown }> {
export default class EventEmitter<
T extends EventMap = { [key: string]: unknown },
> {
index = 0;
public eventMap: T = {} as T;
constructor() {
}
constructor() {}
private cbs: { [key: string]: ((data?: unknown) => unknown)[] } = {};
private cbsOnce: { [key: string]: ((data?: unknown) => unknown)[] } = {};
@@ -29,7 +29,11 @@ export default class EventEmitter<T extends EventMap = { [key: string]: unknown
}
}
public on<K extends EventKey<T>>(event: K, cb: EventReceiver<T[K]>, throttleTimer = 0) {
public on<K extends EventKey<T>>(
event: K,
cb: EventReceiver<T[K]>,
throttleTimer = 0,
) {
if (throttleTimer > 0) cb = throttle(cb, throttleTimer);
const cbs = Object.assign(this.cbs, {
[event]: [...(this.cbs[event] || []), cb],
@@ -38,7 +42,7 @@ export default class EventEmitter<T extends EventMap = { [key: string]: unknown
// console.log('New EventEmitter ', this.constructor.name);
return () => {
cbs[event]?.splice(cbs[event].indexOf(cb), 1);
this.cbs[event]?.splice(cbs[event].indexOf(cb), 1);
};
}
@@ -48,10 +52,17 @@ export default class EventEmitter<T extends EventMap = { [key: string]: unknown
* @param {function} cb Listener, gets called everytime the event is emitted
* @returns {function} Returns a function which removes the listener when called
*/
public once<K extends EventKey<T>>(event: K, cb: EventReceiver<T[K]>): () => void {
this.cbsOnce[event] = [...(this.cbsOnce[event] || []), cb];
public once<K extends EventKey<T>>(
event: K,
cb: EventReceiver<T[K]>,
): () => void {
const cbsOnce = Object.assign(this.cbsOnce, {
[event]: [...(this.cbsOnce[event] || []), cb],
});
this.cbsOnce = cbsOnce;
return () => {
this.cbsOnce[event].splice(this.cbsOnce[event].indexOf(cb), 1);
cbsOnce[event]?.splice(cbsOnce[event].indexOf(cb), 1);
};
}

View File

@@ -6,7 +6,10 @@ export function lerp(a: number, b: number, t: number) {
return a + (b - a) * t;
}
export function animate(duration: number, callback: (progress: number) => void | false) {
export function animate(
duration: number,
callback: (progress: number) => void | false,
) {
const start = performance.now();
const loop = (time: number) => {
const progress = (time - start) / duration;
@@ -18,7 +21,7 @@ export function animate(duration: number, callback: (progress: number) => void |
} else {
callback(1);
}
}
};
requestAnimationFrame(loop);
}
@@ -33,33 +36,37 @@ export function createNodePath({
aspectRatio = 1,
} = {}) {
return `M0,${cornerTop}
${cornerTop
? ` V${cornerTop}
${
cornerTop
? ` V${cornerTop}
Q0,0 ${cornerTop * aspectRatio},0
H${100 - cornerTop * aspectRatio}
Q100,0 100,${cornerTop}
`
: ` V0
: ` V0
H100
`
}
}
V${y - height / 2}
${rightBump
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
: ` H100`
}
${cornerBottom
? ` V${100 - cornerBottom}
${
rightBump
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
: ` H100`
}
${
cornerBottom
? ` V${100 - cornerBottom}
Q100,100 ${100 - cornerBottom * aspectRatio},100
H${cornerBottom * aspectRatio}
Q0,100 0,${100 - cornerBottom}
`
: `${leftBump ? `V100 H0` : `V100`}`
}
${leftBump
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${y - height / 2}`
: ` H0`
}
: `${leftBump ? `V100 H0` : `V100`}`
}
${
leftBump
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${y - height / 2}`
: ` H0`
}
Z`.replace(/\s+/g, " ");
}
@@ -71,35 +78,14 @@ export const debounce = (fn: Function, ms = 300) => {
};
};
export const clone: <T>(v: T) => T = "structedClone" in globalThis ? globalThis.structuredClone : (obj) => JSON.parse(JSON.stringify(obj));
export const createLogger = (() => {
let maxLength = 5;
return (scope: string) => {
maxLength = Math.max(maxLength, scope.length);
let muted = false;
return {
log: (...args: any[]) => !muted && console.log(`[%c${scope.padEnd(maxLength, " ")}]:`, "color: #888", ...args),
group: (...args: any[]) => !muted && console.groupCollapsed(`[%c${scope.padEnd(maxLength, " ")}]:`, "color: #888", ...args),
groupEnd: () => !muted && console.groupEnd(),
info: (...args: any[]) => !muted && console.info(`[%c${scope.padEnd(maxLength, " ")}]:`, "color: #888", ...args),
warn: (...args: any[]) => !muted && console.warn(`[%c${scope.padEnd(maxLength, " ")}]:`, "color: #888", ...args),
error: (...args: any[]) => console.error(`[%c${scope.padEnd(maxLength, " ")}]:`, "color: #f88", ...args),
mute() {
muted = true;
},
unmute() {
muted = false;
}
}
}
})();
export const clone: <T>(v: T) => T =
"structedClone" in globalThis
? globalThis.structuredClone
: (obj) => JSON.parse(JSON.stringify(obj));
export function withSubComponents<A, B extends Record<string, any>>(
component: A,
subcomponents: B
subcomponents: B,
): A & B {
Object.keys(subcomponents).forEach((key) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@@ -1,20 +0,0 @@
export default <R, A extends any[]>(
fn: (...args: A) => R,
delay: number
): ((...args: A) => R) => {
let wait = false;
return (...args: A) => {
if (wait) return undefined;
const val = fn(...args);
wait = true;
setTimeout(() => {
wait = false;
}, delay);
return val;
}
};

View File

@@ -1,24 +1,24 @@
import { create, type Delta } from "jsondiffpatch";
import type { Graph } from "@nodes/types";
import { createLogger, clone } from "./helpers/index.js";
import { clone } from "./helpers/index.js";
import { createLogger } from "@nodes/utils";
const diff = create({
objectHash: function (obj, index) {
if (obj === null) return obj;
if ("id" in obj) return obj.id;
if ("id" in obj) return obj.id as string;
if ("_id" in obj) return obj._id as string;
if (Array.isArray(obj)) {
return obj.join("-")
return obj.join("-");
}
return obj?.id || obj._id || '$$index:' + index;
}
})
return "$$index:" + index;
},
});
const log = createLogger("history")
log.mute();
const log = createLogger("history");
// log.mute();
export class HistoryManager {
index: number = -1;
history: Delta[] = [];
private initialState: Graph | undefined;
@@ -27,26 +27,25 @@ export class HistoryManager {
private opts = {
debounce: 400,
maxHistory: 100,
}
};
constructor({ maxHistory = 100, debounce = 100 } = {}) {
this.history = [];
this.index = -1;
this.opts.debounce = debounce;
this.opts.maxHistory = maxHistory;
globalThis["_history"] = this;
}
save(state: Graph) {
if (!this.state) {
this.state = clone(state);
this.initialState = this.state;
log.log("initial state saved")
log.log("initial state saved");
} else {
const newState = state;
const delta = diff.diff(this.state, newState);
if (delta) {
log.log("saving state")
log.log("saving state");
// Add the delta to history
if (this.index < this.history.length - 1) {
// Clear the history after the current index if new changes are made
@@ -62,7 +61,7 @@ export class HistoryManager {
}
this.state = newState;
} else {
log.log("no changes")
log.log("no changes");
}
}
}
@@ -76,7 +75,7 @@ export class HistoryManager {
undo() {
if (this.index === -1 && this.initialState) {
log.log("reached start, loading initial state")
log.log("reached start, loading initial state");
return clone(this.initialState);
} else {
const delta = this.history[this.index];
@@ -96,7 +95,7 @@ export class HistoryManager {
this.state = nextState;
return clone(nextState);
} else {
log.log("reached end")
log.log("reached end");
}
}
}

View File

@@ -17,18 +17,18 @@
inView: boolean;
z: number;
};
const { node, inView, z }: Props = $props();
let { node = $bindable(), inView, z }: Props = $props();
const isActive = $derived(graphState.activeNodeId === node.id);
const isSelected = $derived(graphState.selectedNodes.has(node.id));
let strokeColor = $state(colors.selected);
$effect(() => {
appSettings.theme;
appSettings.value.theme;
strokeColor = isSelected
? colors.selected.clone()
? colors.selected
: isActive
? colors.active.clone()
: colors.outline.clone();
? colors.active
: colors.outline;
});
const updateNodePosition =
@@ -41,13 +41,12 @@
const height = getNodeHeight?.(node.type);
$effect(() => {
node.tmp = node.tmp || {};
if (!node?.tmp) node.tmp = {};
node.tmp.mesh = meshRef;
updateNodePosition?.(node);
});
onMount(() => {
node.tmp = node.tmp || {};
if (!node.tmp) node.tmp = {};
node.tmp.mesh = meshRef;
updateNodePosition?.(node);
});
@@ -79,4 +78,4 @@
/>
</T.Mesh>
<NodeHtml {node} {inView} {isActive} {isSelected} {z} />
<NodeHtml bind:node {inView} {isActive} {isSelected} {z} />

View File

@@ -8,8 +8,7 @@
type Props = {
node: Node;
position?: "absolute" | "fixed";
position?: "absolute" | "fixed" | "relative";
isActive?: boolean;
isSelected?: boolean;
inView?: boolean;
@@ -17,7 +16,7 @@
};
let {
node,
node = $bindable(),
position = "absolute",
isActive = false,
isSelected = false,
@@ -28,9 +27,7 @@
const zOffset = (node.tmp?.random || 0) * 0.5;
const zLimit = 2 - zOffset;
const type = node?.tmp?.type;
const parameters = Object.entries(type?.inputs || {}).filter(
const parameters = Object.entries(node?.tmp?.type?.inputs || {}).filter(
(p) =>
p[1].type !== "seed" && !("setting" in p[1]) && p[1]?.hidden !== true,
);

View File

@@ -3,7 +3,7 @@
import type { Node, Socket } from "@nodes/types";
import { getContext } from "svelte";
export let node: Node;
const { node }: { node: Node } = $props();
const setDownSocket = getContext<(socket: Socket) => void>("setDownSocket");
const getSocketPosition =
@@ -33,14 +33,14 @@
rightBump,
aspectRatio,
});
const pathDisabled = createNodePath({
depth: 0,
height: 15,
y: 50,
cornerTop,
rightBump,
aspectRatio,
});
// const pathDisabled = createNodePath({
// depth: 0,
// height: 15,
// y: 50,
// cornerTop,
// rightBump,
// aspectRatio,
// });
const pathHover = createNodePath({
depth: 8.5,
height: 50,
@@ -59,7 +59,7 @@
class="click-target"
role="button"
tabindex="0"
on:mousedown={handleMouseDown}
onmousedown={handleMouseDown}
></div>
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -11,7 +11,7 @@
};
const {
node,
node = $bindable(),
input,
id,
elementId = `input-${Math.random().toString(36).substring(7)}`,
@@ -19,7 +19,18 @@
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(() => {
if (value !== undefined && node?.props?.[id] !== value) {

View File

@@ -17,7 +17,7 @@
isLast?: boolean;
};
const { node = $bindable(), input, id, isLast }: Props = $props();
let { node = $bindable(), input, id, isLast }: Props = $props();
const inputType = node?.tmp?.type?.inputs?.[id]!;
@@ -26,7 +26,6 @@
const graph = getGraphManager();
const graphState = getGraphState();
const graphId = graph?.id;
const inputSockets = graph?.inputSockets;
const elementId = `input-${Math.random().toString(36).substring(7)}`;
@@ -83,12 +82,12 @@
class:disabled={!graphState?.possibleSocketIds.has(socketId)}
>
{#key id && graphId}
<div class="content" class:disabled={$inputSockets?.has(socketId)}>
<div class="content" class:disabled={graph?.inputSockets?.has(socketId)}>
{#if inputType.label !== ""}
<label for={elementId}>{input.label || id}</label>
{/if}
{#if inputType.external !== true}
<NodeInput {elementId} {node} {input} {id} />
<NodeInput {elementId} bind:node {input} {id} />
{/if}
</div>

View File

@@ -0,0 +1,20 @@
import type { Node, NodeDefinition } from "@nodes/types";
export type GraphNode = Node & {
tmp?: {
depth?: number;
mesh?: any;
random?: number;
parents?: Node[];
children?: Node[];
inputNodes?: Record<string, Node>;
type?: NodeDefinition;
downX?: number;
downY?: number;
x?: number;
y?: number;
ref?: HTMLElement;
visible?: boolean;
isMoving?: boolean;
};
};

View File

@@ -1,6 +1,5 @@
<script lang="ts">
import { getContext } from "svelte";
import type { Writable } from "svelte/store";
let index = -1;
let wrapper: HTMLDivElement;
@@ -9,7 +8,7 @@
index = getContext<() => number>("registerCell")();
}
const sizes = getContext<Writable<string[]>>("sizes");
const sizes = getContext<{ value: string[] }>("sizes");
let downSizes: string[] = [];
let downWidth = 0;
@@ -17,7 +16,7 @@
let startX = 0;
function handleMouseDown(event: MouseEvent) {
downSizes = [...$sizes];
downSizes = [...sizes.value];
mouseDown = true;
startX = event.clientX;
downWidth = wrapper.getBoundingClientRect().width;
@@ -26,8 +25,7 @@
function handleMouseMove(event: MouseEvent) {
if (mouseDown) {
const width = downWidth + startX - event.clientX;
$sizes[index] = `${width}px`;
$sizes = $sizes;
sizes.value[index] = `${width}px`;
}
}
</script>

View File

@@ -1,27 +1,31 @@
<script lang="ts">
import { setContext, getContext } from "svelte";
import localStore from "$lib/helpers/localStore";
import { localState } from "$lib/helpers/localState.svelte";
const gridId = getContext<string>("grid-id") || "grid-0";
let sizes = localStore<string[]>(gridId, []);
let sizes = localState<string[]>(gridId, []);
const { children } = $props();
let registerIndex = 0;
setContext("registerCell", function () {
let index = registerIndex;
registerIndex++;
if (registerIndex > $sizes.length) {
$sizes = [...$sizes, "1fr"];
if (registerIndex > sizes.value.length) {
sizes.value = [...sizes.value, "1fr"];
}
return index;
});
setContext("sizes", sizes);
$: cols = $sizes.map((size, i) => `${i > 0 ? "1px " : ""}` + size).join(" ");
const cols = $derived(
sizes.value.map((size, i) => `${i > 0 ? "1px " : ""}` + size).join(" "),
);
</script>
<div class="wrapper" style={`grid-template-columns: ${cols};`}>
<slot />
{@render children()}
</div>
<style>

View File

@@ -65,7 +65,7 @@ export function createNodePath({
export const debounce = (fn: Function, ms = 300) => {
let timeoutId: ReturnType<typeof setTimeout>;
return function(this: any, ...args: any[]) {
return function (this: any, ...args: any[]) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), ms);
};
@@ -131,41 +131,100 @@ export function humanizeDuration(durationInMilliseconds: number) {
return durationString.trim();
}
export function debounceAsyncFunction<T extends any[], R>(func: (...args: T) => Promise<R>): (...args: T) => Promise<R> {
let currentPromise: Promise<R> | null = null;
let nextArgs: T | null = null;
let resolveNext: ((result: R) => void) | null = null;
// export function debounceAsyncFunction<T extends any[], R>(
// func: (...args: T) => Promise<R>
// ): (...args: T) => Promise<R> {
// let timeoutId: ReturnType<typeof setTimeout> | null = null;
// let lastPromise: Promise<R> | null = null;
// let lastReject: ((reason?: any) => void) | null = null;
//
// return (...args: T): Promise<R> => {
// if (timeoutId) {
// clearTimeout(timeoutId);
// if (lastReject) {
// lastReject(new Error("Debounced: Previous call was canceled."));
// }
// }
//
// return new Promise<R>((resolve, reject) => {
// lastReject = reject;
// timeoutId = setTimeout(() => {
// timeoutId = null;
// lastReject = null;
// lastPromise = func(...args).then(resolve, reject);
// }, 300); // Default debounce time is 300ms; you can make this configurable.
// });
// };
// }
export function debounceAsyncFunction<T extends (...args: any[]) => Promise<any>>(asyncFn: T): T {
let isRunning = false;
let latestArgs: Parameters<T> | null = null;
let resolveNext: (() => void) | null = null;
const debouncedFunction = async (...args: T): Promise<R> => {
if (currentPromise) {
// Store the latest arguments and create a new promise to resolve them later
nextArgs = args;
return new Promise<R>((resolve) => {
return (async function serializedFunction(...args: Parameters<T>): Promise<ReturnType<T>> {
latestArgs = args;
if (isRunning) {
// Wait for the current execution to finish
await new Promise<void>((resolve) => {
resolveNext = resolve;
});
} else {
// Execute the function immediately
try {
currentPromise = func(...args);
const result = await currentPromise;
return result;
} finally {
currentPromise = null;
// If there are stored arguments, call the function again with the latest arguments
if (nextArgs) {
const argsToUse = nextArgs;
const resolver = resolveNext;
nextArgs = null;
resolveNext = null;
resolver!(await debouncedFunction(...argsToUse));
}
}
// Indicate the function is running
isRunning = true;
try {
// Execute with the latest arguments
const result = await asyncFn(...latestArgs!);
return result;
} finally {
// Allow the next execution
isRunning = false;
if (resolveNext) {
resolveNext();
resolveNext = null;
}
}
};
return debouncedFunction;
}) as T;
}
// export function debounceAsyncFunction<T extends any[], R>(func: (...args: T) => Promise<R>): (...args: T) => Promise<R> {
// let currentPromise: Promise<R> | null = null;
// let nextArgs: T | null = null;
// let resolveNext: ((result: R) => void) | null = null;
//
// const debouncedFunction = async (...args: T): Promise<R> => {
// if (currentPromise) {
// // Store the latest arguments and create a new promise to resolve them later
// nextArgs = args;
// return new Promise<R>((resolve) => {
// resolveNext = resolve;
// });
// } else {
// // Execute the function immediately
// try {
// currentPromise = func(...args);
// const result = await currentPromise;
// return result;
// } finally {
// currentPromise = null;
// // If there are stored arguments, call the function again with the latest arguments
// if (nextArgs) {
// const argsToUse = nextArgs;
// const resolver = resolveNext;
// nextArgs = null;
// resolveNext = null;
// resolver!(await debouncedFunction(...argsToUse));
// }
// }
// }
// };
//
// return debouncedFunction;
// }
export function withArgsChangeOnly<T extends any[], R>(func: (...args: T) => R): (...args: T) => R {
let lastArgs: T | undefined = undefined;
let lastResult: R;

View File

@@ -1,11 +1,34 @@
export function localState<T>(key: string, defaultValue: T): T {
const stored = localStorage.getItem(key)
const state = $state(stored ? JSON.parse(stored) : defaultValue)
$effect.root(() => {
$effect(() => {
const value = $state.snapshot(state);
localStorage.setItem(key, JSON.stringify(value));
import { browser } from "$app/environment";
export class LocalStore<T> {
value = $state<T>() as T;
key = "";
constructor(key: string, value: T) {
this.key = key;
this.value = value;
if (browser) {
const item = localStorage.getItem(key);
if (item) this.value = this.deserialize(item);
}
$effect.root(() => {
$effect(() => {
localStorage.setItem(this.key, this.serialize(this.value));
});
});
});
return state;
}
serialize(value: T): string {
return JSON.stringify(value);
}
deserialize(item: string): T {
return JSON.parse(item);
}
}
export function localState<T>(key: string, value: T) {
return new LocalStore(key, value);
}

View File

@@ -1,20 +1,19 @@
export default <R, A extends any[]>(
fn: (...args: A) => R,
delay: number
): ((...args: A) => R) => {
let wait = false;
export default <T extends unknown[]>(
callback: (...args: T) => void,
delay: number,
) => {
let isWaiting = false;
return (...args: A) => {
if (wait) return undefined;
return (...args: T) => {
if (isWaiting) {
return;
}
const val = fn(...args);
wait = true;
callback(...args);
isWaiting = true;
setTimeout(() => {
wait = false;
isWaiting = false;
}, delay);
return val;
}
};
};

View File

@@ -1,6 +1,6 @@
import { createWasmWrapper } from "@nodes/utils"
import fs from "fs/promises"
import path from "path"
import { createWasmWrapper } from "@nodes/utils";
import fs from "fs/promises";
import path from "path";
export async function getWasm(id: `${string}/${string}/${string}`) {
const filePath = path.resolve(`../nodes/${id}/pkg/index_bg.wasm`);
@@ -8,17 +8,15 @@ export async function getWasm(id: `${string}/${string}/${string}`) {
try {
await fs.access(filePath);
} catch (e) {
return null
return null;
}
const file = await fs.readFile(filePath);
return new Uint8Array(file);
}
export async function getNodeWasm(id: `${string}/${string}/${string}`) {
const wasmBytes = await getWasm(id);
if (!wasmBytes) return null;
@@ -27,9 +25,7 @@ export async function getNodeWasm(id: `${string}/${string}/${string}`) {
return wrapper;
}
export async function getNode(id: `${string}/${string}/${string}`) {
const wrapper = await getNodeWasm(id);
const definition = wrapper?.get_definition?.();
@@ -37,18 +33,17 @@ export async function getNode(id: `${string}/${string}/${string}`) {
if (!definition) return null;
return definition;
}
export async function getCollectionNodes(userId: `${string}/${string}`) {
const nodes = await fs.readdir(path.resolve(`../nodes/${userId}`));
return nodes
.filter(n => n !== "pkg" && n !== ".template")
.map(n => {
.filter((n) => n !== "pkg" && n !== ".template")
.map((n) => {
return {
id: `${userId}/${n}`,
}
})
};
});
}
export async function getCollection(userId: `${string}/${string}`) {
@@ -56,36 +51,40 @@ export async function getCollection(userId: `${string}/${string}`) {
return {
id: userId,
nodes,
}
};
}
export async function getUserCollections(userId: string) {
const collections = await fs.readdir(path.resolve(`../nodes/${userId}`));
return Promise.all(collections.map(async n => {
const nodes = await getCollectionNodes(`${userId}/${n}`);
return {
id: `${userId}/${n}`,
nodes,
}
}));
return Promise.all(
collections.map(async (n) => {
const nodes = await getCollectionNodes(`${userId}/${n}`);
return {
id: `${userId}/${n}`,
nodes,
};
}),
);
}
export async function getUser(userId: string) {
const collections = await getUserCollections(userId);
return {
id: userId,
collections
}
collections,
};
}
export async function getUsers() {
const nodes = await fs.readdir(path.resolve("../nodes"));
const users = await Promise.all(nodes.map(async n => {
const collections = await getUserCollections(n);
return {
id: n,
collections
}
}))
const users = await Promise.all(
nodes.map(async (n) => {
const collections = await getUserCollections(n);
return {
id: n,
collections,
};
}),
);
return users;
}

View File

@@ -3,7 +3,6 @@
import type { NodeDefinition } from "@nodes/types";
export let node: NodeDefinition;
console.log(node);
let dragging = false;

View File

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

View File

@@ -27,10 +27,12 @@
function constructPath() {
max = max !== undefined ? max : Math.max(...points);
min = min !== undefined ? min : Math.min(...points);
const mi = min as number;
const ma = max as number;
return points
.map((point, i) => {
const x = (i / (points.length - 1)) * 100;
const y = 100 - ((point - min) / (max - min)) * 100;
const y = 100 - ((point - mi) / (ma - mi)) * 100;
return `${x},${y}`;
})
.join(" ");

View File

@@ -7,11 +7,15 @@
import type { PerspectiveCamera, Vector3Tuple } from "three";
import type { OrbitControls as OrbitControlsType } from "three/examples/jsm/controls/OrbitControls.js";
let camera: PerspectiveCamera;
let controls: OrbitControlsType;
let camera = $state<PerspectiveCamera>();
let controls = $state<OrbitControlsType>();
export let center: Vector3;
export let centerCamera: boolean = true;
type Props = {
center: Vector3;
centerCamera: boolean;
};
const { center, centerCamera }: Props = $props();
const cameraTransform = localStore<{
camera: Vector3Tuple;
@@ -22,7 +26,7 @@
});
function saveCameraState() {
if (!camera) return;
if (!camera || !controls) return;
let cPos = camera.position.toArray();
let tPos = controls.target.toArray();
// check if tPos is NaN or tPos is NaN
@@ -35,6 +39,7 @@
let isRunning = false;
const task = useTask(() => {
if (!controls) return;
let length = center.clone().sub(controls.target).length();
if (length < 0.01 || !centerCamera) {
isRunning = false;
@@ -47,22 +52,24 @@
});
task.stop();
$: if (
center &&
controls &&
centerCamera &&
(center.x !== controls.target.x ||
center.y !== controls.target.y ||
center.z !== controls.target.z) &&
!isRunning
) {
isRunning = true;
task.start();
}
$effect(() => {
if (
center &&
controls &&
centerCamera &&
(center.x !== controls.target.x ||
center.y !== controls.target.y ||
center.z !== controls.target.z) &&
!isRunning
) {
isRunning = true;
task.start();
}
});
onMount(() => {
controls.target.fromArray($cameraTransform.target);
controls.update();
controls?.target.fromArray($cameraTransform.target);
controls?.update();
});
</script>

View File

@@ -1,6 +1,11 @@
<script lang="ts">
import { T, useTask, useThrelte } from "@threlte/core";
import { MeshLineGeometry, MeshLineMaterial, Text } from "@threlte/extras";
import {
Grid,
MeshLineGeometry,
MeshLineMaterial,
Text,
} from "@threlte/extras";
import {
type Group,
type BufferGeometry,
@@ -9,7 +14,6 @@
Box3,
Mesh,
MeshBasicMaterial,
Color,
} from "three";
import { appSettings } from "../settings/app-settings.svelte";
import Camera from "./Camera.svelte";
@@ -42,11 +46,9 @@
export const invalidate = function () {
if (scene) {
geometries = scene.children
.filter(
(child) => "geometry" in child && child.isObject3D && child.geometry,
)
.filter((child) => "geometry" in child && child.isObject3D)
.map((child) => {
return child.geometry;
return (child as Mesh).geometry;
});
}
@@ -72,7 +74,7 @@
}
$effect(() => {
const wireframe = appSettings.debug.wireframe;
const wireframe = appSettings.value.debug.wireframe;
scene.traverse(function (child) {
if (isMesh(child) && isMatCapMaterial(child.material)) {
child.material.wireframe = wireframe;
@@ -92,18 +94,24 @@
<Camera {center} {centerCamera} />
{#if appSettings.showGrid}
<T.GridHelper
args={[20, 20]}
colorGrid={colors["outline"]}
colorCenterLine={new Color("red")}
{#if appSettings.value.showGrid}
<Grid
cellColor={colors["outline"]}
cellThickness={0.7}
infiniteGrid
sectionThickness={0.7}
sectionDistance={2}
sectionColor={colors["outline"]}
fadeDistance={50}
fadeStrength={10}
fadeOrigin={new Vector3(0, 0, 0)}
/>
{/if}
<T.Group>
{#if geometries}
{#each geometries as geo}
{#if appSettings.debug.showIndices}
{#if appSettings.value.debug.showIndices}
{#each geo.attributes.position.array as _, i}
{#if i % 3 === 0}
<Text fontSize={0.25} position={getPosition(geo, i)} />
@@ -111,7 +119,7 @@
{/each}
{/if}
{#if appSettings.debug.showVertices}
{#if appSettings.value.debug.showVertices}
<T.Points visible={true}>
<T is={geo} />
<T.PointsMaterial size={0.25} />
@@ -123,7 +131,7 @@
<T.Group bind:ref={scene}></T.Group>
</T.Group>
{#if appSettings.debug.showStemLines && lines}
{#if appSettings.value.debug.showStemLines && lines}
{#each lines as line}
<T.Mesh>
<MeshLineGeometry points={line} />

View File

@@ -68,7 +68,7 @@
const inputs = splitNestedArray(result);
perf.endPoint();
if (appSettings.debug.showStemLines) {
if (appSettings.value.debug.showStemLines) {
perf.addPoint("create-lines");
lines = inputs
.map((input) => {
@@ -91,7 +91,7 @@
};
</script>
{#if appSettings.debug.showPerformancePanel}
{#if appSettings.value.debug.showPerformancePanel}
<SmallPerformanceViewer {fps} store={perf} />
{/if}

View File

@@ -1,20 +1,27 @@
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) {
let ints = new Uint8Array(arr);
function fastArrayHash(arr: Int32Array) {
const sampleDistance = Math.max(Math.floor(arr.length / 100), 1);
const sampleCount = Math.floor(arr.length / sampleDistance);
const sampleDistance = Math.max(Math.floor(ints.length / 100), 1);
const sampleCount = Math.floor(ints.length / sampleDistance);
let hash = new Uint8Array(sampleCount);
let hash = new Int32Array(sampleCount);
for (let i = 0; i < sampleCount; i++) {
const index = i * sampleDistance;
hash[i] = ints[index];
hash[i] = arr[index];
}
return fastHashArrayBuffer(hash.buffer);
return fastHashArrayBuffer(hash);
}
export function createGeometryPool(parentScene: Group, material: Material) {
@@ -26,8 +33,10 @@ export function createGeometryPool(parentScene: Group, material: Material) {
let totalVertices = 0;
let totalFaces = 0;
function updateSingleGeometry(data: Int32Array, existingMesh: Mesh | null = null) {
function updateSingleGeometry(
data: Int32Array,
existingMesh: Mesh | null = null,
) {
let hash = fastArrayHash(data);
let geometry = existingMesh ? existingMesh.geometry : new BufferGeometry();
@@ -50,11 +59,7 @@ export function createGeometryPool(parentScene: Group, material: Material) {
index = indicesEnd;
// Vertices
const vertices = new Float32Array(
data.buffer,
index * 4,
vertexCount * 3,
);
const vertices = new Float32Array(data.buffer, index * 4, vertexCount * 3);
index = index + vertexCount * 3;
let posAttribute = geometry.getAttribute(
@@ -71,11 +76,7 @@ export function createGeometryPool(parentScene: Group, material: Material) {
);
}
const normals = new Float32Array(
data.buffer,
index * 4,
vertexCount * 3,
);
const normals = new Float32Array(data.buffer, index * 4, vertexCount * 3);
index = index + vertexCount * 3;
if (
@@ -109,11 +110,8 @@ export function createGeometryPool(parentScene: Group, material: Material) {
}
}
return {
update(
newData: Int32Array[],
) {
update(newData: Int32Array[]) {
totalVertices = 0;
totalFaces = 0;
for (let i = 0; i < Math.max(newData.length, meshes.length); i++) {
@@ -127,11 +125,14 @@ export function createGeometryPool(parentScene: Group, material: Material) {
}
}
return { totalVertices, totalFaces };
}
}
},
};
}
export function createInstancedGeometryPool(parentScene: Group, material: Material) {
export function createInstancedGeometryPool(
parentScene: Group,
material: Material,
) {
const scene = new Group();
parentScene.add(scene);
@@ -139,19 +140,25 @@ export function createInstancedGeometryPool(parentScene: Group, material: Materi
let totalVertices = 0;
let totalFaces = 0;
function updateSingleInstance(data: Int32Array, existingInstance: InstancedMesh | null = null) {
function updateSingleInstance(
data: Int32Array,
existingInstance: InstancedMesh | null = null,
) {
let hash = fastArrayHash(data);
let geometry = existingInstance ? existingInstance.geometry : new BufferGeometry();
let geometry = existingInstance
? existingInstance.geometry
: new BufferGeometry();
// Extract data from the encoded array
let index = 0;
const geometryType = data[index++];
// const geometryType = data[index++];
index++;
const vertexCount = data[index++];
const faceCount = data[index++];
const instanceCount = data[index++];
const stemDepth = data[index++];
// const stemDepth = data[index++];
index++;
totalVertices += vertexCount * instanceCount;
totalFaces += faceCount * instanceCount;
@@ -168,11 +175,7 @@ export function createInstancedGeometryPool(parentScene: Group, material: Materi
}
// Vertices
const vertices = new Float32Array(
data.buffer,
index * 4,
vertexCount * 3,
);
const vertices = new Float32Array(data.buffer, index * 4, vertexCount * 3);
index = index + vertexCount * 3;
let posAttribute = geometry.getAttribute(
"position",
@@ -187,11 +190,7 @@ export function createInstancedGeometryPool(parentScene: Group, material: Materi
);
}
const normals = new Float32Array(
data.buffer,
index * 4,
vertexCount * 3,
);
const normals = new Float32Array(data.buffer, index * 4, vertexCount * 3);
index = index + vertexCount * 3;
const normalsAttribute = geometry.getAttribute(
"normal",
@@ -203,20 +202,23 @@ export function createInstancedGeometryPool(parentScene: Group, material: Materi
geometry.setAttribute("normal", new Float32BufferAttribute(normals, 3));
}
if (existingInstance && instanceCount > existingInstance.geometry.userData.count) {
console.log("recreating instance")
if (
existingInstance &&
instanceCount > existingInstance.geometry.userData.count
) {
console.log("recreating instance");
scene.remove(existingInstance);
instances.splice(instances.indexOf(existingInstance), 1);
existingInstance = new InstancedMesh(geometry, material, instanceCount);
scene.add(existingInstance)
instances.push(existingInstance)
scene.add(existingInstance);
instances.push(existingInstance);
} else if (!existingInstance) {
console.log("creating instance")
console.log("creating instance");
existingInstance = new InstancedMesh(geometry, material, instanceCount);
scene.add(existingInstance)
instances.push(existingInstance)
scene.add(existingInstance);
instances.push(existingInstance);
} else {
console.log("updating instance")
console.log("updating instance");
existingInstance.count = instanceCount;
}
@@ -225,28 +227,31 @@ export function createInstancedGeometryPool(parentScene: Group, material: Materi
const matrices = new Float32Array(
data.buffer,
index * 4,
instanceCount * 16);
instanceCount * 16,
);
for (let i = 0; i < instanceCount; i++) {
const matrix = new Matrix4().fromArray(matrices.subarray(i * 16, i * 16 + 16));
const matrix = new Matrix4().fromArray(
matrices.subarray(i * 16, i * 16 + 16),
);
existingInstance.setMatrixAt(i, matrix);
}
geometry.userData = {
vertexCount,
faceCount,
count: Math.max(instanceCount, existingInstance.geometry.userData.count || 0),
count: Math.max(
instanceCount,
existingInstance.geometry.userData.count || 0,
),
hash,
};
existingInstance.instanceMatrix.needsUpdate = true;
}
return {
update(
newData: Int32Array[],
) {
update(newData: Int32Array[]) {
totalVertices = 0;
totalFaces = 0;
for (let i = 0; i < Math.max(newData.length, instances.length); i++) {
@@ -260,6 +265,6 @@ export function createInstancedGeometryPool(parentScene: Group, material: Materi
}
}
return { totalVertices, totalFaces };
}
}
},
};
}

View File

@@ -1,12 +1,26 @@
import type { Graph, NodeDefinition, NodeInput, NodeRegistry, RuntimeExecutor, SyncCache } from "@nodes/types";
import { concatEncodedArrays, createLogger, encodeFloat, fastHashArrayBuffer, type PerformanceStore } from "@nodes/utils";
import type {
Graph,
Node,
NodeDefinition,
NodeInput,
NodeRegistry,
RuntimeExecutor,
SyncCache,
} from "@nodes/types";
import {
concatEncodedArrays,
createLogger,
encodeFloat,
fastHashArrayBuffer,
type PerformanceStore,
} from "@nodes/utils";
const log = createLogger("runtime-executor");
log.mute()
// log.mute();
function getValue(input: NodeInput, value?: unknown) {
if (value === undefined && "value" in input) {
value = input.value
value = input.value;
}
if (input.type === "float") {
@@ -15,7 +29,13 @@ function getValue(input: NodeInput, value?: unknown) {
if (Array.isArray(value)) {
if (input.type === "vec3") {
return [0, value.length + 1, ...value.map(v => encodeFloat(v)), 1, 1] as number[];
return [
0,
value.length + 1,
...value.map((v) => encodeFloat(v)),
1,
1,
] as number[];
}
return [0, value.length + 1, ...value, 1, 1] as number[];
}
@@ -36,22 +56,23 @@ function getValue(input: NodeInput, value?: unknown) {
}
export class MemoryRuntimeExecutor implements RuntimeExecutor {
private definitionMap: Map<string, NodeDefinition> = new Map();
private randomSeed = Math.floor(Math.random() * 100000000);
perf?: PerformanceStore;
constructor(private registry: NodeRegistry, private cache?: SyncCache<Int32Array>) { }
constructor(
private registry: NodeRegistry,
private cache?: SyncCache<Int32Array>,
) {}
private async getNodeDefinitions(graph: Graph) {
if (this.registry.status !== "ready") {
throw new Error("Node registry is not ready");
}
await this.registry.load(graph.nodes.map(node => node.type));
await this.registry.load(graph.nodes.map((node) => node.type));
const typeMap = new Map<string, NodeDefinition>();
for (const node of graph.nodes) {
@@ -66,18 +87,22 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
}
private async addMetaData(graph: Graph) {
// First, lets check if all nodes have a definition
this.definitionMap = await this.getNodeDefinitions(graph);
const outputNode = graph.nodes.find(node => node.type.endsWith("/output"));
const outputNode = graph.nodes.find((node) =>
node.type.endsWith("/output"),
) as Node;
if (!outputNode) {
throw new Error("No output node found");
}
outputNode.tmp = outputNode.tmp || {};
outputNode.tmp.depth = 0;
const nodeMap = new Map(graph.nodes.map(node => [node.id, node]));
const nodeMap = new Map<number, Node>(
graph.nodes.map((node) => [node.id, node]),
);
// loop through all edges and assign the parent and child nodes to each node
for (const edge of graph.edges) {
@@ -96,7 +121,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
}
}
const nodes = []
const nodes = [];
// loop through all the nodes and assign each nodes its depth
const stack = [outputNode];
@@ -125,7 +150,6 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
}
async execute(graph: Graph, settings: Record<string, unknown>) {
this.perf?.addPoint("runtime");
let a = performance.now();
@@ -137,71 +161,74 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
this.perf?.addPoint("collect-metadata", b - a);
/*
* Here we sort the nodes into buckets, which we then execute one by one
* +-b2-+-b1-+---b0---+
* | | | |
* | n3 | n2 | Output |
* | n6 | n4 | Level |
* | | n5 | |
* | | | |
* +----+----+--------+
*/
* Here we sort the nodes into buckets, which we then execute one by one
* +-b2-+-b1-+---b0---+
* | | | |
* | n3 | n2 | Output |
* | n6 | n4 | Level |
* | | n5 | |
* | | | |
* +----+----+--------+
*/
// we execute the nodes from the bottom up
const sortedNodes = nodes.sort((a, b) => (b.tmp?.depth || 0) - (a.tmp?.depth || 0));
const sortedNodes = nodes.sort(
(a, b) => (b.tmp?.depth || 0) - (a.tmp?.depth || 0),
);
// here we store the intermediate results of the nodes
const results: Record<string, Int32Array> = {};
for (const node of sortedNodes) {
const node_type = this.definitionMap.get(node.type)!;
if (!node_type || !node.tmp || !node_type.execute) {
log.warn(`Node ${node.id} has no definition`);
continue;
};
}
a = performance.now();
// Collect the inputs for the node
const inputs = Object.entries(node_type.inputs || {}).map(([key, input]) => {
if (input.type === "seed") {
if (settings["randomSeed"] === true) {
return Math.floor(Math.random() * 100000000)
} else {
return this.randomSeed
const inputs = Object.entries(node_type.inputs || {}).map(
([key, input]) => {
if (input.type === "seed") {
if (settings["randomSeed"] === true) {
return Math.floor(Math.random() * 100000000);
} else {
return this.randomSeed;
}
}
}
// If the input is linked to a setting, we use that value
if (input.setting) {
return getValue(input, settings[input.setting]);
}
// check if the input is connected to another node
const inputNode = node.tmp?.inputNodes?.[key];
if (inputNode) {
if (results[inputNode.id] === undefined) {
throw new Error(`Node ${node.type} is missing input from node ${inputNode.type}`);
// If the input is linked to a setting, we use that value
if (input.setting) {
return getValue(input, settings[input.setting]);
}
return results[inputNode.id];
}
// If the value is stored in the node itself, we use that value
if (node.props?.[key] !== undefined) {
return getValue(input, node.props[key]);
}
// check if the input is connected to another node
const inputNode = node.tmp?.inputNodes?.[key];
if (inputNode) {
if (results[inputNode.id] === undefined) {
throw new Error(
`Node ${node.type} is missing input from node ${inputNode.type}`,
);
}
return results[inputNode.id];
}
return getValue(input);
});
// If the value is stored in the node itself, we use that value
if (node.props?.[key] !== undefined) {
return getValue(input, node.props[key]);
}
return getValue(input);
},
);
b = performance.now();
this.perf?.addPoint("collected-inputs", b - a);
try {
a = performance.now();
const encoded_inputs = concatEncodedArrays(inputs);
b = performance.now();
@@ -234,13 +261,10 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
this.perf?.addPoint("node/" + node_type.id, b - a);
log.log("Result:", results[node.id]);
log.groupEnd();
} catch (e) {
log.groupEnd();
log.error(`Error executing node ${node_type.id || node.id}`, e);
}
}
// return the result of the parent of the output node
@@ -253,11 +277,9 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
this.perf?.endPoint("runtime");
return res as unknown as Int32Array;
}
getPerformanceData() {
return this.perf?.get();
}
}

View File

@@ -6,14 +6,16 @@ import { MemoryRuntimeCache } from "./runtime-executor-cache";
const cache = new MemoryRuntimeCache();
const indexDbCache = new IndexDBCache("node-registry");
const nodeRegistry = new RemoteNodeRegistry("");
nodeRegistry.cache = indexDbCache;
const nodeRegistry = new RemoteNodeRegistry("", indexDbCache);
const executor = new MemoryRuntimeExecutor(nodeRegistry, cache);
const performanceStore = createPerformanceStore();
executor.perf = performanceStore;
export async function executeGraph(graph: Graph, settings: Record<string, unknown>): Promise<Int32Array> {
export async function executeGraph(
graph: Graph,
settings: Record<string, unknown>,
): Promise<Int32Array> {
await nodeRegistry.load(graph.nodes.map((n) => n.type));
performanceStore.startRun();
let res = await executor.execute(graph, settings);

View File

@@ -0,0 +1,205 @@
<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;
type SettingsNode = InputType | SettingsGroup;
interface SettingsGroup {
title?: string;
[key: string]: any;
}
type SettingsType = Record<string, SettingsNode>;
type SettingsValue = Record<
string,
Record<string, unknown> | string | number | boolean | number[]
>;
type Props = {
id: string;
key?: string;
value: SettingsValue;
type: SettingsType;
depth?: number;
};
// Local persistent state for <details> sections
const openSections = localState<Record<string, boolean>>("open-details", {});
let { id, key = "", value = $bindable(), type, depth = 0 }: Props = $props();
function isNodeInput(v: SettingsNode | undefined): v is InputType {
return !!v && typeof v === "object" && "type" in v;
}
function getDefaultValue(): unknown {
if (key === "" || key === "title") return;
const node = type[key];
if (!isNodeInput(node)) return;
const anyNode = node as any;
// select input: use index into options
if (Array.isArray(anyNode.options)) {
if (value?.[key] !== undefined) {
return anyNode.options.indexOf(value[key]);
}
return 0;
}
if (value?.[key] !== undefined) return value[key];
if ("value" in node && anyNode.value !== undefined) {
return anyNode.value;
}
switch (node.type) {
case "boolean":
return 0;
case "float":
return 0.5;
case "integer":
case "select":
return 0;
default:
return 0;
}
}
let internalValue = $state(getDefaultValue());
let open = $state(openSections.value[id]);
// Persist <details> open/closed state for groups
if (depth > 0 && !isNodeInput(type[key!])) {
$effect(() => {
if (open !== undefined) {
openSections.value[id] = open;
}
});
}
// Sync internalValue back into `value`
$effect(() => {
if (key === "" || internalValue === undefined) return;
const node = type[key];
if (
isNodeInput(node) &&
Array.isArray((node as any).options) &&
typeof internalValue === "number"
) {
value[key] = (node as any).options[internalValue] as any;
} else {
value[key] = internalValue as any;
}
});
</script>
{#if key && isNodeInput(type?.[key])}
<!-- Leaf input -->
<div class="input input-{type[key].type}" class:first-level={depth === 1}>
{#if type[key].type === "button"}
<button onclick={() => type[key].callback()}>
{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}
<!-- Root: iterate over top-level keys -->
{#each Object.keys(type ?? {}).filter((k) => k !== "title") as childKey}
<NestedSettings
id={`${id}.${childKey}`}
key={childKey}
bind:value
{type}
depth={depth + 1}
/>
{/each}
<hr />
{:else if key && type?.[key]}
<!-- Group -->
{#if depth > 0}
<hr />
{/if}
<details bind:open>
<summary><p>{(type[key] as SettingsGroup).title || key}</p></summary>
<div class="content">
{#each Object.keys(type[key] as SettingsGroup).filter((k) => k !== "title") as childKey}
<NestedSettings
id={`${id}.${childKey}`}
key={childKey}
bind:value={value[key] as SettingsValue}
type={type[key] as unknown 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

@@ -1,7 +1,16 @@
import { localState } from "$lib/helpers/localState.svelte";
import type { NodeInput } from "@nodes/types";
import type { SettingsType } from ".";
const themes = ["dark", "light", "catppuccin", "solarized", "high-contrast", "nord", "dracula"];
const themes = [
"dark",
"light",
"catppuccin",
"solarized",
"high-contrast",
"nord",
"dracula",
];
export const AppSettingTypes = {
theme: {
@@ -18,28 +27,33 @@ export const AppSettingTypes = {
centerCamera: {
type: "boolean",
label: "Center Camera",
value: true
value: true,
},
nodeInterface: {
title: "Node Interface",
showNodeGrid: {
type: "boolean",
label: "Show Grid",
value: true
value: true,
},
snapToGrid: {
type: "boolean",
label: "Snap to Grid",
value: true
value: true,
},
showHelp: {
type: "boolean",
label: "Show Help",
value: false
}
value: false,
},
},
debug: {
title: "Debug",
amount: {
type: "number",
label: "Amount",
value: 4,
},
wireframe: {
type: "boolean",
label: "Wireframe",
@@ -81,52 +95,61 @@ export const AppSettingTypes = {
type: "integer",
min: 2,
max: 15,
value: 4
value: 4,
},
loadGrid: {
type: "button",
label: "Load Grid"
label: "Load Grid",
},
loadTree: {
type: "button",
label: "Load Tree"
label: "Load Tree",
},
lottaFaces: {
type: "button",
label: "Load 'lots of faces'"
label: "Load 'lots of faces'",
},
lottaNodes: {
type: "button",
label: "Load 'lots of nodes'"
label: "Load 'lots of nodes'",
},
lottaNodesAndFaces: {
type: "button",
label: "Load 'lots of nodes and faces'"
}
label: "Load 'lots of nodes and faces'",
},
},
}
} as const
},
} as const satisfies SettingsType;
type IsInputDefinition<T> = T extends NodeInput ? T : never;
type HasTitle = { title: string };
type Widen<T> = T extends boolean
? boolean
: T extends number
? number
: T extends string
? string
: T;
type ExtractSettingsValues<T> = {
[K in keyof T]: T[K] extends HasTitle
? ExtractSettingsValues<Omit<T[K], 'title'>>
: T[K] extends IsInputDefinition<T[K]>
? T[K] extends { value: any }
? T[K]['value']
: never
: T[K] extends Record<string, any>
? ExtractSettingsValues<T[K]>
: never;
-readonly [K in keyof T]: T[K] extends HasTitle
? ExtractSettingsValues<Omit<T[K], "title">>
: T[K] extends IsInputDefinition<T[K]>
? T[K] extends { value: infer V }
? Widen<V>
: never
: T[K] extends Record<string, any>
? ExtractSettingsValues<T[K]>
: never;
};
function settingsToStore<T>(settings: T): ExtractSettingsValues<T> {
export function settingsToStore<T>(settings: T): ExtractSettingsValues<T> {
const result = {} as any;
for (const key in settings) {
const value = settings[key];
if (value && typeof value === 'object') {
if ('value' in value) {
if (value && typeof value === "object") {
if ("value" in value) {
result[key] = value.value;
} else {
result[key] = settingsToStore(value);
@@ -136,11 +159,14 @@ function settingsToStore<T>(settings: T): ExtractSettingsValues<T> {
return result;
}
export const appSettings = localState("app-settings", settingsToStore(AppSettingTypes));
export let appSettings = localState(
"app-settings",
settingsToStore(AppSettingTypes),
);
$effect.root(() => {
$effect(() => {
const theme = appSettings.theme;
const theme = appSettings.value.theme;
const classes = document.documentElement.classList;
const newClassName = `theme-${theme}`;
if (classes) {

View File

@@ -0,0 +1,27 @@
import type { NodeInput } from "@nodes/types";
type Button = { type: "button"; label?: string };
export type SettingsStore = {
[key: string]: SettingsStore | string | number | boolean;
};
type InputType = NodeInput | Button;
type SettingsNode = InputType | SettingsGroup;
export interface SettingsGroup {
title?: string;
[key: string]: SettingsNode | string | number | undefined;
}
export type SettingsType = Record<string, SettingsNode>;
export type SettingsValue = Record<
string,
Record<string, unknown> | string | number | boolean | number[]
>;
export function isNodeInput(v: SettingsNode | undefined): v is InputType {
return !!v && "type" in v;
}

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,157 +0,0 @@
<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>;
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;
}
let internalValue = $state(Array.isArray(type?.[key]?.options) ? type[key]?.options?.indexOf(value?.[key]) : value?.[key]);
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={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}
type={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}
{/if}
<style>
summary {
cursor: pointer;
user-select: none;
margin-bottom: 1em;
}
summary::marker { }
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

@@ -1,7 +1,7 @@
<script lang="ts">
import type { Node, NodeInput } from "@nodes/types";
import NestedSettings from "./NestedSettings.svelte";
import type { GraphManager } from "$lib/graph-interface/graph-manager";
import NestedSettings from "$lib/settings/NestedSettings.svelte";
import type { GraphManager } from "$lib/graph-interface/graph-manager.svelte";
type Props = {
manager: GraphManager;
@@ -69,24 +69,14 @@
}
$effect(() => {
if (store && store) {
if (store) {
updateNode();
}
});
</script>
{#if node}
{#key node.id}
{#if nodeDefinition && store && Object.keys(nodeDefinition).length > 0}
<NestedSettings
id="activeNodeSettings"
bind:value={store}
type={nodeDefinition}
/>
{:else}
<p class="mx-4">Active Node has no Settings</p>
{/if}
{/key}
{:else}
<p class="mx-4">No active node</p>
{/if}
<NestedSettings
id="activeNodeSettings"
bind:value={store}
type={nodeDefinition}
/>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import type { Node } from "@nodes/types";
import type { GraphManager } from "$lib/graph-interface/graph-manager";
import type { GraphManager } from "$lib/graph-interface/graph-manager.svelte";
import ActiveNodeSelected from "./ActiveNodeSelected.svelte";
type Props = {

View File

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

8
app/src/lib/types.ts Normal file
View File

@@ -0,0 +1,8 @@
import type {
Graph,
Node as NodeType,
NodeDefinition,
NodeInput,
RuntimeExecutor,
} from "@nodes/types";
export type { Graph, NodeDefinition, NodeInput };

View File

@@ -4,21 +4,20 @@
import * as templates from "$lib/graph-templates";
import type { Graph, Node } from "@nodes/types";
import Viewer from "$lib/result-viewer/Viewer.svelte";
import Settings from "$lib/settings/Settings.svelte";
import {
appSettings,
AppSettingTypes,
} from "$lib/settings/app-settings.svelte";
import Keymap from "$lib/settings/panels/Keymap.svelte";
import Keymap from "$lib/sidebar/panels/Keymap.svelte";
import Sidebar from "$lib/sidebar/Sidebar.svelte";
import { createKeyMap } from "$lib/helpers/createKeyMap";
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 Panel from "$lib/settings/Panel.svelte";
import GraphSettings from "$lib/settings/panels/GraphSettings.svelte";
import NestedSettings from "$lib/settings/panels/NestedSettings.svelte";
import Panel from "$lib/sidebar/Panel.svelte";
import NestedSettings from "$lib/settings/NestedSettings.svelte";
import type { Group } from "three";
import ExportSettings from "$lib/settings/panels/ExportSettings.svelte";
import ExportSettings from "$lib/sidebar/panels/ExportSettings.svelte";
import {
MemoryRuntimeCache,
WorkerRuntimeExecutor,
@@ -26,35 +25,34 @@
} from "$lib/runtime";
import { IndexDBCache, RemoteNodeRegistry } from "@nodes/registry";
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 { onMount } from "svelte";
let performanceStore = createPerformanceStore();
const registryCache = new IndexDBCache("node-registry");
const nodeRegistry = new RemoteNodeRegistry("");
nodeRegistry.cache = registryCache;
const nodeRegistry = new RemoteNodeRegistry("", registryCache);
const workerRuntime = new WorkerRuntimeExecutor();
const runtimeCache = new MemoryRuntimeCache();
const memoryRuntime = new MemoryRuntimeExecutor(nodeRegistry, runtimeCache);
memoryRuntime.perf = performanceStore;
const runtime = $derived(
appSettings.debug.useWorker ? workerRuntime : memoryRuntime,
appSettings.value.debug.useWorker ? workerRuntime : memoryRuntime,
);
let activeNode = $state<Node | undefined>(undefined);
let scene = $state<Group>(null!);
let graph = localStorage.getItem("graph")
? JSON.parse(localStorage.getItem("graph")!)
: templates.defaultPlant;
let graph = $state(
localStorage.getItem("graph")
? JSON.parse(localStorage.getItem("graph")!)
: templates.defaultPlant,
);
let graphInterface = $state<ReturnType<typeof GraphInterface>>(null!);
let viewerComponent = $state<ReturnType<typeof Viewer>>();
const manager = $derived(graphInterface?.manager);
const managerStatus = $derived(manager?.status);
async function randomGenerate() {
if (!manager) return;
@@ -71,17 +69,35 @@
},
]);
let graphSettings = $state<Record<string, any>>({});
let graphSettingTypes = $state({});
$effect(() => {
if (graphSettings) {
manager?.setSettings($state.snapshot(graphSettings));
}
});
type BooleanSchema = {
[key: string]: {
type: "boolean";
value: false;
};
};
let graphSettingTypes = $state<BooleanSchema>({
randomSeed: { type: "boolean", value: false },
});
let runIndex = 0;
const handleUpdate = debounceAsyncFunction(
async (g: Graph, s: Record<string, any> = graphSettings) => {
runIndex++;
performanceStore.startRun();
try {
let a = performance.now();
const graphResult = await runtime.execute(g, $state.snapshot(s));
const graphResult = await runtime.execute(
$state.snapshot(g),
$state.snapshot(s),
);
let b = performance.now();
if (appSettings.debug.useWorker) {
if (appSettings.value.debug.useWorker) {
let perfData = await runtime.getPerformanceData();
let lastRun = perfData?.at(-1);
if (lastRun?.total) {
@@ -94,7 +110,6 @@
);
}
}
viewerComponent?.update(graphResult);
} catch (error) {
console.log("errors", error);
@@ -104,39 +119,40 @@
},
);
// $ if (AppSettings) {
// //@ts-ignore
// AppSettingTypes.debug.stressTest.loadGrid.callback = () => {
// graph = templates.grid($AppSettings.amount, $AppSettings.amount);
// };
// //@ts-ignore
// AppSettingTypes.debug.stressTest.loadTree.callback = () => {
// graph = templates.tree($AppSettings.amount);
// };
// //@ts-ignore
// AppSettingTypes.debug.stressTest.lottaFaces.callback = () => {
// graph = templates.lottaFaces;
// };
// //@ts-ignore
// AppSettingTypes.debug.stressTest.lottaNodes.callback = () => {
// graph = templates.lottaNodes;
// };
// //@ts-ignore
// AppSettingTypes.debug.stressTest.lottaNodesAndFaces.callback = () => {
// graph = templates.lottaNodesAndFaces;
// };
// }
$effect(() => {
//@ts-ignore
AppSettingTypes.debug.stressTest.loadGrid.callback = () => {
graph = templates.grid(
appSettings.value.debug.amount.value,
appSettings.value.debug.amount.value,
);
};
//@ts-ignore
AppSettingTypes.debug.stressTest.loadTree.callback = () => {
graph = templates.tree(appSettings.value.debug.amount.value);
};
//@ts-ignore
AppSettingTypes.debug.stressTest.lottaFaces.callback = () => {
graph = templates.lottaFaces;
};
//@ts-ignore
AppSettingTypes.debug.stressTest.lottaNodes.callback = () => {
graph = templates.lottaNodes;
};
//@ts-ignore
AppSettingTypes.debug.stressTest.lottaNodesAndFaces.callback = () => {
graph = templates.lottaNodesAndFaces;
};
});
function handleSave(graph: Graph) {
localStorage.setItem("graph", JSON.stringify(graph));
}
onMount(() => {
handleUpdate(graph);
});
</script>
<svelte:document on:keydown={applicationKeymap.handleKeyboardEvent} />
<div class="wrapper manager-{$managerStatus}">
<svelte:document onkeydown={applicationKeymap.handleKeyboardEvent} />
<div class="wrapper manager-{manager?.status}">
<header></header>
<Grid.Row>
<Grid.Cell>
@@ -144,102 +160,101 @@
bind:scene
bind:this={viewerComponent}
perf={performanceStore}
centerCamera={appSettings.centerCamera}
centerCamera={appSettings.value.centerCamera}
/>
</Grid.Cell>
<Grid.Cell>
{#key graph}
<GraphInterface
bind:this={graphInterface}
{graph}
registry={nodeRegistry}
showGrid={appSettings.nodeInterface.showNodeGrid}
snapToGrid={appSettings.nodeInterface.snapToGrid}
bind:activeNode
bind:showHelp={appSettings.nodeInterface.showHelp}
bind:settings={graphSettings}
bind:settingTypes={graphSettingTypes}
onresult={(result) => handleUpdate(result)}
onsave={(graph) => handleSave(graph)}
/>
<Settings>
<Panel id="general" title="General" icon="i-tabler-settings">
<NestedSettings
id="general"
value={appSettings}
type={AppSettingTypes}
/>
</Panel>
<Panel
id="shortcuts"
title="Keyboard Shortcuts"
icon="i-tabler-keyboard"
>
<Keymap
keymaps={[
{ keymap: applicationKeymap, title: "Application" },
{ keymap: graphInterface.keymap, title: "Node-Editor" },
]}
/>
</Panel>
<Panel id="exports" title="Exporter" icon="i-tabler-package-export">
<ExportSettings {scene} />
</Panel>
<Panel
id="node-store"
classes="text-green-400"
title="Node Store"
icon="i-tabler-database"
>
<NodeStore registry={nodeRegistry} />
</Panel>
<Panel
id="performance"
title="Performance"
classes="text-red-400"
hidden={!appSettings.debug.showPerformancePanel}
icon="i-tabler-brand-speedtest"
>
{#if $performanceStore}
<PerformanceViewer data={$performanceStore} />
{/if}
</Panel>
<Panel
id="benchmark"
title="Benchmark"
classes="text-red-400"
hidden={!appSettings.debug.showBenchmarkPanel}
icon="i-tabler-graph"
>
<BenchmarkPanel run={randomGenerate} />
</Panel>
<Panel
<GraphInterface
{graph}
bind:this={graphInterface}
registry={nodeRegistry}
showGrid={appSettings.value.nodeInterface.showNodeGrid}
snapToGrid={appSettings.value.nodeInterface.snapToGrid}
bind:activeNode
bind:showHelp={appSettings.value.nodeInterface.showHelp}
bind:settings={graphSettings}
bind:settingTypes={graphSettingTypes}
onresult={(result) => handleUpdate(result)}
onsave={(graph) => handleSave(graph)}
/>
<Sidebar>
<Panel id="general" title="General" icon="i-tabler-settings">
<NestedSettings
id="general"
bind:value={appSettings.value}
type={AppSettingTypes}
/>
</Panel>
<Panel
id="shortcuts"
title="Keyboard Shortcuts"
icon="i-tabler-keyboard"
>
<Keymap
keymaps={[
{ keymap: applicationKeymap, title: "Application" },
{ keymap: graphInterface.keymap, title: "Node-Editor" },
]}
/>
</Panel>
<Panel id="exports" title="Exporter" icon="i-tabler-package-export">
<ExportSettings {scene} />
</Panel>
<Panel
id="node-store"
classes="text-green-400"
title="Node Store"
icon="i-tabler-database"
>
<NodeStore registry={nodeRegistry} />
</Panel>
<Panel
id="performance"
title="Performance"
classes="text-red-400"
hidden={!appSettings.value.debug.showPerformancePanel}
icon="i-tabler-brand-speedtest"
>
{#if $performanceStore}
<PerformanceViewer data={$performanceStore} />
{/if}
</Panel>
<Panel
id="benchmark"
title="Benchmark"
classes="text-red-400"
hidden={!appSettings.value.debug.showBenchmarkPanel}
icon="i-tabler-graph"
>
<BenchmarkPanel run={randomGenerate} />
</Panel>
<Panel
id="graph-settings"
title="Graph Settings"
classes="text-blue-400"
icon="i-custom-graph"
>
<NestedSettings
id="graph-settings"
title="Graph Settings"
classes="text-blue-400"
icon="i-custom-graph"
>
{#if Object.keys(graphSettingTypes).length > 0}
<GraphSettings type={graphSettingTypes} store={graphSettings} />
{/if}
</Panel>
<Panel
id="active-node"
title="Node Settings"
classes="text-blue-400"
icon="i-tabler-adjustments"
>
<ActiveNodeSettings {manager} node={activeNode} />
</Panel>
</Settings>
{/key}
type={graphSettingTypes}
bind:value={graphSettings}
/>
</Panel>
<Panel
id="active-node"
title="Node Settings"
classes="text-blue-400"
icon="i-tabler-adjustments"
>
<ActiveNodeSettings {manager} node={activeNode} />
</Panel>
</Sidebar>
</Grid.Cell>
</Grid.Row>
</div>
<style>
header {
/* border-bottom: solid thin var(--outline); */
background-color: var(--layer-1);
}

View File

@@ -1,27 +1,36 @@
import type { RequestHandler } from "./$types";
import type { EntryGenerator, RequestHandler } from "./$types";
import * as registry from "$lib/node-registry";
import type { EntryGenerator } from "../$types";
export const prerender = true;
export const entries: EntryGenerator = async () => {
const users = await registry.getUsers();
return users.map(user => {
return user.collections.map(collection => {
return collection.nodes.map(node => {
return { user: user.id, collection: collection.id.split("/")[1], node: node.id.split("/")[2] }
return users
.map((user) => {
return user.collections.map((collection) => {
return collection.nodes.map((node) => {
return {
user: user.id,
collection: collection.id.split("/")[1],
node: node.id.split("/")[2],
};
});
});
})
}).flat(2);
}
.flat(2);
};
export const GET: RequestHandler = async function GET({ params }) {
const wasm = await registry.getWasm(`${params.user}/${params.collection}/${params.node}`);
const wasm = await registry.getWasm(
`${params.user}/${params.collection}/${params.node}`,
);
if (!wasm) {
return new Response("Not found", { status: 404 });
}
return new Response(wasm, { status: 200, headers: { "Content-Type": "application/wasm" } });
}
return new Response(wasm, {
status: 200,
headers: { "Content-Type": "application/wasm" },
});
};

View File

@@ -1,100 +0,0 @@
<script lang="ts">
import {
decodeFloat,
encodeFloat,
decodeNestedArray,
encodeNestedArray,
concatEncodedArrays,
} from "@nodes/utils";
console.clear();
{
const encodedPositions = new Int32Array([
encodeFloat(1.1),
encodeFloat(2.0),
encodeFloat(3.0),
encodeFloat(4.0),
encodeFloat(5.0),
encodeFloat(6.0),
encodeFloat(7.0),
encodeFloat(8.0),
encodeFloat(9.0),
]);
// Create a Float32Array using the same buffer that backs the Int32Array
const floatView = new Float32Array(encodedPositions.buffer);
console.log({ encodedPositions, floatView });
}
if (false) {
const input_a = encodeNestedArray([1, 2, 3]);
const input_b = 2;
const input_c = 89;
const input_d = encodeNestedArray([4, 5, 6]);
const output = concatNestedArrays([input_a, input_b, input_c, input_d]);
const decoded = decodeNestedArray(output);
console.log("CONCAT", [input_a, input_b, input_c, input_d]);
console.log(output);
console.log(decoded);
}
if (false) {
let maxError = 0;
new Array(10_000).fill(null).forEach((v, i) => {
const input = i < 5_000 ? i : Math.random() * 100;
const encoded = encodeFloat(input);
const output = decodeFloat(encoded[0], encoded[1]);
const error = Math.abs(input - output);
if (error > maxError) {
maxError = error;
}
});
console.log("DECODE FLOAT");
console.log(maxError);
console.log(encodeFloat(2.0));
console.log("----");
}
if (false) {
console.log("Turning Int32Array into Array");
const test_size = 2_000_000;
const a = new Int32Array(test_size);
let t0 = performance.now();
for (let i = 0; i < test_size; i++) {
a[i] = Math.floor(Math.random() * 100);
}
console.log("TIME", performance.now() - t0);
t0 = performance.now();
const b = [...a.slice(0, test_size)];
console.log("TIME", performance.now() - t0);
console.log(typeof b, Array.isArray(b), b instanceof Int32Array);
}
if (false) {
// const input = [5, [6, 1], [7, 2, [5, 1]]];
// const input = [5, [], [6, []], []];
// const input = [52];
const input = [0, 0, [0, 2, 0, 128, 0, 128], 0, 128];
console.log("INPUT");
console.log(input);
let encoded = encodeNestedArray(input);
// encoded = [];
console.log("ENCODED");
console.log(encoded);
encoded = [0, 2, 1, 0, 4, 4, 2, 4, 1, 2, 2, 0, 3, 2, 3, 1, 1, 1, 1];
const decoded = decodeNestedArray(encoded);
console.log("DECODED");
console.log(decoded);
console.log("EQUALS", JSON.stringify(input) === JSON.stringify(decoded));
}
</script>

View File

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

View File

@@ -1,10 +1,12 @@
{
"scripts": {
"build": "pnpm build:nodes && pnpm build:app",
"build:story": "pnpm -r --filter 'ui' story:build",
"build:app": "BASE_PATH=/ui pnpm -r --filter 'ui' build && pnpm -r --filter 'app' build",
"build:nodes": "pnpm -r --filter './nodes/**' build",
"dev:nodes": "pnpm -r --parallel --filter './nodes/**' dev",
"build:deploy": "pnpm build && cp -r ./packages/ui/storybook-static ./app/build/ui",
"build:deploy": "pnpm build",
"dev": "pnpm -r --filter 'app' --filter './packages/node-registry' dev"
}
},
"packageManager": "pnpm@10.23.0"
}

View File

@@ -12,6 +12,6 @@
"dependencies": {
"@nodes/types": "link:../types",
"@nodes/utils": "link:../utils",
"idb": "^8.0.0"
"idb": "^8.0.3"
}
}

View File

@@ -1,83 +1,88 @@
import { type NodeRegistry, type NodeDefinition, NodeDefinitionSchema, type AsyncCache } from "@nodes/types";
import { createWasmWrapper, createLogger } from "@nodes/utils";
import {
NodeDefinitionSchema,
type AsyncCache,
type NodeDefinition,
type NodeRegistry,
} from "@nodes/types";
import { createLogger, createWasmWrapper } from "@nodes/utils";
const log = createLogger("node-registry");
log.mute();
// log.mute();
export class RemoteNodeRegistry implements NodeRegistry {
status: "loading" | "ready" | "error" = "loading";
private nodes: Map<string, NodeDefinition> = new Map();
cache?: AsyncCache<ArrayBuffer>;
async fetchJson(url: string) {
const response = await fetch(`${this.url}/${url}`);
fetch: typeof fetch = globalThis.fetch.bind(globalThis);
constructor(private url: string) { }
async fetchUsers() {
const response = await this.fetch(`${this.url}/nodes/users.json`);
if (!response.ok) {
throw new Error(`Failed to load users`);
log.error(`Failed to load ${url}`, { response, url, host: this.url });
throw new Error(`Failed to load ${url}`);
}
return response.json();
}
async fetchUser(userId: `${string}`) {
const response = await this.fetch(`${this.url}/nodes/${userId}.json`);
async fetchArrayBuffer(url: string) {
const response = await fetch(`${this.url}/${url}`);
if (!response.ok) {
throw new Error(`Failed to load user ${userId}`);
}
return response.json();
}
async fetchCollection(userCollectionId: `${string}/${string}`) {
const response = await this.fetch(`${this.url}/nodes/${userCollectionId}.json`);
if (!response.ok) {
throw new Error(`Failed to load collection ${userCollectionId}`);
}
return response.json();
}
async fetchNodeDefinition(nodeId: `${string}/${string}/${string}`) {
const response = await this.fetch(`${this.url}/nodes/${nodeId}.json`);
if (!response.ok) {
throw new Error(`Failed to load node definition ${nodeId}`);
}
return response.json()
}
private async fetchNodeWasm(nodeId: `${string}/${string}/${string}`) {
const response = await this.fetch(`${this.url}/nodes/${nodeId}.wasm`);
if (!response.ok) {
if (this.cache) {
let value = await this.cache.get(nodeId);
if (value) {
return value;
}
}
throw new Error(`Failed to load node wasm ${nodeId}`);
log.error(`Failed to load ${url}`, { response, url, host: this.url });
throw new Error(`Failed to load ${url}`);
}
return response.arrayBuffer();
}
constructor(
private url: string,
private cache?: AsyncCache<ArrayBuffer>,
) {}
async fetchUsers() {
return this.fetchJson(`nodes/users.json`);
}
async fetchUser(userId: `${string}`) {
return this.fetchJson(`user/${userId}.json`);
}
async fetchCollection(userCollectionId: `${string}/${string}`) {
return this.fetchJson(`nodes/${userCollectionId}.json`);
}
async fetchNodeDefinition(nodeId: `${string}/${string}/${string}`) {
return this.fetchJson(`nodes/${nodeId}.json`);
}
private async fetchNodeWasm(nodeId: `${string}/${string}/${string}`) {
const cachedNode = this.cache?.get(nodeId);
if(cachedNode){
return cachedNode;
}
const node = this.fetchArrayBuffer(`nodes/${nodeId}.wasm`);
if (node) {
return node;
}
throw new Error(`Failed to load node wasm ${nodeId}`);
}
async load(nodeIds: `${string}/${string}/${string}`[]) {
const a = performance.now();
const nodes = await Promise.all([...new Set(nodeIds).values()].map(async id => {
const nodes = await Promise.all(
[...new Set(nodeIds).values()].map(async (id) => {
if (this.nodes.has(id)) {
return this.nodes.get(id)!;
}
if (this.nodes.has(id)) {
return this.nodes.get(id)!;
}
const wasmBuffer = await this.fetchNodeWasm(id);
return this.register(wasmBuffer);
}));
const wasmBuffer = await this.fetchNodeWasm(id);
return this.register(wasmBuffer);
}),
);
const duration = performance.now() - a;
@@ -87,11 +92,10 @@ export class RemoteNodeRegistry implements NodeRegistry {
log.groupEnd();
this.status = "ready";
return nodes
return nodes;
}
async register(wasmBuffer: ArrayBuffer) {
const wrapper = createWasmWrapper(wasmBuffer);
const definition = NodeDefinitionSchema.safeParse(wrapper.get_definition());
@@ -107,8 +111,8 @@ export class RemoteNodeRegistry implements NodeRegistry {
let node = {
...definition.data,
execute: wrapper.execute
}
execute: wrapper.execute,
};
this.nodes.set(definition.data.id, node);

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.13.1"
},
"devDependencies": {
"@hey-api/openapi-ts": "^0.88.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

@@ -13,6 +13,6 @@
"author": "",
"license": "ISC",
"dependencies": {
"zod": "^3.23.8"
"zod": "^4.1.12"
}
}

View File

@@ -1,25 +1,25 @@
import { Graph, NodeDefinition, NodeId } from "./types";
import type { Graph, NodeDefinition, NodeType } from "./types";
export interface NodeRegistry {
/**
* The status of the node registry
* @remarks The status should be "loading" when the registry is loading, "ready" when the registry is ready, and "error" if an error occurred while loading the registry
*/
* The status of the node registry
* @remarks The status should be "loading" when the registry is loading, "ready" when the registry is ready, and "error" if an error occurred while loading the registry
*/
status: "loading" | "ready" | "error";
/**
* Load the nodes with the given ids
* @param nodeIds - The ids of the nodes to load
* @returns A promise that resolves when the nodes are loaded
* @throws An error if the nodes could not be loaded
* @remarks This method should be called before calling getNode or getAllNodes
*/
load: (nodeIds: NodeId[]) => Promise<NodeDefinition[]>;
* @param nodeIds - The ids of the nodes to load
* @returns A promise that resolves when the nodes are loaded
* @throws An error if the nodes could not be loaded
* @remarks This method should be called before calling getNode or getAllNodes
*/
load: (nodeIds: NodeType[]) => Promise<NodeDefinition[]>;
/**
* Get a node by id
* @param id - The id of the node to get
* @returns The node with the given id, or undefined if no such node exists
*/
getNode: (id: NodeId | string) => NodeDefinition | undefined;
getNode: (id: NodeType | string) => NodeDefinition | undefined;
/**
* Get all nodes
* @returns An array of all nodes
@@ -27,30 +27,30 @@ export interface NodeRegistry {
getAllNodes: () => NodeDefinition[];
/**
* Register a new node
* @param wasmBuffer - The WebAssembly buffer for the node
* @returns The node definition
* Register a new node
* @param wasmBuffer - The WebAssembly buffer for the node
* @returns The node definition
*/
register: (wasmBuffer: ArrayBuffer) => Promise<NodeDefinition>;
cache?: AsyncCache<ArrayBuffer>;
}
export interface RuntimeExecutor {
/**
* Execute the given graph
* @param graph - The graph to execute
* @returns The result of the execution
*/
execute: (graph: Graph, settings: Record<string, unknown>) => Promise<Int32Array>;
* Execute the given graph
* @param graph - The graph to execute
* @returns The result of the execution
*/
execute: (
graph: Graph,
settings: Record<string, unknown>,
) => Promise<Int32Array>;
}
export interface SyncCache<T = unknown> {
/**
* The maximum number of items that can be stored in the cache
* @remarks When the cache size exceeds this value, the oldest items should be removed
*/
* The maximum number of items that can be stored in the cache
* @remarks When the cache size exceeds this value, the oldest items should be removed
*/
size: number;
/**
@@ -69,14 +69,13 @@ export interface SyncCache<T = unknown> {
* Clear the cache
*/
clear: () => void;
}
export interface AsyncCache<T = unknown> {
/**
* The maximum number of items that can be stored in the cache
* @remarks When the cache size exceeds this value, the oldest items should be removed
*/
* The maximum number of items that can be stored in the cache
* @remarks When the cache size exceeds this value, the oldest items should be removed
*/
size: number;
/**

View File

@@ -1,6 +1,17 @@
export type { NodeInput } from "./inputs";
export type { NodeRegistry, RuntimeExecutor, SyncCache, AsyncCache } from "./components";
export type { Node, NodeDefinition, Socket, NodeId, Edge, Graph } from "./types";
export type {
NodeRegistry,
RuntimeExecutor,
SyncCache,
AsyncCache,
} from "./components";
export type {
Node,
NodeDefinition,
Socket,
NodeType,
Edge,
Graph,
} from "./types";
export { NodeSchema, GraphSchema } from "./types";
export { NodeDefinitionSchema } from "./types";

View File

@@ -6,11 +6,23 @@ const DefaultOptionsSchema = z.object({
setting: z.string().optional(),
label: z.string().optional(),
description: z.string().optional(),
accepts: z.array(z.string()).optional(),
accepts: z
.array(
z.union([
z.literal("float"),
z.literal("integer"),
z.literal("boolean"),
z.literal("select"),
z.literal("seed"),
z.literal("vec3"),
z.literal("geometry"),
z.literal("path"),
]),
)
.optional(),
hidden: z.boolean().optional(),
});
export const NodeInputFloatSchema = z.object({
...DefaultOptionsSchema.shape,
type: z.literal("float"),
@@ -40,7 +52,7 @@ export const NodeInputSelectSchema = z.object({
...DefaultOptionsSchema.shape,
type: z.literal("select"),
options: z.array(z.string()).optional(),
value: z.number().optional(),
value: z.string().optional(),
});
export const NodeInputSeedSchema = z.object({
@@ -74,7 +86,7 @@ export const NodeInputSchema = z.union([
NodeInputSeedSchema,
NodeInputVec3Schema,
NodeInputGeometrySchema,
NodeInputPathSchema
NodeInputPathSchema,
]);
export type NodeInput = z.infer<typeof NodeInputSchema>;

View File

@@ -1,27 +1,21 @@
import { z } from "zod";
import { NodeInputSchema } from "./inputs";
export type NodeId = `${string}/${string}/${string}`;
export const NodeTypeSchema = z
.string()
.regex(/^[^/]+\/[^/]+\/[^/]+$/, "Invalid NodeId format")
.transform((value) => value as `${string}/${string}/${string}`);
export const NodeSchema = z.object({
id: z.number(),
type: z.string(),
props: z.record(z.union([z.number(), z.array(z.number())])).optional(),
meta: z.object({
title: z.string().optional(),
lastModified: z.string().optional(),
}).optional(),
position: z.tuple([z.number(), z.number()])
});
export type NodeType = z.infer<typeof NodeTypeSchema>;
export type Node = {
tmp?: {
depth?: number;
mesh?: any;
random?: number;
parents?: Node[],
children?: Node[],
inputNodes?: Record<string, Node>
parents?: Node[];
children?: Node[];
inputNodes?: Record<string, Node>;
type?: NodeDefinition;
downX?: number;
downY?: number;
@@ -30,17 +24,34 @@ export type Node = {
ref?: HTMLElement;
visible?: boolean;
isMoving?: boolean;
}
};
} & z.infer<typeof NodeSchema>;
export const NodeDefinitionSchema = z.object({
id: z.string(),
inputs: z.record(NodeInputSchema).optional(),
id: NodeTypeSchema,
inputs: z.record(z.string(), NodeInputSchema).optional(),
outputs: z.array(z.string()).optional(),
meta: z.object({
description: z.string().optional(),
title: z.string().optional(),
}).optional(),
meta: z
.object({
description: z.string().optional(),
title: z.string().optional(),
})
.optional(),
});
export const NodeSchema = z.object({
id: z.number(),
type: NodeTypeSchema,
props: z
.record(z.string(), z.union([z.number(), z.array(z.number())]))
.optional(),
meta: z
.object({
title: z.string().optional(),
lastModified: z.string().optional(),
})
.optional(),
position: z.tuple([z.number(), z.number()]),
});
export type NodeDefinition = z.infer<typeof NodeDefinitionSchema> & {
@@ -56,12 +67,14 @@ export type Socket = {
export type Edge = [Node, number, Node, string];
export const GraphSchema = z.object({
id: z.number().optional(),
meta: z.object({
title: z.string().optional(),
lastModified: z.string().optional(),
}).optional(),
settings: z.record(z.any()).optional(),
id: z.number(),
meta: z
.object({
title: z.string().optional(),
lastModified: z.string().optional(),
})
.optional(),
settings: z.record(z.string(), z.any()).optional(),
nodes: z.array(NodeSchema),
edges: z.array(z.tuple([z.number(), z.number(), z.number(), z.string()])),
});

View File

@@ -30,36 +30,36 @@
"svelte": "^4.0.0"
},
"devDependencies": {
"@storybook/addon-essentials": "^8.4.1",
"@storybook/addon-svelte-csf": "5.0.0-next.10",
"@storybook/addon-themes": "^8.4.1",
"@storybook/svelte": "^8.4.1",
"@storybook/sveltekit": "^8.4.1",
"@sveltejs/adapter-static": "^3.0.6",
"@sveltejs/kit": "^2.7.4",
"@sveltejs/package": "^2.3.7",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@storybook/addon-essentials": "^8.6.14",
"@storybook/addon-svelte-csf": "5.0.10",
"@storybook/addon-themes": "^10.0.8",
"@storybook/svelte": "^10.0.8",
"@storybook/sveltekit": "^10.0.8",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.49.0",
"@sveltejs/package": "^2.5.6",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@types/eslint": "^9.6.1",
"@typescript-eslint/eslint-plugin": "^8.12.2",
"@typescript-eslint/parser": "^8.12.2",
"eslint": "^9.14.0",
"eslint-plugin-storybook": "^0.10.2",
"eslint-plugin-svelte": "^2.46.0",
"publint": "^0.2.12",
"storybook": "^8.4.1",
"svelte": "^5.1.9",
"svelte-check": "^4.0.5",
"@typescript-eslint/eslint-plugin": "^8.47.0",
"@typescript-eslint/parser": "^8.47.0",
"eslint": "^9.39.1",
"eslint-plugin-storybook": "^10.0.8",
"eslint-plugin-svelte": "^3.13.0",
"publint": "^0.3.15",
"storybook": "^10.0.8",
"svelte": "^5.43.14",
"svelte-check": "^4.3.4",
"tslib": "^2.8.1",
"typescript": "^5.6.3",
"vite": "^5.4.10",
"vitest": "^2.1.4"
"typescript": "^5.9.3",
"vite": "^7.2.4",
"vitest": "^4.0.13",
"@nodes/types": "link:../types"
},
"svelte": "./dist/index.js",
"types": "./dist/index.d.ts",
"type": "module",
"dependencies": {
"@nodes/types": "link:../types",
"@threlte/core": "^7.3.1",
"@threlte/extras": "^8.12.0"
"@threlte/core": "^8.3.0",
"@threlte/extras": "^9.7.0"
}
}

View File

@@ -3,12 +3,13 @@
title?: string;
transparent?: boolean;
children?: import('svelte').Snippet;
open?: boolean;
}
let { title = "Details", transparent = false, children }: Props = $props();
let { title = "Details", transparent = false, children, open = $bindable(false) }: Props = $props();
</script>
<details class:transparent>
<details class:transparent bind:open>
<summary>{title}</summary>
<div class="content">
{@render children?.()}
@@ -33,7 +34,4 @@
outline: none;
}
.content {
/* padding-left: 12px; */
}
</style>

View File

@@ -3,7 +3,7 @@
ctrl?: boolean;
shift?: boolean;
alt?: boolean;
key: string;
key: string | string[];
}
let {

View File

@@ -13,7 +13,7 @@
"@nodes/types": "link:../types"
},
"devDependencies": {
"vite": "^5.4.10",
"vitest": "^2.1.4"
"vite": "^7.2.4",
"vitest": "^4.0.13"
}
}

View File

@@ -1,47 +1,84 @@
// https://github.com/6502/sha256/blob/main/sha256.js
function sha256(data?: string | Uint8Array) {
let h0 = 0x6a09e667, h1 = 0xbb67ae85, h2 = 0x3c6ef372, h3 = 0xa54ff53a,
h4 = 0x510e527f, h5 = 0x9b05688c, h6 = 0x1f83d9ab, h7 = 0x5be0cd19,
tsz = 0, bp = 0;
const k = [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2],
rrot = (x, n) => (x >>> n) | (x << (32 - n)),
function sha256(data?: string | Int32Array) {
let h0 = 0x6a09e667,
h1 = 0xbb67ae85,
h2 = 0x3c6ef372,
h3 = 0xa54ff53a,
h4 = 0x510e527f,
h5 = 0x9b05688c,
h6 = 0x1f83d9ab,
h7 = 0x5be0cd19,
tsz = 0,
bp = 0;
const k = [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
],
rrot = (x: number, n: number) => (x >>> n) | (x << (32 - n)),
w = new Uint32Array(64),
buf = new Uint8Array(64),
process = () => {
for (let j = 0, r = 0; j < 16; j++, r += 4) {
w[j] = (buf[r] << 24) | (buf[r + 1] << 16) | (buf[r + 2] << 8) | buf[r + 3];
w[j] =
(buf[r] << 24) | (buf[r + 1] << 16) | (buf[r + 2] << 8) | buf[r + 3];
}
for (let j = 16; j < 64; j++) {
let s0 = rrot(w[j - 15], 7) ^ rrot(w[j - 15], 18) ^ (w[j - 15] >>> 3);
let s1 = rrot(w[j - 2], 17) ^ rrot(w[j - 2], 19) ^ (w[j - 2] >>> 10);
w[j] = (w[j - 16] + s0 + w[j - 7] + s1) | 0;
}
let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7;
let a = h0,
b = h1,
c = h2,
d = h3,
e = h4,
f = h5,
g = h6,
h = h7;
for (let j = 0; j < 64; j++) {
let S1 = rrot(e, 6) ^ rrot(e, 11) ^ rrot(e, 25),
ch = (e & f) ^ ((~e) & g),
ch = (e & f) ^ (~e & g),
t1 = (h + S1 + ch + k[j] + w[j]) | 0,
S0 = rrot(a, 2) ^ rrot(a, 13) ^ rrot(a, 22),
maj = (a & b) ^ (a & c) ^ (b & c),
t2 = (S0 + maj) | 0;
h = g; g = f; f = e; e = (d + t1) | 0; d = c; c = b; b = a; a = (t1 + t2) | 0;
h = g;
g = f;
f = e;
e = (d + t1) | 0;
d = c;
c = b;
b = a;
a = (t1 + t2) | 0;
}
h0 = (h0 + a) | 0; h1 = (h1 + b) | 0; h2 = (h2 + c) | 0; h3 = (h3 + d) | 0;
h4 = (h4 + e) | 0; h5 = (h5 + f) | 0; h6 = (h6 + g) | 0; h7 = (h7 + h) | 0;
h0 = (h0 + a) | 0;
h1 = (h1 + b) | 0;
h2 = (h2 + c) | 0;
h3 = (h3 + d) | 0;
h4 = (h4 + e) | 0;
h5 = (h5 + f) | 0;
h6 = (h6 + g) | 0;
h7 = (h7 + h) | 0;
bp = 0;
},
add = data => {
if (typeof data === "string") {
data = typeof TextEncoder === "undefined" ? Buffer.from(data) : (new TextEncoder).encode(data);
}
add = (input: string | Int32Array) => {
const data =
typeof input === "string"
? typeof TextEncoder === "undefined"
? //@ts-ignore
Buffer.from(input)
: new TextEncoder().encode(input)
: input;
for (let i = 0; i < data.length; i++) {
buf[bp++] = data[i];
if (bp === 64) process();
@@ -49,7 +86,8 @@ function sha256(data?: string | Uint8Array) {
tsz += data.length;
},
digest = () => {
buf[bp++] = 0x80; if (bp == 64) process();
buf[bp++] = 0x80;
if (bp == 64) process();
if (bp + 8 > 64) {
while (bp < 64) buf[bp++] = 0x00;
process();
@@ -57,24 +95,48 @@ function sha256(data?: string | Uint8Array) {
while (bp < 58) buf[bp++] = 0x00;
// Max number of bytes is 35,184,372,088,831
let L = tsz * 8;
buf[bp++] = (L / 1099511627776.) & 255;
buf[bp++] = (L / 4294967296.) & 255;
buf[bp++] = (L / 1099511627776) & 255;
buf[bp++] = (L / 4294967296) & 255;
buf[bp++] = L >>> 24;
buf[bp++] = (L >>> 16) & 255;
buf[bp++] = (L >>> 8) & 255;
buf[bp++] = L & 255;
process();
let reply = new Uint8Array(32);
reply[0] = h0 >>> 24; reply[1] = (h0 >>> 16) & 255; reply[2] = (h0 >>> 8) & 255; reply[3] = h0 & 255;
reply[4] = h1 >>> 24; reply[5] = (h1 >>> 16) & 255; reply[6] = (h1 >>> 8) & 255; reply[7] = h1 & 255;
reply[8] = h2 >>> 24; reply[9] = (h2 >>> 16) & 255; reply[10] = (h2 >>> 8) & 255; reply[11] = h2 & 255;
reply[12] = h3 >>> 24; reply[13] = (h3 >>> 16) & 255; reply[14] = (h3 >>> 8) & 255; reply[15] = h3 & 255;
reply[16] = h4 >>> 24; reply[17] = (h4 >>> 16) & 255; reply[18] = (h4 >>> 8) & 255; reply[19] = h4 & 255;
reply[20] = h5 >>> 24; reply[21] = (h5 >>> 16) & 255; reply[22] = (h5 >>> 8) & 255; reply[23] = h5 & 255;
reply[24] = h6 >>> 24; reply[25] = (h6 >>> 16) & 255; reply[26] = (h6 >>> 8) & 255; reply[27] = h6 & 255;
reply[28] = h7 >>> 24; reply[29] = (h7 >>> 16) & 255; reply[30] = (h7 >>> 8) & 255; reply[31] = h7 & 255;
reply[0] = h0 >>> 24;
reply[1] = (h0 >>> 16) & 255;
reply[2] = (h0 >>> 8) & 255;
reply[3] = h0 & 255;
reply[4] = h1 >>> 24;
reply[5] = (h1 >>> 16) & 255;
reply[6] = (h1 >>> 8) & 255;
reply[7] = h1 & 255;
reply[8] = h2 >>> 24;
reply[9] = (h2 >>> 16) & 255;
reply[10] = (h2 >>> 8) & 255;
reply[11] = h2 & 255;
reply[12] = h3 >>> 24;
reply[13] = (h3 >>> 16) & 255;
reply[14] = (h3 >>> 8) & 255;
reply[15] = h3 & 255;
reply[16] = h4 >>> 24;
reply[17] = (h4 >>> 16) & 255;
reply[18] = (h4 >>> 8) & 255;
reply[19] = h4 & 255;
reply[20] = h5 >>> 24;
reply[21] = (h5 >>> 16) & 255;
reply[22] = (h5 >>> 8) & 255;
reply[23] = h5 & 255;
reply[24] = h6 >>> 24;
reply[25] = (h6 >>> 16) & 255;
reply[26] = (h6 >>> 8) & 255;
reply[27] = h6 & 255;
reply[28] = h7 >>> 24;
reply[29] = (h7 >>> 16) & 255;
reply[30] = (h7 >>> 8) & 255;
reply[31] = h7 & 255;
let res = "";
reply.forEach(x => res += ("0" + x.toString(16)).slice(-2));
reply.forEach((x) => (res += ("0" + x.toString(16)).slice(-2)));
return res;
};
@@ -83,8 +145,8 @@ function sha256(data?: string | Uint8Array) {
return { add, digest };
}
export function fastHashArrayBuffer(buffer: ArrayBuffer): string {
return sha256(new Uint8Array(buffer)).digest();
export function fastHashArrayBuffer(buffer: string | Int32Array): string {
return sha256(buffer).digest();
}
// Shamelessly copied from
@@ -101,22 +163,19 @@ export function fastHashString(input: string) {
return hash;
}
export function fastHash(input: (string | Int32Array | number)[]) {
const s = sha256();
for (let i = 0; i < input.length; i++) {
const v = input[i]
const v = input[i];
if (typeof v === "string") {
s.add(v);
} else if (v instanceof Int32Array) {
s.add(new Uint8Array(v.buffer));
s.add(v);
} else {
s.add(v.toString());
}
}
return s.digest()
return s.digest();
}

View File

@@ -1,7 +1,8 @@
type SparseArray<T = number> = (T | T[] | SparseArray<T>)[];
export function concatEncodedArrays(input: (number | number[] | Int32Array)[]): Int32Array {
export function concatEncodedArrays(
input: (number | number[] | Int32Array)[],
): Int32Array {
let totalLength = 4;
for (let i = 0; i < input.length; i++) {
const item = input[i];
@@ -36,7 +37,7 @@ export function concatEncodedArrays(input: (number | number[] | Int32Array)[]):
result[totalLength - 2] = 1;
result[totalLength - 1] = 1;
return result
return result;
}
// Encodes a nested array into a flat array with bracket and distance notation
@@ -68,12 +69,11 @@ export function encodeNestedArray(array: SparseArray): number[] {
}
return [...encoded, 1, 1];
};
}
function decode_recursive(dense: number[] | Int32Array, index = 0) {
if (dense instanceof Int32Array) {
dense = Array.from(dense)
dense = Array.from(dense);
}
const decoded: (number | number[])[] = [];
@@ -82,12 +82,17 @@ function decode_recursive(dense: number[] | Int32Array, index = 0) {
index += 2; // Skip the initial bracket notation
while (index < dense.length) {
if (index === nextBracketIndex) {
if (dense[index] === 0) { // Opening bracket detected
const [p, nextIndex, _nextBracketIndex] = decode_recursive(dense, index);
decoded.push(p);
if (dense[index] === 0) {
// Opening bracket detected
const [p, nextIndex, _nextBracketIndex] = decode_recursive(
dense,
index,
);
decoded.push(...p);
index = nextIndex + 1;
nextBracketIndex = _nextBracketIndex;
} else { // Closing bracket detected
} else {
// Closing bracket detected
nextBracketIndex = dense[index + 1] + index + 1;
return [decoded, index, nextBracketIndex] as const;
}
@@ -103,7 +108,6 @@ export function decodeNestedArray(dense: number[] | Int32Array) {
return decode_recursive(dense, 0)[0];
}
export function splitNestedArray(input: Int32Array) {
let index = 0;
const length = input.length;

View File

@@ -1,21 +1,25 @@
//@ts-nocheck
import { NodeDefinition } from "@nodes/types";
const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
const cachedTextDecoder = new TextDecoder("utf-8", {
ignoreBOM: true,
fatal: true,
});
const cachedTextEncoder = new TextEncoder();
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
? function (arg, view) {
return cachedTextEncoder.encodeInto(arg, view);
}
: function (arg, view) {
const buf = cachedTextEncoder.encode(arg);
view.set(buf);
return {
read: arg.length,
written: buf.length
};
});
const encodeString =
typeof cachedTextEncoder.encodeInto === "function"
? function (arg, view) {
return cachedTextEncoder.encodeInto(arg, view);
}
: function (arg, view) {
const buf = cachedTextEncoder.encode(arg);
view.set(buf);
return {
read: arg.length,
written: buf.length,
};
};
function createWrapper() {
let wasm: any;
@@ -53,7 +57,9 @@ function createWrapper() {
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
}
function getObject(idx: number) { return heap[idx]; }
function getObject(idx: number) {
return heap[idx];
}
function addHeapObject(obj: any) {
if (heap_next === heap.length) heap.push(heap.length + 1);
@@ -63,9 +69,11 @@ function createWrapper() {
return idx;
}
let WASM_VECTOR_LEN = 0;
function passArray32ToWasm0(arg: ArrayLike<number>, malloc: (arg0: number, arg1: number) => number) {
function passArray32ToWasm0(
arg: ArrayLike<number>,
malloc: (arg0: number, arg1: number) => number,
) {
const ptr = malloc(arg.length * 4, 4) >>> 0;
getUint32Memory0().set(arg, ptr / 4);
WASM_VECTOR_LEN = arg.length;
@@ -89,21 +97,10 @@ function createWrapper() {
return ret;
}
function getArrayJsValueFromWasm0(ptr: number, len: number) {
ptr = ptr >>> 0;
const mem = getUint32Memory0();
const slice = mem.subarray(ptr / 4, ptr / 4 + len);
const result = [];
for (let i = 0; i < slice.length; i++) {
result.push(takeObject(slice[i]));
}
return result;
}
function __wbindgen_string_new(arg0: number, arg1: number) {
const ret = getStringFromWasm0(arg0, arg1);
return addHeapObject(ret);
};
}
// Additional methods and their internal helpers can also be refactored in a similar manner.
function get_definition() {
@@ -124,7 +121,6 @@ function createWrapper() {
}
}
function execute(args: Int32Array) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
@@ -141,12 +137,19 @@ function createWrapper() {
}
}
function passStringToWasm0(arg: string, malloc: (arg0: any, arg1: number) => number, realloc: ((arg0: number, arg1: any, arg2: number, arg3: number) => number) | undefined) {
function passStringToWasm0(
arg: string,
malloc: (arg0: any, arg1: number) => number,
realloc:
| ((arg0: number, arg1: any, arg2: number, arg3: number) => number)
| undefined,
) {
if (realloc === undefined) {
const buf = cachedTextEncoder.encode(arg);
const ptr = malloc(buf.length, 1) >>> 0;
getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf);
getUint8Memory0()
.subarray(ptr, ptr + buf.length)
.set(buf);
WASM_VECTOR_LEN = buf.length;
return ptr;
}
@@ -160,7 +163,7 @@ function createWrapper() {
for (; offset < len; offset++) {
const code = arg.charCodeAt(offset);
if (code > 0x7F) break;
if (code > 0x7f) break;
mem[ptr + offset] = code;
}
@@ -168,7 +171,7 @@ function createWrapper() {
if (offset !== 0) {
arg = arg.slice(offset);
}
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
ptr = realloc(ptr, len, (len = offset + arg.length * 3), 1) >>> 0;
const view = getUint8Memory0().subarray(ptr + offset, ptr + len);
const ret = encodeString(arg, view);
@@ -183,15 +186,19 @@ function createWrapper() {
function __wbg_new_abda76e883ba8a5f() {
const ret = new Error();
return addHeapObject(ret);
};
}
function __wbg_stack_658279fe44541cf6(arg0, arg1) {
const ret = getObject(arg1).stack;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const ptr1 = passStringToWasm0(
ret,
wasm.__wbindgen_malloc,
wasm.__wbindgen_realloc,
);
const len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
};
}
function __wbg_error_f851667af71bcfc6(arg0, arg1) {
let deferred0_0;
@@ -203,27 +210,25 @@ function createWrapper() {
} finally {
wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
}
};
}
function __wbindgen_object_drop_ref(arg0) {
takeObject(arg0);
};
}
function __wbg_log_5bb5f88f245d7762(arg0) {
console.log(getObject(arg0));
};
}
function __wbindgen_throw(arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
};
}
return {
setInstance(instance: WebAssembly.Instance) {
wasm = instance.exports;
},
exports: {
// Expose other methods that interact with the wasm instance
execute,
@@ -240,11 +245,12 @@ function createWrapper() {
};
}
export function createWasmWrapper(wasmBuffer: ArrayBuffer) {
export function createWasmWrapper(wasmBuffer: ArrayBuffer | Uint8Array) {
const wrapper = createWrapper();
const module = new WebAssembly.Module(wasmBuffer);
const instance = new WebAssembly.Instance(module, { ["./index_bg.js"]: wrapper });
const instance = new WebAssembly.Instance(module, {
["./index_bg.js"]: wrapper,
});
wrapper.setInstance(instance);
return wrapper.exports;
}

5838
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

1
store/.env Normal file
View File

@@ -0,0 +1 @@
DATABASE_URL=postgres://nodarium:nodarium@postgres-db:5432/nodarium

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"]

65
store/bin/upload.ts Normal file
View File

@@ -0,0 +1,65 @@
import * as path from "jsr:@std/path";
const arg = Deno.args[0];
const base = arg.startsWith("/") ? arg : path.join(Deno.cwd(), arg);
const dirs = Deno.readDir(base);
type Node = {
user: string;
system: string;
id: string;
path: string;
};
const nodes: Node[] = [];
for await (const dir of dirs) {
if (dir.isDirectory) {
const userDir = path.join(base, dir.name);
for await (const userName of Deno.readDir(userDir)) {
if (userName.isDirectory) {
const nodeSystemDir = path.join(userDir, userName.name);
for await (const nodeDir of Deno.readDir(nodeSystemDir)) {
if (nodeDir.isDirectory && !nodeDir.name.startsWith(".")) {
const wasmFilePath = path.join(
nodeSystemDir,
nodeDir.name,
"pkg",
"index_bg.wasm",
);
nodes.push({
user: dir.name,
system: userName.name,
id: nodeDir.name,
path: wasmFilePath,
});
}
}
}
}
}
}
async function postNode(node: Node) {
const wasmContent = await Deno.readFile(node.path);
const url = `http://localhost:8000/nodes`;
// const url = "https://node-store.app.max-richter.dev/nodes";
const res = await fetch(url, {
method: "POST",
body: wasmContent,
});
if (res.ok) {
console.log(`Uploaded ${node.id}`);
} else {
const text = await res.text();
console.log(`Failed to upload ${node.id}: ${res.status} ${text}`);
}
}
for (const node of nodes) {
await postNode(node);
}

29
store/compose.yml Normal file
View File

@@ -0,0 +1,29 @@
services:
app:
image: denoland/deno:latest
working_dir: /app
ports:
- 8000:8000
environment:
DATABASE_URL: postgres://nodarium:nodarium@db:5432/nodarium
volumes:
- .:/app
- deno-cache:/deno-dir/
command: task dev
depends_on:
- db
db:
image: postgres:latest
environment:
POSTGRES_USER: nodarium
POSTGRES_PASSWORD: nodarium
POSTGRES_DB: nodarium
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
postgres-data:
deno-cache:

22
store/deno.json Normal file
View File

@@ -0,0 +1,22 @@
{
"tasks": {
"dev": "deno run -A --watch src/server.ts",
"run": "deno run -A src/server.ts",
"test": "deno run vitest",
"drizzle": "podman-compose exec app deno --env -A --node-modules-dir npm:drizzle-kit",
"upload": "deno run --allow-read --allow-net bin/upload.ts"
},
"imports": {
"@asteasolutions/zod-to-openapi": "npm:@asteasolutions/zod-to-openapi@^7.3.0",
"@hono/swagger-ui": "npm:@hono/swagger-ui@^0.5.0",
"@hono/zod-openapi": "npm:@hono/zod-openapi@^0.18.3",
"@std/assert": "jsr:@std/assert@1",
"@types/pg": "npm:@types/pg@^8.11.10",
"drizzle-kit": "npm:drizzle-kit@^0.30.1",
"drizzle-orm": "npm:drizzle-orm@^0.38.2",
"hono": "npm:hono@^4.6.14",
"pg": "npm:pg@^8.13.1",
"vitest": "npm:vitest@^2.1.8",
"zod": "npm:zod@^3.24.1"
}
}

883
store/deno.lock generated Normal file
View File

@@ -0,0 +1,883 @@
{
"version": "4",
"specifiers": {
"jsr:@std/assert@1": "1.0.9",
"jsr:@std/assert@^1.0.9": "1.0.9",
"jsr:@std/bytes@^1.0.2": "1.0.4",
"jsr:@std/crypto@^1.0.3": "1.0.3",
"jsr:@std/expect@*": "1.0.9",
"jsr:@std/internal@^1.0.5": "1.0.5",
"jsr:@std/path@*": "1.0.8",
"jsr:@std/uuid@*": "1.0.4",
"npm:@asteasolutions/zod-to-openapi@^7.3.0": "7.3.0_zod@3.24.1",
"npm:@hono/swagger-ui@0.5": "0.5.0_hono@4.6.14",
"npm:@hono/zod-openapi@~0.18.3": "0.18.3_hono@4.6.14_zod@3.24.1",
"npm:@types/node@*": "22.5.4",
"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": "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:hono@^4.6.14": "4.6.14",
"npm:pg@^8.13.1": "8.13.1",
"npm:vitest@^2.1.8": "2.1.8_vite@5.4.11",
"npm:zod@^3.24.1": "3.24.1"
},
"jsr": {
"@std/assert@1.0.9": {
"integrity": "a9f0c611a869cc791b26f523eec54c7e187aab7932c2c8e8bea0622d13680dcd",
"dependencies": [
"jsr:@std/internal"
]
},
"@std/bytes@1.0.4": {
"integrity": "11a0debe522707c95c7b7ef89b478c13fb1583a7cfb9a85674cd2cc2e3a28abc"
},
"@std/crypto@1.0.3": {
"integrity": "a2a32f51ddef632d299e3879cd027c630dcd4d1d9a5285d6e6788072f4e51e7f"
},
"@std/expect@1.0.9": {
"integrity": "108bb428f17492ac40439479e1dc55fbaae581530e905a8603f97305842a5a01",
"dependencies": [
"jsr:@std/assert@^1.0.9",
"jsr:@std/internal"
]
},
"@std/internal@1.0.5": {
"integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba"
},
"@std/path@1.0.8": {
"integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be"
},
"@std/uuid@1.0.4": {
"integrity": "f4233149cc8b4753cc3763fd83a7c4101699491f55c7be78dc7b30281946d7a0",
"dependencies": [
"jsr:@std/bytes",
"jsr:@std/crypto"
]
}
},
"npm": {
"@asteasolutions/zod-to-openapi@7.3.0_zod@3.24.1": {
"integrity": "sha512-7tE/r1gXwMIvGnXVUdIqUhCU1RevEFC4Jk6Bussa0fk1ecbnnINkZzj1EOAJyE/M3AI25DnHT/zKQL1/FPFi8Q==",
"dependencies": [
"openapi3-ts",
"zod"
]
},
"@drizzle-team/brocli@0.10.2": {
"integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="
},
"@esbuild-kit/core-utils@3.3.2": {
"integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==",
"dependencies": [
"esbuild@0.18.20",
"source-map-support"
]
},
"@esbuild-kit/esm-loader@2.6.5": {
"integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==",
"dependencies": [
"@esbuild-kit/core-utils",
"get-tsconfig"
]
},
"@esbuild/aix-ppc64@0.19.12": {
"integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="
},
"@esbuild/aix-ppc64@0.21.5": {
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="
},
"@esbuild/android-arm64@0.18.20": {
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="
},
"@esbuild/android-arm64@0.19.12": {
"integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA=="
},
"@esbuild/android-arm64@0.21.5": {
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="
},
"@esbuild/android-arm@0.18.20": {
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="
},
"@esbuild/android-arm@0.19.12": {
"integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="
},
"@esbuild/android-arm@0.21.5": {
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="
},
"@esbuild/android-x64@0.18.20": {
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="
},
"@esbuild/android-x64@0.19.12": {
"integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew=="
},
"@esbuild/android-x64@0.21.5": {
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="
},
"@esbuild/darwin-arm64@0.18.20": {
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="
},
"@esbuild/darwin-arm64@0.19.12": {
"integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g=="
},
"@esbuild/darwin-arm64@0.21.5": {
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="
},
"@esbuild/darwin-x64@0.18.20": {
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="
},
"@esbuild/darwin-x64@0.19.12": {
"integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A=="
},
"@esbuild/darwin-x64@0.21.5": {
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="
},
"@esbuild/freebsd-arm64@0.18.20": {
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="
},
"@esbuild/freebsd-arm64@0.19.12": {
"integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA=="
},
"@esbuild/freebsd-arm64@0.21.5": {
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="
},
"@esbuild/freebsd-x64@0.18.20": {
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="
},
"@esbuild/freebsd-x64@0.19.12": {
"integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg=="
},
"@esbuild/freebsd-x64@0.21.5": {
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="
},
"@esbuild/linux-arm64@0.18.20": {
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="
},
"@esbuild/linux-arm64@0.19.12": {
"integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA=="
},
"@esbuild/linux-arm64@0.21.5": {
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="
},
"@esbuild/linux-arm@0.18.20": {
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="
},
"@esbuild/linux-arm@0.19.12": {
"integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w=="
},
"@esbuild/linux-arm@0.21.5": {
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="
},
"@esbuild/linux-ia32@0.18.20": {
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="
},
"@esbuild/linux-ia32@0.19.12": {
"integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA=="
},
"@esbuild/linux-ia32@0.21.5": {
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="
},
"@esbuild/linux-loong64@0.18.20": {
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="
},
"@esbuild/linux-loong64@0.19.12": {
"integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA=="
},
"@esbuild/linux-loong64@0.21.5": {
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="
},
"@esbuild/linux-mips64el@0.18.20": {
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="
},
"@esbuild/linux-mips64el@0.19.12": {
"integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w=="
},
"@esbuild/linux-mips64el@0.21.5": {
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="
},
"@esbuild/linux-ppc64@0.18.20": {
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="
},
"@esbuild/linux-ppc64@0.19.12": {
"integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg=="
},
"@esbuild/linux-ppc64@0.21.5": {
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="
},
"@esbuild/linux-riscv64@0.18.20": {
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="
},
"@esbuild/linux-riscv64@0.19.12": {
"integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg=="
},
"@esbuild/linux-riscv64@0.21.5": {
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="
},
"@esbuild/linux-s390x@0.18.20": {
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="
},
"@esbuild/linux-s390x@0.19.12": {
"integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg=="
},
"@esbuild/linux-s390x@0.21.5": {
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="
},
"@esbuild/linux-x64@0.18.20": {
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="
},
"@esbuild/linux-x64@0.19.12": {
"integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg=="
},
"@esbuild/linux-x64@0.21.5": {
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="
},
"@esbuild/netbsd-x64@0.18.20": {
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="
},
"@esbuild/netbsd-x64@0.19.12": {
"integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA=="
},
"@esbuild/netbsd-x64@0.21.5": {
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="
},
"@esbuild/openbsd-x64@0.18.20": {
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="
},
"@esbuild/openbsd-x64@0.19.12": {
"integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw=="
},
"@esbuild/openbsd-x64@0.21.5": {
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="
},
"@esbuild/sunos-x64@0.18.20": {
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="
},
"@esbuild/sunos-x64@0.19.12": {
"integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA=="
},
"@esbuild/sunos-x64@0.21.5": {
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="
},
"@esbuild/win32-arm64@0.18.20": {
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="
},
"@esbuild/win32-arm64@0.19.12": {
"integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A=="
},
"@esbuild/win32-arm64@0.21.5": {
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="
},
"@esbuild/win32-ia32@0.18.20": {
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="
},
"@esbuild/win32-ia32@0.19.12": {
"integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ=="
},
"@esbuild/win32-ia32@0.21.5": {
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="
},
"@esbuild/win32-x64@0.18.20": {
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="
},
"@esbuild/win32-x64@0.19.12": {
"integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="
},
"@esbuild/win32-x64@0.21.5": {
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="
},
"@hono/swagger-ui@0.5.0_hono@4.6.14": {
"integrity": "sha512-MWYYSv9kC8IwFBLZdwgZZMT9zUq2C/4/ekuyEYOkHEgUMqu+FG3eebtBZ4ofMh60xYRxRR2BgQGoNIILys/PFg==",
"dependencies": [
"hono"
]
},
"@hono/zod-openapi@0.18.3_hono@4.6.14_zod@3.24.1": {
"integrity": "sha512-bNlRDODnp7P9Fs13ZPajEOt13G0XwXKfKRHMEFCphQsFiD1Y+twzHaglpNAhNcflzR1DQwHY92ZS06b4LTPbIQ==",
"dependencies": [
"@asteasolutions/zod-to-openapi",
"@hono/zod-validator",
"hono",
"zod"
]
},
"@hono/zod-validator@0.4.2_hono@4.6.14_zod@3.24.1": {
"integrity": "sha512-1rrlBg+EpDPhzOV4hT9pxr5+xDVmKuz6YJl+la7VCwK6ass5ldyKm5fD+umJdV2zhHD6jROoCCv8NbTwyfhT0g==",
"dependencies": [
"hono",
"zod"
]
},
"@jridgewell/sourcemap-codec@1.5.0": {
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
},
"@rollup/rollup-android-arm-eabi@4.28.1": {
"integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ=="
},
"@rollup/rollup-android-arm64@4.28.1": {
"integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA=="
},
"@rollup/rollup-darwin-arm64@4.28.1": {
"integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ=="
},
"@rollup/rollup-darwin-x64@4.28.1": {
"integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ=="
},
"@rollup/rollup-freebsd-arm64@4.28.1": {
"integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA=="
},
"@rollup/rollup-freebsd-x64@4.28.1": {
"integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ=="
},
"@rollup/rollup-linux-arm-gnueabihf@4.28.1": {
"integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA=="
},
"@rollup/rollup-linux-arm-musleabihf@4.28.1": {
"integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg=="
},
"@rollup/rollup-linux-arm64-gnu@4.28.1": {
"integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA=="
},
"@rollup/rollup-linux-arm64-musl@4.28.1": {
"integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A=="
},
"@rollup/rollup-linux-loongarch64-gnu@4.28.1": {
"integrity": "sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA=="
},
"@rollup/rollup-linux-powerpc64le-gnu@4.28.1": {
"integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A=="
},
"@rollup/rollup-linux-riscv64-gnu@4.28.1": {
"integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA=="
},
"@rollup/rollup-linux-s390x-gnu@4.28.1": {
"integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg=="
},
"@rollup/rollup-linux-x64-gnu@4.28.1": {
"integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw=="
},
"@rollup/rollup-linux-x64-musl@4.28.1": {
"integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g=="
},
"@rollup/rollup-win32-arm64-msvc@4.28.1": {
"integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A=="
},
"@rollup/rollup-win32-ia32-msvc@4.28.1": {
"integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA=="
},
"@rollup/rollup-win32-x64-msvc@4.28.1": {
"integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA=="
},
"@types/estree@1.0.6": {
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="
},
"@types/node@22.5.4": {
"integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
"dependencies": [
"undici-types"
]
},
"@types/pg@8.11.10": {
"integrity": "sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg==",
"dependencies": [
"@types/node",
"pg-protocol",
"pg-types@4.0.2"
]
},
"@vitest/expect@2.1.8": {
"integrity": "sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==",
"dependencies": [
"@vitest/spy",
"@vitest/utils",
"chai",
"tinyrainbow"
]
},
"@vitest/mocker@2.1.8_vite@5.4.11": {
"integrity": "sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==",
"dependencies": [
"@vitest/spy",
"estree-walker",
"magic-string",
"vite"
]
},
"@vitest/pretty-format@2.1.8": {
"integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==",
"dependencies": [
"tinyrainbow"
]
},
"@vitest/runner@2.1.8": {
"integrity": "sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==",
"dependencies": [
"@vitest/utils",
"pathe"
]
},
"@vitest/snapshot@2.1.8": {
"integrity": "sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==",
"dependencies": [
"@vitest/pretty-format",
"magic-string",
"pathe"
]
},
"@vitest/spy@2.1.8": {
"integrity": "sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==",
"dependencies": [
"tinyspy"
]
},
"@vitest/utils@2.1.8": {
"integrity": "sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==",
"dependencies": [
"@vitest/pretty-format",
"loupe",
"tinyrainbow"
]
},
"assertion-error@2.0.1": {
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="
},
"buffer-from@1.1.2": {
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
},
"cac@6.7.14": {
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="
},
"chai@5.1.2": {
"integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==",
"dependencies": [
"assertion-error",
"check-error",
"deep-eql",
"loupe",
"pathval"
]
},
"check-error@2.1.1": {
"integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="
},
"debug@4.4.0": {
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"dependencies": [
"ms"
]
},
"deep-eql@5.0.2": {
"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="
},
"drizzle-kit@0.30.1_esbuild@0.19.12": {
"integrity": "sha512-HmA/NeewvHywhJ2ENXD3KvOuM/+K2dGLJfxVfIHsGwaqKICJnS+Ke2L6UcSrSrtMJLJaT0Im1Qv4TFXfaZShyw==",
"dependencies": [
"@drizzle-team/brocli",
"@esbuild-kit/esm-loader",
"esbuild@0.19.12",
"esbuild-register"
]
},
"drizzle-orm@0.38.2_@types+pg@8.11.10_pg@8.13.1": {
"integrity": "sha512-eCE3yPRAskLo1WpM9OHpFaM70tBEDsWhwR/0M3CKyztAXKR9Qs3asZlcJOEliIcUSg8GuwrlY0dmYDgmm6y5GQ==",
"dependencies": [
"@types/pg",
"pg"
]
},
"es-module-lexer@1.5.4": {
"integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw=="
},
"esbuild-register@3.6.0_esbuild@0.19.12": {
"integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==",
"dependencies": [
"debug",
"esbuild@0.19.12"
]
},
"esbuild@0.18.20": {
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
"dependencies": [
"@esbuild/android-arm@0.18.20",
"@esbuild/android-arm64@0.18.20",
"@esbuild/android-x64@0.18.20",
"@esbuild/darwin-arm64@0.18.20",
"@esbuild/darwin-x64@0.18.20",
"@esbuild/freebsd-arm64@0.18.20",
"@esbuild/freebsd-x64@0.18.20",
"@esbuild/linux-arm@0.18.20",
"@esbuild/linux-arm64@0.18.20",
"@esbuild/linux-ia32@0.18.20",
"@esbuild/linux-loong64@0.18.20",
"@esbuild/linux-mips64el@0.18.20",
"@esbuild/linux-ppc64@0.18.20",
"@esbuild/linux-riscv64@0.18.20",
"@esbuild/linux-s390x@0.18.20",
"@esbuild/linux-x64@0.18.20",
"@esbuild/netbsd-x64@0.18.20",
"@esbuild/openbsd-x64@0.18.20",
"@esbuild/sunos-x64@0.18.20",
"@esbuild/win32-arm64@0.18.20",
"@esbuild/win32-ia32@0.18.20",
"@esbuild/win32-x64@0.18.20"
]
},
"esbuild@0.19.12": {
"integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
"dependencies": [
"@esbuild/aix-ppc64@0.19.12",
"@esbuild/android-arm@0.19.12",
"@esbuild/android-arm64@0.19.12",
"@esbuild/android-x64@0.19.12",
"@esbuild/darwin-arm64@0.19.12",
"@esbuild/darwin-x64@0.19.12",
"@esbuild/freebsd-arm64@0.19.12",
"@esbuild/freebsd-x64@0.19.12",
"@esbuild/linux-arm@0.19.12",
"@esbuild/linux-arm64@0.19.12",
"@esbuild/linux-ia32@0.19.12",
"@esbuild/linux-loong64@0.19.12",
"@esbuild/linux-mips64el@0.19.12",
"@esbuild/linux-ppc64@0.19.12",
"@esbuild/linux-riscv64@0.19.12",
"@esbuild/linux-s390x@0.19.12",
"@esbuild/linux-x64@0.19.12",
"@esbuild/netbsd-x64@0.19.12",
"@esbuild/openbsd-x64@0.19.12",
"@esbuild/sunos-x64@0.19.12",
"@esbuild/win32-arm64@0.19.12",
"@esbuild/win32-ia32@0.19.12",
"@esbuild/win32-x64@0.19.12"
]
},
"esbuild@0.21.5": {
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"dependencies": [
"@esbuild/aix-ppc64@0.21.5",
"@esbuild/android-arm@0.21.5",
"@esbuild/android-arm64@0.21.5",
"@esbuild/android-x64@0.21.5",
"@esbuild/darwin-arm64@0.21.5",
"@esbuild/darwin-x64@0.21.5",
"@esbuild/freebsd-arm64@0.21.5",
"@esbuild/freebsd-x64@0.21.5",
"@esbuild/linux-arm@0.21.5",
"@esbuild/linux-arm64@0.21.5",
"@esbuild/linux-ia32@0.21.5",
"@esbuild/linux-loong64@0.21.5",
"@esbuild/linux-mips64el@0.21.5",
"@esbuild/linux-ppc64@0.21.5",
"@esbuild/linux-riscv64@0.21.5",
"@esbuild/linux-s390x@0.21.5",
"@esbuild/linux-x64@0.21.5",
"@esbuild/netbsd-x64@0.21.5",
"@esbuild/openbsd-x64@0.21.5",
"@esbuild/sunos-x64@0.21.5",
"@esbuild/win32-arm64@0.21.5",
"@esbuild/win32-ia32@0.21.5",
"@esbuild/win32-x64@0.21.5"
]
},
"estree-walker@3.0.3": {
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"dependencies": [
"@types/estree"
]
},
"expect-type@1.1.0": {
"integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA=="
},
"fsevents@2.3.3": {
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="
},
"get-tsconfig@4.8.1": {
"integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==",
"dependencies": [
"resolve-pkg-maps"
]
},
"hono@4.6.14": {
"integrity": "sha512-j4VkyUp2xazGJ8eCCLN1Vm/bxdvm/j5ZuU9AIjLu9vapn2M44p9L3Ktr9Vnb2RN2QtcR/wVjZVMlT5k7GJQgPw=="
},
"loupe@3.1.2": {
"integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg=="
},
"magic-string@0.30.17": {
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
"dependencies": [
"@jridgewell/sourcemap-codec"
]
},
"ms@2.1.3": {
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"nanoid@3.3.8": {
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="
},
"obuf@1.1.2": {
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="
},
"openapi3-ts@4.4.0": {
"integrity": "sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw==",
"dependencies": [
"yaml"
]
},
"pathe@1.1.2": {
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="
},
"pathval@2.0.0": {
"integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA=="
},
"pg-cloudflare@1.1.1": {
"integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q=="
},
"pg-connection-string@2.7.0": {
"integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA=="
},
"pg-int8@1.0.1": {
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="
},
"pg-numeric@1.0.2": {
"integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw=="
},
"pg-pool@3.7.0_pg@8.13.1": {
"integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==",
"dependencies": [
"pg"
]
},
"pg-protocol@1.7.0": {
"integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ=="
},
"pg-types@2.2.0": {
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"dependencies": [
"pg-int8",
"postgres-array@2.0.0",
"postgres-bytea@1.0.0",
"postgres-date@1.0.7",
"postgres-interval@1.2.0"
]
},
"pg-types@4.0.2": {
"integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==",
"dependencies": [
"pg-int8",
"pg-numeric",
"postgres-array@3.0.2",
"postgres-bytea@3.0.0",
"postgres-date@2.1.0",
"postgres-interval@3.0.0",
"postgres-range"
]
},
"pg@8.13.1": {
"integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==",
"dependencies": [
"pg-cloudflare",
"pg-connection-string",
"pg-pool",
"pg-protocol",
"pg-types@2.2.0",
"pgpass"
]
},
"pgpass@1.0.5": {
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"dependencies": [
"split2"
]
},
"picocolors@1.1.1": {
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
},
"postcss@8.4.49": {
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
"dependencies": [
"nanoid",
"picocolors",
"source-map-js"
]
},
"postgres-array@2.0.0": {
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="
},
"postgres-array@3.0.2": {
"integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog=="
},
"postgres-bytea@1.0.0": {
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="
},
"postgres-bytea@3.0.0": {
"integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==",
"dependencies": [
"obuf"
]
},
"postgres-date@1.0.7": {
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="
},
"postgres-date@2.1.0": {
"integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA=="
},
"postgres-interval@1.2.0": {
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"dependencies": [
"xtend"
]
},
"postgres-interval@3.0.0": {
"integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw=="
},
"postgres-range@1.1.4": {
"integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w=="
},
"resolve-pkg-maps@1.0.0": {
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="
},
"rollup@4.28.1": {
"integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==",
"dependencies": [
"@rollup/rollup-android-arm-eabi",
"@rollup/rollup-android-arm64",
"@rollup/rollup-darwin-arm64",
"@rollup/rollup-darwin-x64",
"@rollup/rollup-freebsd-arm64",
"@rollup/rollup-freebsd-x64",
"@rollup/rollup-linux-arm-gnueabihf",
"@rollup/rollup-linux-arm-musleabihf",
"@rollup/rollup-linux-arm64-gnu",
"@rollup/rollup-linux-arm64-musl",
"@rollup/rollup-linux-loongarch64-gnu",
"@rollup/rollup-linux-powerpc64le-gnu",
"@rollup/rollup-linux-riscv64-gnu",
"@rollup/rollup-linux-s390x-gnu",
"@rollup/rollup-linux-x64-gnu",
"@rollup/rollup-linux-x64-musl",
"@rollup/rollup-win32-arm64-msvc",
"@rollup/rollup-win32-ia32-msvc",
"@rollup/rollup-win32-x64-msvc",
"@types/estree",
"fsevents"
]
},
"siginfo@2.0.0": {
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="
},
"source-map-js@1.2.1": {
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
},
"source-map-support@0.5.21": {
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dependencies": [
"buffer-from",
"source-map"
]
},
"source-map@0.6.1": {
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
},
"split2@4.2.0": {
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="
},
"stackback@0.0.2": {
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="
},
"std-env@3.8.0": {
"integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w=="
},
"tinybench@2.9.0": {
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="
},
"tinyexec@0.3.1": {
"integrity": "sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ=="
},
"tinypool@1.0.2": {
"integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA=="
},
"tinyrainbow@1.2.0": {
"integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ=="
},
"tinyspy@3.0.2": {
"integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q=="
},
"undici-types@6.19.8": {
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
},
"vite-node@2.1.8": {
"integrity": "sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==",
"dependencies": [
"cac",
"debug",
"es-module-lexer",
"pathe",
"vite"
]
},
"vite@5.4.11": {
"integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==",
"dependencies": [
"esbuild@0.21.5",
"fsevents",
"postcss",
"rollup"
]
},
"vitest@2.1.8_vite@5.4.11": {
"integrity": "sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==",
"dependencies": [
"@vitest/expect",
"@vitest/mocker",
"@vitest/pretty-format",
"@vitest/runner",
"@vitest/snapshot",
"@vitest/spy",
"@vitest/utils",
"chai",
"debug",
"expect-type",
"magic-string",
"pathe",
"std-env",
"tinybench",
"tinyexec",
"tinypool",
"tinyrainbow",
"vite",
"vite-node",
"why-is-node-running"
]
},
"why-is-node-running@2.3.0": {
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
"dependencies": [
"siginfo",
"stackback"
]
},
"xtend@4.0.2": {
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
},
"yaml@2.6.1": {
"integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg=="
},
"zod@3.24.1": {
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="
}
},
"workspace": {
"dependencies": [
"jsr:@std/assert@1",
"npm:@asteasolutions/zod-to-openapi@^7.3.0",
"npm:@hono/swagger-ui@0.5",
"npm:@hono/zod-openapi@~0.18.3",
"npm:@types/pg@^8.11.10",
"npm:drizzle-kit@~0.30.1",
"npm:drizzle-orm@~0.38.2",
"npm:hono@^4.6.14",
"npm:pg@^8.13.1",
"npm:vitest@^2.1.8",
"npm:zod@^3.24.1"
]
}
}

11
store/drizzle.config.ts Normal file
View File

@@ -0,0 +1,11 @@
import { defineConfig } from "drizzle-kit";
export default defineConfig({
out: "./drizzle",
schema: "./src/db/schema.ts",
dialect: "postgresql",
dbCredentials: {
url: Deno.env.get("DATABASE_URL")!,
},
});

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

@@ -0,0 +1,217 @@
{
"id": "15ad729d-5756-4c06-87ed-cb8b721201f9",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"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": {
"name": "nodes",
"schema": "",
"columns": {
"id": {
"name": "id",
"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,
"notNull": true
},
"content": {
"name": "content",
"type": "bytea",
"primaryKey": false,
"notNull": true
},
"definition": {
"name": "definition",
"type": "json",
"primaryKey": false,
"notNull": true
},
"hash": {
"name": "hash",
"type": "varchar(16)",
"primaryKey": false,
"notNull": true
},
"previous": {
"name": "previous",
"type": "varchar(16)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"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": {},
"uniqueConstraints": {
"nodes_hash_unique": {
"name": "nodes_hash_unique",
"nullsNotDistinct": false,
"columns": [
"hash"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1734703963242,
"tag": "0000_known_kid_colt",
"breakpoints": true
}
]
}

1
store/openapi.json Normal file

File diff suppressed because one or more lines are too long

22
store/src/db/db.ts Normal file
View File

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

2
store/src/db/schema.ts Normal file
View File

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

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

@@ -0,0 +1,352 @@
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
import { HTTPException } from "hono/http-exception";
import { idRegex, NodeDefinitionSchema } from "./validations/types.ts";
import * as service from "./node.service.ts";
import { bodyLimit } from "hono/body-limit";
import { ZodSchema } from "zod";
import { CustomError } from "./errors.ts";
const nodeRouter = new OpenAPIHono();
const createParamSchema = (name: string) =>
z
.string()
.min(3)
.max(20)
.refine(
(value) => idRegex.test(value),
`${name} must contain only letters, numbers, "-", or "_"`,
)
.openapi({ param: { name, in: "path" } });
const createResponseSchema = <T extends ZodSchema>(
description: string,
schema: T,
) => ({
200: {
content: { "application/json": { schema } },
description,
},
});
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",
path: "/{user}.json",
request: {
params: z.object({
user: createParamSchema("user").optional(),
}),
},
responses: createResponseSchema(
"Retrieve nodes for a user",
z.array(NodeDefinitionSchema),
),
}),
async (c) => {
const user = c.req.param("user.json").replace(/\.json$/, "");
try {
const nodes = await service.getNodeDefinitionsByUser(user);
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" });
}
},
);
nodeRouter.openapi(
createRoute({
method: "get",
path: "/{user}/{system}.json",
request: {
params: z.object({
user: createParamSchema("user"),
system: createParamSchema("system").optional(),
}),
},
responses: createResponseSchema(
"Retrieve nodes for a system",
z.array(NodeDefinitionSchema),
),
}),
async (c) => {
const { user } = c.req.valid("param");
const system = c.req.param("system.json").replace(/\.json$/, "");
console.log("Get Nodes by System", { user, system });
try {
const nodes = await service.getNodesBySystem(user, system);
return c.json({
id: `${user}/${system}`,
nodes: nodes.map((n) => ({ id: n.id.split("@")[0] })),
});
} 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}.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);
} 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}.wasm",
request: {
params: z.object({
user: createParamSchema("user"),
system: createParamSchema("system"),
nodeId: createParamSchema("nodeId").optional(),
}),
},
responses: {
200: {
content: { "application/wasm": { schema: z.any() } },
description: "Retrieve a node's WASM file",
},
},
}),
async (c) => {
const { user, system } = c.req.valid("param");
const nodeId = c.req.param("nodeId.wasm");
console.log("Get NodeWasm 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}.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 };

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

@@ -0,0 +1,241 @@
import { db } from "../../db/db.ts";
import { nodeTable } from "./node.schema.ts";
import { NodeDefinition, NodeDefinitionSchema } from "./validations/types.ts";
import { and, asc, eq } from "drizzle-orm";
import { createHash } from "node:crypto";
import { extractDefinition } from "./worker/index.ts";
import { InvalidNodeDefinitionError, NodeNotFoundError } from "./errors.ts";
export type CreateNodeDTO = {
id: string;
system: string;
user: string;
content: ArrayBuffer;
};
function getNodeHash(content: Uint8Array) {
const hash = createHash("sha256");
hash.update(content);
return hash.digest("hex").slice(0, 16);
}
export async function createNode(
wasmBuffer: ArrayBuffer,
content: Uint8Array,
): Promise<NodeDefinition> {
const def = await extractDefinition(wasmBuffer);
const [userId, systemId, nodeId] = def.id.split("/");
const hash = getNodeHash(content);
const node: typeof nodeTable.$inferInsert = {
userId,
systemId,
nodeId,
definition: def,
hash,
content: content,
};
const previousNode = await db
.select({ hash: nodeTable.hash })
.from(nodeTable)
.orderBy(asc(nodeTable.createdAt))
.limit(1);
if (previousNode[0]) {
node.previous = previousNode[0].hash;
}
await db.insert(nodeTable).values(node);
return def;
}
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(
username: string,
systemId: string,
): Promise<NodeDefinition[]> {
const nodes = await db
.selectDistinctOn(
[nodeTable.userId, nodeTable.systemId, nodeTable.nodeId],
{ definition: nodeTable.definition, hash: nodeTable.hash },
)
.from(nodeTable)
.where(
and(eq(nodeTable.systemId, systemId), eq(nodeTable.userId, username)),
)
.orderBy(nodeTable.userId, nodeTable.systemId, nodeTable.nodeId);
const definitions = nodes
.map(
(node) =>
[NodeDefinitionSchema.safeParse(node.definition), node.hash] as const,
)
.filter(([v]) => v.success)
.map(([v, hash]) => ({
...v.data,
// id: v?.data?.id + "@" + hash,
}));
return definitions;
}
export async function getNodeWasmById(
userName: string,
systemId: string,
nodeId: string,
) {
const node = await db
.select({ content: nodeTable.content })
.from(nodeTable)
.where(
and(
eq(nodeTable.userId, userName),
eq(nodeTable.systemId, systemId),
eq(nodeTable.nodeId, nodeId),
),
)
.orderBy(asc(nodeTable.createdAt))
.limit(1);
if (!node[0]) {
throw new NodeNotFoundError();
}
return node[0].content;
}
export async function getNodeDefinitionById(
userName: string,
systemId: string,
nodeId: string,
) {
const node = await db
.select({
definition: nodeTable.definition,
hash: nodeTable.hash,
})
.from(nodeTable)
.where(
and(
eq(nodeTable.userId, userName),
eq(nodeTable.systemId, systemId),
eq(nodeTable.nodeId, nodeId),
),
)
.orderBy(asc(nodeTable.createdAt))
.limit(1);
if (!node[0]) {
throw new NodeNotFoundError();
}
const definition = NodeDefinitionSchema.safeParse(node[0]?.definition);
if (!definition.success) {
throw new InvalidNodeDefinitionError();
}
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

@@ -0,0 +1,11 @@
import { expect } from "jsr:@std/expect";
import { router } from "../router.ts";
Deno.test("simple test", async () => {
const res = await router.request("/max/plants/test.json");
const json = await res.text();
expect(true).toEqual(true);
expect(json).toEqual({ hello: "world" });
});

View File

@@ -0,0 +1,81 @@
import { z } from "@hono/zod-openapi";
const DefaultOptionsSchema = z.object({
internal: z.boolean().optional(),
external: z.boolean().optional(),
setting: z.string().optional(),
label: z.string().optional(),
description: z.string().optional(),
accepts: z.array(z.string()).optional(),
hidden: z.boolean().optional(),
});
const NodeInputFloatSchema = z.object({
...DefaultOptionsSchema.shape,
type: z.literal("float"),
element: z.literal("slider").optional(),
value: z.number().optional(),
min: z.number().optional(),
max: z.number().optional(),
step: z.number().optional(),
});
const NodeInputIntegerSchema = z.object({
...DefaultOptionsSchema.shape,
type: z.literal("integer"),
element: z.literal("slider").optional(),
value: z.number().optional(),
min: z.number().optional(),
max: z.number().optional(),
});
const NodeInputBooleanSchema = z.object({
...DefaultOptionsSchema.shape,
type: z.literal("boolean"),
value: z.boolean().optional(),
});
const NodeInputSelectSchema = z.object({
...DefaultOptionsSchema.shape,
type: z.literal("select"),
options: z.array(z.string()).optional(),
value: z.number().optional(),
});
const NodeInputSeedSchema = z.object({
...DefaultOptionsSchema.shape,
type: z.literal("seed"),
value: z.number().optional(),
});
const NodeInputVec3Schema = z.object({
...DefaultOptionsSchema.shape,
type: z.literal("vec3"),
value: z.array(z.number()).optional(),
});
const NodeInputGeometrySchema = z.object({
...DefaultOptionsSchema.shape,
type: z.literal("geometry"),
});
const NodeInputPathSchema = z.object({
...DefaultOptionsSchema.shape,
type: z.literal("path"),
});
export const NodeInputSchema = z
.union([
NodeInputSeedSchema,
NodeInputBooleanSchema,
NodeInputFloatSchema,
NodeInputIntegerSchema,
NodeInputSelectSchema,
NodeInputSeedSchema,
NodeInputVec3Schema,
NodeInputGeometrySchema,
NodeInputPathSchema,
])
.openapi("NodeInput");
export type NodeInput = z.infer<typeof NodeInputSchema>;

View File

@@ -0,0 +1,31 @@
import { z } from "zod";
import { NodeInputSchema } from "./inputs.ts";
export type NodeId = `${string}/${string}/${string}`;
export const idRegex = /[a-z0-9-]+/i;
const idSchema = z
.string()
.regex(
new RegExp(
`^(${idRegex.source})/(${idRegex.source})/(${idRegex.source})$`,
),
"Invalid id format",
);
export const NodeDefinitionSchema = z
.object({
id: idSchema,
inputs: z.record(NodeInputSchema).optional(),
outputs: z.array(z.string()).optional(),
meta: z
.object({
description: z.string().optional(),
title: z.string().optional(),
})
.optional(),
})
.openapi("NodeDefinition");
export type NodeDefinition = z.infer<typeof NodeDefinitionSchema>;

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());
}
};
});
}

Some files were not shown because too many files have changed in this diff Show More