Merge pull request 'chore: setup linting' (#29) from chore/linting into main

Reviewed-on: #29
This commit was merged in pull request #29.
This commit is contained in:
2026-02-02 16:28:42 +01:00
174 changed files with 6017 additions and 5109 deletions

View File

@@ -10,38 +10,33 @@
"json": {
// https://dprint.dev/plugins/json/config/
},
"markdown": {
},
"toml": {
},
"dockerfile": {
},
"ruff": {
},
"jupyter": {
},
"malva": {
},
"markdown": {},
"toml": {},
"dockerfile": {},
"ruff": {},
"jupyter": {},
"malva": {},
"markup": {
// https://dprint.dev/plugins/markup_fmt/config/
"scriptIndent": true,
"styleIndent": true,
},
"yaml": {
},
"graphql": {
},
"yaml": {},
"graphql": {},
"exec": {
"cwd": "${configDir}",
"commands": [{
"command": "rustfmt",
"exts": ["rs"],
"cacheKeyFiles": [
"rustfmt.toml",
"rust-toolchain.toml",
],
}],
"commands": [
{
"command": "rustfmt",
"exts": [
"rs",
],
"cacheKeyFiles": [
"rustfmt.toml",
"rust-toolchain.toml",
],
},
],
},
"excludes": [
"**/node_modules",

View File

@@ -1,38 +1,82 @@
name: Deploy to GitHub Pages
name: 🏗️ Build and Deploy
on:
push:
branches: "main"
branches: ["*"]
env:
PNPM_CACHE_FOLDER: .pnpm-store
x-templates:
setup-steps: &setup-steps
- name: 📑 Checkout Code
uses: actions/checkout@v4
- name: 💾 Setup pnpm Cache
uses: actions/cache@v4
with:
path: ${{ env.PNPM_CACHE_FOLDER }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: 📦 Install Dependencies
run: pnpm install --frozen-lockfile --store-dir ${{ env.PNPM_CACHE_FOLDER }}
jobs:
build_site:
lint:
runs-on: ubuntu-latest
container: jimfx/nodes:latest
steps:
- name: Checkout
uses: actions/checkout@v4
- <<: *setup-steps
- name: 🧹 Run Linter
run: pnpm lint
- name: Install dependencies
run: pnpm install --frozen-lockfile
format:
runs-on: ubuntu-latest
container: jimfx/nodes:latest
steps:
- <<: *setup-steps
- name: 🎨 Check Formatting
run: pnpm format:check
- name: build
typecheck:
runs-on: ubuntu-latest
container: jimfx/nodes:latest
steps:
- <<: *setup-steps
- name: 🧬 Type Check
run: pnpm check
build_and_deploy:
if: github.ref == 'refs/heads/main'
needs: [lint, format, typecheck]
runs-on: ubuntu-latest
container: jimfx/nodes:latest
steps:
- <<: *setup-steps
- name: 🛠️ Build Site
run: pnpm run build:deploy
- name: 🔑 Configure rclone
run: |
echo "$SSH_PRIVATE_KEY" > /tmp/id_rsa
chmod 600 /tmp/id_rsa
mkdir -p ~/.config/rclone
echo -e "[sftp-remote]\ntype = sftp\nhost = ${SSH_HOST}\nuser = ${SSH_USER}\nport = ${SSH_PORT}\nkey_file = /tmp/id_rsa" > ~/.config/rclone/rclone.conf
cat <<EOF > ~/.config/rclone/rclone.conf
[sftp-remote]
type = sftp
host = ${SSH_HOST}
user = ${SSH_USER}
port = ${SSH_PORT}
key_use_agent = false
key_data = ${SSH_PRIVATE_KEY}
EOF
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_HOST: ${{ vars.SSH_HOST }}
SSH_PORT: ${{ vars.SSH_PORT }}
SSH_USER: ${{ vars.SSH_USER }}
- name: 🚀 Deploy Changed Files via rclone
- name: 🚚 Deploy via rclone
run: |
echo "Uploading the rest"
rclone sync --update -v --progress --exclude _astro/** --stats 2s --stats-one-line ./app/build/ sftp-remote:${REMOTE_DIR} --transfers 4
rclone sync --update -v --progress --exclude "_astro/**" --stats 2s --stats-one-line ./app/build/ sftp-remote:${REMOTE_DIR} --transfers 4
env:
REMOTE_DIR: ${{ vars.REMOTE_DIR }}

View File

@@ -25,18 +25,7 @@ FROM nginx:alpine AS runner
RUN rm /etc/nginx/conf.d/default.conf
COPY <<EOF /etc/nginx/conf.d/app.conf
server {
listen 80;
server_name _;
root /app;
index index.html;
location / {
try_files \$uri \$uri/ /index.html;
}
}
EOF
COPY app/docker/app.conf /etc/nginx/conf.d/app.conf
COPY --from=builder /app/app/build /app

10
app/docker/app.conf Normal file
View File

@@ -0,0 +1,10 @@
server {
listen 80;
server_name _;
root /app;
index index.html;
location / {
try_files \$uri \$uri/ /index.html;
}
}

37
app/eslint.config.mjs Normal file
View File

@@ -0,0 +1,37 @@
import { includeIgnoreFile } from '@eslint/compat';
import js from '@eslint/js';
import svelte from 'eslint-plugin-svelte';
import { defineConfig } from 'eslint/config';
import globals from 'globals';
import path from 'node:path';
import ts from 'typescript-eslint';
import svelteConfig from './svelte.config.js';
const gitignorePath = path.resolve(import.meta.dirname, '.gitignore');
export default defineConfig(
includeIgnoreFile(gitignorePath),
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs.recommended,
{
languageOptions: { globals: { ...globals.browser, ...globals.node } },
rules: {
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
'no-undef': 'off'
}
},
{
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
languageOptions: {
parserOptions: {
projectService: true,
extraFileExtensions: ['.svelte'],
parser: ts.parser,
tsconfigRootDir: import.meta.dirname,
svelteConfig
}
}
}
);

View File

@@ -7,10 +7,13 @@
"dev": "vite dev",
"build": "svelte-kit sync && vite build",
"test": "vitest",
"preview": "vite preview"
"preview": "vite preview",
"format": "dprint fmt -c '../.dprint.jsonc' .",
"format:check": "dprint check -c '../.dprint.jsonc' .",
"lint": "eslint .",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
},
"dependencies": {
"@nodarium/registry": "workspace:*",
"@nodarium/ui": "workspace:*",
"@nodarium/utils": "workspace:*",
"@sveltejs/kit": "^2.50.0",
@@ -26,6 +29,8 @@
"wabt": "^1.0.39"
},
"devDependencies": {
"@eslint/compat": "^2.0.2",
"@eslint/js": "^9.39.2",
"@iconify-json/tabler": "^1.2.26",
"@iconify/tailwind4": "^1.2.1",
"@nodarium/types": "workspace:",
@@ -34,10 +39,15 @@
"@tsconfig/svelte": "^5.0.6",
"@types/file-saver": "^2.0.7",
"@types/three": "^0.182.0",
"dprint": "^0.51.1",
"eslint": "^9.39.2",
"eslint-plugin-svelte": "^3.14.0",
"globals": "^17.3.0",
"svelte": "^5.46.4",
"svelte-check": "^4.3.5",
"tslib": "^2.8.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.54.0",
"vite": "^7.3.1",
"vite-plugin-comlink": "^5.3.0",
"vite-plugin-glsl": "^1.5.5",

View File

@@ -2,5 +2,5 @@
@source "../../packages/ui/**/*.svelte";
@plugin "@iconify/tailwind4" {
prefix: "i";
icon-sets: from-folder(custom, "./src/lib/icons")
};
icon-sets: from-folder(custom, "./src/lib/icons");
}

14
app/src/app.d.ts vendored
View File

@@ -1,13 +1,13 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

View File

@@ -1,28 +1,26 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/svelte.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
<title>Nodes</title>
<script>
var store = localStorage.getItem('node-settings');
if (store) {
try {
var value = JSON.parse(store);
var themes = ['dark', 'light', 'catppuccin'];
if (themes[value.theme]) {
document.documentElement.classList.add('theme-' + themes[value.theme]);
}
} catch (e) {}
}
</script>
</head>
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/svelte.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
<title>Nodes</title>
<script>
var store = localStorage.getItem("node-settings");
if (store) {
try {
var value = JSON.parse(store);
var themes = ["dark", "light", "catppuccin"];
if (themes[value.theme]) {
document.documentElement.classList.add("theme-" + themes[value.theme]);
}
} catch (e) { }
}
</script>
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -1,2 +1,2 @@
import { PUBLIC_ANALYTIC_SCRIPT } from "$env/static/public";
import { PUBLIC_ANALYTIC_SCRIPT } from '$env/static/public';
export const ANALYTIC_SCRIPT = PUBLIC_ANALYTIC_SCRIPT;

View File

@@ -1,9 +1,9 @@
<script lang="ts">
import { T } from "@threlte/core";
import BackgroundVert from "./Background.vert";
import BackgroundFrag from "./Background.frag";
import { colors } from "../graph/colors.svelte";
import { appSettings } from "$lib/settings/app-settings.svelte";
import { T } from "@threlte/core";
import { colors } from "../graph/colors.svelte";
import BackgroundFrag from "./Background.frag";
import BackgroundVert from "./Background.vert";
type Props = {
minZoom: number;

View File

@@ -116,7 +116,7 @@
</div>
<div class="content">
{#each nodes as node}
{#each nodes as node (node.id)}
<div
class="result"
role="treeitem"

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { HTML } from "@threlte/extras";
import { HTML } from '@threlte/extras';
type Props = {
p1: { x: number; y: number };
@@ -10,7 +10,7 @@
const {
p1 = { x: 0, y: 0 },
p2 = { x: 0, y: 0 },
cameraPosition = [0, 1, 0],
cameraPosition = [0, 1, 0]
}: Props = $props();
const width = $derived(Math.abs(p1.x - p2.x) * cameraPosition[2]);
@@ -24,7 +24,8 @@
<div
class="box-selection"
style={`width: ${width}px; height: ${height}px;`}
></div>
>
</div>
</HTML>
<style>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { T } from "@threlte/core";
import { type OrthographicCamera } from "three";
import { T } from '@threlte/core';
import { type OrthographicCamera } from 'three';
type Props = {
camera: OrthographicCamera;
position: [number, number, number];

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import type { NodeDefinition, NodeRegistry } from "@nodarium/types";
import { onMount } from "svelte";
import type { NodeDefinition, NodeRegistry } from '@nodarium/types';
import { onMount } from 'svelte';
let mx = $state(0);
let my = $state(0);
@@ -20,15 +20,15 @@
my = ev.clientY;
if (!target) return;
const closest = target?.closest?.("[data-node-type]");
const closest = target?.closest?.('[data-node-type]');
if (!closest) {
node = undefined;
return;
}
let nodeType = closest.getAttribute("data-node-type");
let nodeInput = closest.getAttribute("data-node-input");
let nodeType = closest.getAttribute('data-node-type');
let nodeInput = closest.getAttribute('data-node-input');
if (!nodeType) {
node = undefined;
@@ -40,9 +40,9 @@
onMount(() => {
const style = wrapper.parentElement?.style;
style?.setProperty("cursor", "help");
style?.setProperty('cursor', 'help');
return () => {
style?.removeProperty("cursor");
style?.removeProperty('cursor');
};
});
</script>
@@ -53,12 +53,12 @@
class="help-wrapper p-4"
class:visible={node}
bind:clientWidth={width}
style="--my:{my}px; --mx:{Math.min(mx, window.innerWidth - width - 20)}px;"
style="--my: {my}px; --mx: {Math.min(mx, window.innerWidth - width - 20)}px"
bind:this={wrapper}
>
<p class="m-0 text-light opacity-40 flex items-center gap-3 mb-4">
<span class="i-tabler-help block w-4 h-4"></span>
{node?.id.split("/").at(-1) || "Help"}
{node?.id.split('/').at(-1) || 'Help'}
{#if input}
<span>> {input}</span>
{/if}
@@ -77,7 +77,7 @@
{#if !input}
<div>
<span class="i-tabler-arrow-right opacity-30">-></span>
{node?.outputs?.map((o) => o).join(", ") ?? "nothing"}
{node?.outputs?.map((o) => o).join(', ') ?? 'nothing'}
</div>
{/if}
{/if}

View File

@@ -1,11 +1,32 @@
<script lang="ts">
import { MeshLineGeometry, MeshLineMaterial } from "@threlte/extras";
import { points, lines, rects } from "./store.js";
import { T } from "@threlte/core";
import { Color } from "three";
import type { Box } from '@nodarium/types';
import { T } from '@threlte/core';
import { MeshLineGeometry, MeshLineMaterial } from '@threlte/extras';
import { Color, Vector3 } from 'three';
import { lines, points, rects } from './store.js';
type Line = {
points: Vector3[];
color?: Color;
};
function getEachKey(value: Vector3 | Box | Line): string {
if ('x' in value) {
return [value.x, value.y, value.z].join('-');
}
if ('minX' in value) {
return [value.maxX, value.minX, value.maxY, value.minY].join('-');
}
if ('points' in value) {
return getEachKey(value.points[Math.floor(value.points.length / 2)]);
}
return '';
}
</script>
{#each $points as point}
{#each $points as point (getEachKey(point))}
<T.Mesh
position.x={point.x}
position.y={point.y}
@@ -17,7 +38,7 @@
</T.Mesh>
{/each}
{#each $rects as rect, i}
{#each $rects as rect, i (getEachKey(rect))}
<T.Mesh
position.x={(rect.minX + rect.maxX) / 2}
position.y={0}
@@ -32,11 +53,11 @@
</T.Mesh>
{/each}
{#each $lines as line}
{#each $lines as line (getEachKey(line))}
<T.Mesh position.y={1}>
<MeshLineGeometry points={line.points} />
<MeshLineMaterial
color={line.color || "red"}
color={line.color || 'red'}
linewidth={1}
attenuate={false}
/>

View File

@@ -1,16 +1,18 @@
<script module lang="ts">
import { colors } from "../graph/colors.svelte";
import { colors } from '../graph/colors.svelte';
const circleMaterial = new MeshBasicMaterial({
color: colors.edge.clone(),
toneMapped: false,
toneMapped: false
});
let lineColor = $state(colors.edge.clone().convertSRGBToLinear());
$effect.root(() => {
$effect(() => {
appSettings.value.theme;
if (appSettings.value.theme === undefined) {
return;
}
circleMaterial.color = colors.edge.clone().convertSRGBToLinear();
lineColor = colors.edge.clone().convertSRGBToLinear();
});
@@ -20,19 +22,19 @@
new Vector2(0, 0),
new Vector2(0, 0),
new Vector2(0, 0),
new Vector2(0, 0),
new Vector2(0, 0)
);
</script>
<script lang="ts">
import { T } from "@threlte/core";
import { MeshLineGeometry, MeshLineMaterial } from "@threlte/extras";
import { MeshBasicMaterial, Vector3 } from "three";
import { CubicBezierCurve } from "three/src/extras/curves/CubicBezierCurve.js";
import { Vector2 } from "three/src/math/Vector2.js";
import { appSettings } from "$lib/settings/app-settings.svelte";
import { getGraphState } from "../graph-state.svelte";
import { onDestroy } from "svelte";
import { appSettings } from '$lib/settings/app-settings.svelte';
import { T } from '@threlte/core';
import { MeshLineGeometry, MeshLineMaterial } from '@threlte/extras';
import { onDestroy } from 'svelte';
import { MeshBasicMaterial, Vector3 } from 'three';
import { CubicBezierCurve } from 'three/src/extras/curves/CubicBezierCurve.js';
import { Vector2 } from 'three/src/math/Vector2.js';
import { getGraphState } from '../graph-state.svelte';
const graphState = getGraphState();
@@ -63,7 +65,7 @@
lastId = curveId;
const length = Math.floor(
Math.sqrt(Math.pow(new_x, 2) + Math.pow(new_y, 2)) / 4,
Math.sqrt(Math.pow(new_x, 2) + Math.pow(new_y, 2)) / 4
);
const samples = Math.max(length * 16, 10);
@@ -83,7 +85,7 @@
id,
x1,
y1,
$state.snapshot(points) as unknown as Vector3[],
$state.snapshot(points) as unknown as Vector3[]
);
}
}

View File

@@ -1,23 +1,23 @@
export const setXYZXYZ = (array: number[], location: number, x: number, y: number, z: number) => {
array[location + 0] = x
array[location + 1] = y
array[location + 2] = z
array[location + 0] = x;
array[location + 1] = y;
array[location + 2] = z;
array[location + 3] = x
array[location + 4] = y
array[location + 5] = z
}
array[location + 3] = x;
array[location + 4] = y;
array[location + 5] = z;
};
export const setXY = (array: number[], location: number, x: number, y: number) => {
array[location + 0] = x
array[location + 1] = y
}
array[location + 0] = x;
array[location + 1] = y;
};
export const setXYZ = (array: number[], location: number, x: number, y: number, z: number) => {
array[location + 0] = x
array[location + 1] = y
array[location + 2] = z
}
array[location + 0] = x;
array[location + 1] = y;
array[location + 2] = z;
};
export const setXYZW = (
array: number[],
@@ -27,8 +27,8 @@ export const setXYZW = (
z: number,
w: number
) => {
array[location + 0] = x
array[location + 1] = y
array[location + 2] = z
array[location + 3] = w
}
array[location + 0] = x;
array[location + 1] = y;
array[location + 2] = z;
array[location + 3] = w;
};

View File

@@ -1,5 +1,5 @@
import throttle from '$lib/helpers/throttle';
import { RemoteNodeRegistry } from '@nodarium/registry';
import { RemoteNodeRegistry } from '$lib/node-registry/index';
import type {
Edge,
Graph,
@@ -12,7 +12,7 @@ import type {
} from '@nodarium/types';
import { fastHashString } from '@nodarium/utils';
import { createLogger } from '@nodarium/utils';
import { SvelteMap } from 'svelte/reactivity';
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
import EventEmitter from './helpers/EventEmitter';
import { HistoryManager } from './history-manager';
@@ -23,7 +23,7 @@ const remoteRegistry = new RemoteNodeRegistry('');
const clone = 'structuredClone' in self
? self.structuredClone
: (args: any) => JSON.parse(JSON.stringify(args));
: (args: unknown) => JSON.parse(JSON.stringify(args));
function areSocketsCompatible(
output: string | undefined,
@@ -57,7 +57,7 @@ function areEdgesEqual(firstEdge: Edge, secondEdge: Edge) {
export class GraphManager extends EventEmitter<{
save: Graph;
result: any;
result: unknown;
settings: {
types: Record<string, NodeInput>;
values: Record<string, unknown>;
@@ -79,7 +79,7 @@ export class GraphManager extends EventEmitter<{
currentUndoGroup: number | null = null;
inputSockets = $derived.by(() => {
const s = new Set<string>();
const s = new SvelteSet<string>();
for (const edge of this.edges) {
s.add(`${edge[2].id}-${edge[3]}`);
}
@@ -122,7 +122,7 @@ export class GraphManager extends EventEmitter<{
private lastSettingsHash = 0;
setSettings(settings: Record<string, unknown>) {
let hash = fastHashString(JSON.stringify(settings));
const hash = fastHashString(JSON.stringify(settings));
if (hash === this.lastSettingsHash) return;
this.lastSettingsHash = hash;
@@ -136,7 +136,7 @@ export class GraphManager extends EventEmitter<{
}
getLinkedNodes(node: NodeInstance) {
const nodes = new Set<NodeInstance>();
const nodes = new SvelteSet<NodeInstance>();
const stack = [node];
while (stack.length) {
const n = stack.pop();
@@ -171,7 +171,7 @@ export class GraphManager extends EventEmitter<{
const targetInput = toNode.state?.type?.inputs?.[toSocketKey];
const targetAcceptedTypes = [targetInput?.type, ...(targetInput?.accepts || [])];
const bestInputEntry = draggedInputs.find(([_, input]) => {
const bestInputEntry = draggedInputs.find(([, input]) => {
const accepted = [input.type, ...(input.accepts || [])];
return areSocketsCompatible(edgeOutputSocketType, accepted);
});
@@ -209,7 +209,7 @@ export class GraphManager extends EventEmitter<{
const draggedOutputs = draggedNode.state.type.outputs ?? [];
// Optimization: Pre-calculate parents to avoid cycles
const parentIds = new Set(this.getParentsOfNode(draggedNode).map(n => n.id));
const parentIds = new SvelteSet(this.getParentsOfNode(draggedNode).map(n => n.id));
return this.edges.filter((edge) => {
const [fromNode, fromSocketIdx, toNode, toSocketKey] = edge;
@@ -266,7 +266,7 @@ export class GraphManager extends EventEmitter<{
}
private _init(graph: Graph) {
const nodes = new Map(
const nodes = new SvelteMap(
graph.nodes.map((node) => {
const nodeType = this.registry.getNode(node.type);
const n = node as NodeInstance;
@@ -310,11 +310,11 @@ export class GraphManager extends EventEmitter<{
logger.info('loading graph', { nodes: graph.nodes, edges: graph.edges, id: graph.id });
const nodeIds = Array.from(new Set([...graph.nodes.map((n) => n.type)]));
const nodeIds = Array.from(new SvelteSet([...graph.nodes.map((n) => n.type)]));
await this.registry.load(nodeIds);
// Fetch all nodes from all collections of the loaded nodes
const allCollections = new Set<`${string}/${string}`>();
const allCollections = new SvelteSet<`${string}/${string}`>();
for (const id of nodeIds) {
const [user, collection] = id.split('/');
allCollections.add(`${user}/${collection}`);
@@ -354,7 +354,7 @@ export class GraphManager extends EventEmitter<{
for (const type of types) {
if (type.inputs) {
for (const key in type.inputs) {
let settingId = type.inputs[key].setting;
const settingId = type.inputs[key].setting;
if (settingId) {
settingTypes[settingId] = {
__node_type: type.id,
@@ -409,7 +409,7 @@ export class GraphManager extends EventEmitter<{
const settingValues = this.settings;
if (nodeType.inputs) {
for (const key in nodeType.inputs) {
let settingId = nodeType.inputs[key].setting;
const settingId = nodeType.inputs[key].setting;
if (settingId) {
settingTypes[settingId] = nodeType.inputs[key];
if (
@@ -507,7 +507,7 @@ export class GraphManager extends EventEmitter<{
createGraph(nodes: NodeInstance[], edges: [number, number, number, string][]) {
// map old ids to new ids
const idMap = new Map<number, number>();
const idMap = new SvelteMap<number, number>();
let startId = this.createNodeId();
@@ -731,7 +731,7 @@ export class GraphManager extends EventEmitter<{
// 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.getChildren(node).map((n) => n.id));
const children = new SvelteSet(this.getChildren(node).map((n) => n.id));
const nodes = this.getAllNodes().filter(
(n) => n.id !== node.id && !children.has(n.id)
);
@@ -752,13 +752,13 @@ export class GraphManager extends EventEmitter<{
// 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 parents = new SvelteSet(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(
const edges = new SvelteMap(
this.getEdgesFromNode(node)
.filter((e) => e[1] === index)
.map((e) => [e[2].id, e[3]])

View File

@@ -1,6 +1,6 @@
import type { NodeInstance, Socket } from '@nodarium/types';
import { getContext, setContext } from 'svelte';
import { SvelteSet } from 'svelte/reactivity';
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
import type { OrthographicCamera, Vector3 } from 'three';
import type { GraphManager } from './graph-manager.svelte';
@@ -54,7 +54,7 @@ export class GraphState {
height = $state(100);
hoveredEdgeId = $state<string | null>(null);
edges = new Map<string, EdgeData>();
edges = new SvelteMap<string, EdgeData>();
wrapper = $state<HTMLDivElement>(null!);
rect: DOMRect = $derived(
@@ -100,7 +100,7 @@ export class GraphState {
hoveredSocket = $state<Socket | null>(null);
possibleSockets = $state<Socket[]>([]);
possibleSocketIds = $derived(
new Set(this.possibleSockets.map((s) => `${s.node.id}-${s.index}`))
new SvelteSet(this.possibleSockets.map((s) => `${s.node.id}-${s.index}`))
);
getEdges() {
@@ -155,7 +155,6 @@ export class GraphState {
return 4;
} else if (z > 11) {
return 2;
} else {
}
return 1;
}
@@ -193,7 +192,7 @@ export class GraphState {
(p) =>
p !== 'seed'
&& node?.inputs
&& !('setting' in node?.inputs?.[p])
&& !(node?.inputs?.[p] !== undefined && 'setting' in node.inputs[p])
&& node.inputs[p].hidden !== true
).length;
this.nodeHeightCache[nodeTypeId] = height;
@@ -294,8 +293,8 @@ export class GraphState {
getNodeIdFromEvent(event: MouseEvent) {
let clickedNodeId = -1;
let mx = event.clientX - this.rect.x;
let my = event.clientY - this.rect.y;
const mx = event.clientX - this.rect.x;
const my = event.clientY - this.rect.y;
if (event.button === 0) {
// check if the clicked element is a node

View File

@@ -74,7 +74,7 @@
const output = newNode.state?.type?.outputs?.find((out) => {
if (socketType?.type === out) return true;
if (socketType?.accepts?.includes(out as any)) return true;
if ((socketType?.accepts as string[])?.includes(out)) return true;
return false;
});
@@ -172,7 +172,7 @@
/>
{/if}
{#each graph.edges as edge}
{#each graph.edges as edge (edge)}
{@const [x1, y1, x2, y2] = getEdgePosition(edge)}
<EdgeEl
id={graph.getEdgeId(edge)}

View File

@@ -1,29 +1,25 @@
<script lang="ts">
import type { Graph, NodeInstance, NodeRegistry } from "@nodarium/types";
import GraphEl from "./Graph.svelte";
import { GraphManager } from "../graph-manager.svelte";
import { createKeyMap } from "$lib/helpers/createKeyMap";
import {
GraphState,
setGraphManager,
setGraphState,
} from "../graph-state.svelte";
import { setupKeymaps } from "../keymaps";
import { createKeyMap } from '$lib/helpers/createKeyMap';
import type { Graph, NodeInstance, NodeRegistry } from '@nodarium/types';
import { GraphManager } from '../graph-manager.svelte';
import { GraphState, setGraphManager, setGraphState } from '../graph-state.svelte';
import { setupKeymaps } from '../keymaps';
import GraphEl from './Graph.svelte';
type Props = {
graph?: Graph;
registry: NodeRegistry;
settings?: Record<string, any>;
settings?: Record<string, unknown>;
activeNode?: NodeInstance;
showGrid?: boolean;
snapToGrid?: boolean;
showHelp?: boolean;
settingTypes?: Record<string, any>;
settingTypes?: Record<string, unknown>;
onsave?: (save: Graph) => void;
onresult?: (result: any) => void;
onresult?: (result: unknown) => void;
};
let {
@@ -36,11 +32,12 @@
showHelp = $bindable(false),
settingTypes = $bindable(),
onsave,
onresult,
onresult
}: Props = $props();
export const keymap = createKeyMap([]);
// svelte-ignore state_referenced_locally
export const manager = new GraphManager(registry);
setGraphManager(manager);
@@ -70,14 +67,14 @@
}
});
manager.on("settings", (_settings) => {
manager.on('settings', (_settings) => {
settingTypes = { ...settingTypes, ..._settings.types };
settings = _settings.values;
});
manager.on("result", (result) => onresult?.(result));
manager.on('result', (result) => onresult?.(result));
manager.on("save", (save) => onsave?.(save));
manager.on('save', (save) => onsave?.(save));
$effect(() => {
if (graph) {

View File

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

View File

@@ -6,7 +6,7 @@ export class FileDropEventManager {
constructor(
private graph: GraphManager,
private state: GraphState
) { }
) {}
handleFileDrop(event: DragEvent) {
event.preventDefault();
@@ -17,19 +17,21 @@ export class FileDropEventManager {
let my = event.clientY - this.state.rect.y;
if (nodeId) {
let nodeOffsetX = event.dataTransfer.getData('data/node-offset-x');
let nodeOffsetY = event.dataTransfer.getData('data/node-offset-y');
const nodeOffsetX = event.dataTransfer.getData('data/node-offset-x');
const nodeOffsetY = event.dataTransfer.getData('data/node-offset-y');
if (nodeOffsetX && nodeOffsetY) {
mx += parseInt(nodeOffsetX);
my += parseInt(nodeOffsetY);
}
let props = {};
let rawNodeProps = event.dataTransfer.getData('data/node-props');
const rawNodeProps = event.dataTransfer.getData('data/node-props');
if (rawNodeProps) {
try {
props = JSON.parse(rawNodeProps);
} catch (e) { }
} catch (e) {
console.error('Failed to parse node dropped', e);
}
}
const pos = this.state.projectScreenToWorld(mx, my);
@@ -48,7 +50,7 @@ export class FileDropEventManager {
reader.onload = async (e) => {
const buffer = e.target?.result;
if (buffer?.constructor === ArrayBuffer) {
const nodeType = await this.graph.registry.register(buffer);
const nodeType = await this.graph.registry.register(nodeId, buffer);
this.graph.createNode({
type: nodeType.id,

View File

@@ -7,7 +7,7 @@ export class EdgeInteractionManager {
constructor(
private graph: GraphManager,
private state: GraphState
) { }
) {}
private MIN_DISTANCE = 3;
@@ -85,7 +85,14 @@ export class EdgeInteractionManager {
const pointAy = edge.points[i].z + edge.y1;
const pointBx = edge.points[i + DENSITY].x + edge.x1;
const pointBy = edge.points[i + DENSITY].z + edge.y1;
const distance = distanceFromPointToSegment(pointAx, pointAy, pointBx, pointBy, mouseX, mouseY);
const distance = distanceFromPointToSegment(
pointAx,
pointAy,
pointBx,
pointBy,
mouseX,
mouseY
);
if (distance < this.MIN_DISTANCE) {
if (distance < hoveredEdgeDistance) {
hoveredEdgeDistance = distance;

View File

@@ -177,8 +177,8 @@ export class MouseEventManager {
}
}
let mx = event.clientX - this.state.rect.x;
let my = event.clientY - this.state.rect.y;
const mx = event.clientX - this.state.rect.x;
const my = event.clientY - this.state.rect.y;
this.state.mouseDown = [mx, my];
this.state.cameraDown[0] = this.state.cameraPosition[0];
@@ -242,8 +242,8 @@ export class MouseEventManager {
}
handleWindowMouseMove(event: MouseEvent) {
let mx = event.clientX - this.state.rect.x;
let my = event.clientY - this.state.rect.y;
const mx = event.clientX - this.state.rect.x;
const my = event.clientY - this.state.rect.y;
this.state.mousePosition = this.state.projectScreenToWorld(mx, my);
this.state.hoveredNodeId = this.state.getNodeIdFromEvent(event);
@@ -352,9 +352,9 @@ export class MouseEventManager {
// here we are handling panning of camera
this.state.isPanning = true;
let newX = this.state.cameraDown[0]
const newX = this.state.cameraDown[0]
- (mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
let newY = this.state.cameraDown[1]
const newY = this.state.cameraDown[1]
- (my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
this.state.cameraPosition[0] = newX;
@@ -392,6 +392,7 @@ export class MouseEventManager {
/ zoomRatio;
this.state.cameraPosition[1] = this.state.mousePosition[1]
- (this.state.mousePosition[1] - this.state.cameraPosition[1])
/ zoomRatio, this.state.cameraPosition[2] = newZoom;
/ zoomRatio;
this.state.cameraPosition[2] = newZoom;
}
}

View File

@@ -1,11 +1,11 @@
import throttle from "$lib/helpers/throttle";
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 },
T extends EventMap = { [key: string]: unknown }
> {
index = 0;
public eventMap: T = {} as T;
@@ -32,11 +32,11 @@ export default class EventEmitter<
public on<K extends EventKey<T>>(
event: K,
cb: EventReceiver<T[K]>,
throttleTimer = 0,
throttleTimer = 0
) {
if (throttleTimer > 0) cb = throttle(cb, throttleTimer);
const cbs = Object.assign(this.cbs, {
[event]: [...(this.cbs[event] || []), cb],
[event]: [...(this.cbs[event] || []), cb]
});
this.cbs = cbs;
@@ -54,10 +54,10 @@ export default class EventEmitter<
*/
public once<K extends EventKey<T>>(
event: K,
cb: EventReceiver<T[K]>,
cb: EventReceiver<T[K]>
): () => void {
const cbsOnce = Object.assign(this.cbsOnce, {
[event]: [...(this.cbsOnce[event] || []), cb],
[event]: [...(this.cbsOnce[event] || []), cb]
});
this.cbsOnce = cbsOnce;

View File

@@ -36,7 +36,8 @@ export function createNodePath({
aspectRatio = 1
} = {}) {
return `M0,${cornerTop}
${cornerTop
${
cornerTop
? ` V${cornerTop}
Q0,0 ${cornerTop * aspectRatio},0
H${100 - cornerTop * aspectRatio}
@@ -45,40 +46,37 @@ export function createNodePath({
: ` V0
H100
`
}
}
V${y - height / 2}
${rightBump
${
rightBump
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
: ` H100`
}
${cornerBottom
}
${
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}`
}
${
leftBump
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${
y - height / 2
}`
: ` H0`
}
}
Z`.replace(/\s+/g, ' ');
}
export const debounce = (fn: Function, ms = 300) => {
let timeoutId: ReturnType<typeof setTimeout>;
return function (this: any, ...args: any[]) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), ms);
};
};
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>>(
export function withSubComponents<A, B extends Record<string, unknown>>(
component: A,
subcomponents: B
): A & B {

View File

@@ -1,15 +1,14 @@
import { writable, type Writable } from "svelte/store";
import { type Writable, writable } from 'svelte/store';
function isStore(v: unknown): v is Writable<unknown> {
return v !== null && typeof v === "object" && "subscribe" in v && "set" in v;
return v !== null && typeof v === 'object' && 'subscribe' in v && 'set' in v;
}
const storeIds: Map<string, ReturnType<typeof createLocalStore>> = new Map();
const HAS_LOCALSTORAGE = "localStorage" in globalThis;
const HAS_LOCALSTORAGE = 'localStorage' in globalThis;
function createLocalStore<T>(key: string, initialValue: T | Writable<T>) {
let store: Writable<T>;
if (HAS_LOCALSTORAGE) {
@@ -36,18 +35,15 @@ function createLocalStore<T>(key: string, initialValue: T | Writable<T>) {
subscribe: store.subscribe,
set: store.set,
update: store.update
}
};
}
export default function localStore<T>(key: string, initialValue: T | Writable<T>): Writable<T> {
if (storeIds.has(key)) return storeIds.get(key) as Writable<T>;
const store = createLocalStore(key, initialValue)
const store = createLocalStore(key, initialValue);
storeIds.set(key, store);
return store
return store;
}

View File

@@ -1,21 +1,21 @@
import { create, type Delta } from "jsondiffpatch";
import type { Graph } from "@nodarium/types";
import { clone } from "./helpers/index.js";
import { createLogger } from "@nodarium/utils";
import type { Graph } from '@nodarium/types';
import { createLogger } from '@nodarium/utils';
import { create, type Delta } from 'jsondiffpatch';
import { clone } from './helpers/index.js';
const diff = create({
objectHash: function (obj, index) {
objectHash: function(obj, index) {
if (obj === null) return obj;
if ("id" in obj) return obj.id as string;
if ("_id" in obj) return obj._id as string;
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 "$$index:" + index;
},
return '$$index:' + index;
}
});
const log = createLogger("history");
const log = createLogger('history');
log.mute();
export class HistoryManager {
@@ -26,7 +26,7 @@ export class HistoryManager {
private opts = {
debounce: 400,
maxHistory: 100,
maxHistory: 100
};
constructor({ maxHistory = 100, debounce = 100 } = {}) {
@@ -40,12 +40,12 @@ export class HistoryManager {
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
@@ -61,7 +61,7 @@ export class HistoryManager {
}
this.state = newState;
} else {
log.log("no changes");
log.log('no changes');
}
}
}
@@ -75,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];
@@ -95,7 +95,7 @@ export class HistoryManager {
this.state = nextState;
return clone(nextState);
} else {
log.log("reached end");
log.log('reached end');
}
}
}

View File

@@ -1,13 +1,13 @@
<script lang="ts">
import type { NodeInstance } from "@nodarium/types";
import { getGraphState } from "../graph-state.svelte";
import { T } from "@threlte/core";
import { type Mesh } from "three";
import NodeFrag from "./Node.frag";
import NodeVert from "./Node.vert";
import NodeHtml from "./NodeHTML.svelte";
import { colors } from "../graph/colors.svelte";
import { appSettings } from "$lib/settings/app-settings.svelte";
import { appSettings } from '$lib/settings/app-settings.svelte';
import type { NodeInstance } from '@nodarium/types';
import { T } from '@threlte/core';
import { type Mesh } from 'three';
import { getGraphState } from '../graph-state.svelte';
import { colors } from '../graph/colors.svelte';
import NodeFrag from './Node.frag';
import NodeVert from './Node.vert';
import NodeHtml from './NodeHTML.svelte';
const graphState = getGraphState();
@@ -21,12 +21,12 @@
const isActive = $derived(graphState.activeNodeId === node.id);
const isSelected = $derived(graphState.selectedNodes.has(node.id));
const strokeColor = $derived(
appSettings.value.theme &&
(isSelected
appSettings.value.theme
&& (isSelected
? colors.selected
: isActive
? colors.active
: colors.outline),
? colors.active
: colors.outline)
);
let meshRef: Mesh | undefined = $state();
@@ -55,12 +55,12 @@
fragmentShader={NodeFrag}
transparent
uniforms={{
uColorBright: { value: colors["layer-2"] },
uColorDark: { value: colors["layer-1"] },
uColorBright: { value: colors['layer-2'] },
uColorDark: { value: colors['layer-1'] },
uStrokeColor: { value: colors.outline.clone() },
uStrokeWidth: { value: 1.0 },
uWidth: { value: 20 },
uHeight: { value: height },
uHeight: { value: height }
}}
uniforms.uStrokeColor.value={strokeColor.clone()}
uniforms.uStrokeWidth.value={(7 - z) / 3}

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import type { NodeInstance } from "@nodarium/types";
import NodeHeader from "./NodeHeader.svelte";
import NodeParameter from "./NodeParameter.svelte";
import { getGraphState } from "../graph-state.svelte";
import type { NodeInstance } from '@nodarium/types';
import { getGraphState } from '../graph-state.svelte';
import NodeHeader from './NodeHeader.svelte';
import NodeParameter from './NodeParameter.svelte';
let ref: HTMLDivElement;
@@ -10,7 +10,7 @@
type Props = {
node: NodeInstance;
position?: "absolute" | "fixed" | "relative";
position?: 'absolute' | 'fixed' | 'relative';
isActive?: boolean;
isSelected?: boolean;
inView?: boolean;
@@ -19,11 +19,11 @@
let {
node = $bindable(),
position = "absolute",
position = 'absolute',
isActive = false,
isSelected = false,
inView = true,
z = 2,
z = 2
}: Props = $props();
// If we dont have a random offset, all nodes becom visible at the same zoom level -> stuttering
@@ -31,12 +31,11 @@
const zLimit = 2 - zOffset;
const parameters = Object.entries(node.state?.type?.inputs || {}).filter(
(p) =>
p[1].type !== "seed" && !("setting" in p[1]) && p[1]?.hidden !== true,
(p) => p[1].type !== 'seed' && !('setting' in p[1]) && p[1]?.hidden !== true
);
$effect(() => {
if ("state" in node && !node.state.ref) {
if ('state' in node && !node.state.ref) {
node.state.ref = ref;
graphState?.updateNodePosition(node);
}
@@ -47,7 +46,7 @@
class="node {position}"
class:active={isActive}
style:--cz={z + zOffset}
style:display={inView && z > zLimit ? "block" : "none"}
style:display={inView && z > zLimit ? 'block' : 'none'}
class:selected={isSelected}
class:out-of-view={!inView}
data-node-id={node.id}
@@ -56,7 +55,7 @@
>
<NodeHeader {node} />
{#each parameters as [key, value], i}
{#each parameters as [key, value], i (key)}
<NodeParameter
bind:node
id={key}

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { getGraphState } from "../graph-state.svelte";
import { createNodePath } from "../helpers/index.js";
import type { NodeInstance } from "@nodarium/types";
import type { NodeInstance } from '@nodarium/types';
import { getGraphState } from '../graph-state.svelte';
import { createNodePath } from '../helpers/index.js';
const graphState = getGraphState();
@@ -10,47 +10,52 @@
function handleMouseDown(event: MouseEvent) {
event.stopPropagation();
event.preventDefault();
if ("state" in node) {
if ('state' in node) {
graphState.setDownSocket?.({
node,
index: 0,
position: graphState.getSocketPosition?.(node, 0),
position: graphState.getSocketPosition?.(node, 0)
});
}
}
const cornerTop = 10;
const rightBump = !!node?.state?.type?.outputs?.length;
const rightBump = $derived(!!node?.state?.type?.outputs?.length);
const aspectRatio = 0.25;
const path = createNodePath({
depth: 5.5,
height: 34,
y: 49,
cornerTop,
rightBump,
aspectRatio,
});
const pathHover = createNodePath({
depth: 8.5,
height: 50,
y: 49,
cornerTop,
rightBump,
aspectRatio,
});
const path = $derived(
createNodePath({
depth: 5.5,
height: 34,
y: 49,
cornerTop,
rightBump,
aspectRatio
})
);
const pathHover = $derived(
createNodePath({
depth: 8.5,
height: 50,
y: 49,
cornerTop,
rightBump,
aspectRatio
})
);
</script>
<div class="wrapper" data-node-id={node.id} data-node-type={node.type}>
<div class="content">
{node.type.split("/").pop()}
{node.type.split('/').pop()}
</div>
<div
class="click-target"
role="button"
tabindex="0"
onmousedown={handleMouseDown}
></div>
>
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100"
@@ -62,8 +67,7 @@
--hover-path: path("${pathHover}");
`}
>
<path vector-effect="non-scaling-stroke" stroke="white" stroke-width="0.1"
></path>
<path vector-effect="non-scaling-stroke" stroke="white" stroke-width="0.1"></path>
</svg>
</div>
@@ -104,9 +108,7 @@
svg path {
stroke-width: 0.2px;
transition:
d 0.3s ease,
fill 0.3s ease;
transition: d 0.3s ease, fill 0.3s ease;
fill: var(--layer-2);
stroke: var(--stroke);
stroke-width: var(--stroke-width);

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import type { NodeInstance, NodeInput } from "@nodarium/types";
import { Input } from "@nodarium/ui";
import type { GraphManager } from "../graph-manager.svelte";
import type { NodeInput, NodeInstance } from '@nodarium/types';
import { Input } from '@nodarium/ui';
import type { GraphManager } from '../graph-manager.svelte';
type Props = {
node: NodeInstance;
@@ -16,17 +16,18 @@
input,
id,
elementId = `input-${Math.random().toString(36).substring(7)}`,
graph,
graph
}: Props = $props();
function getDefaultValue() {
if (node?.props?.[id] !== undefined) return node?.props?.[id] as number;
if ("value" in input && input?.value !== undefined)
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;
}
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;
}

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import type { NodeInput, NodeInstance } from "@nodarium/types";
import { createNodePath } from "../helpers";
import NodeInputEl from "./NodeInput.svelte";
import { getGraphManager, getGraphState } from "../graph-state.svelte";
import type { NodeInput, NodeInstance } from '@nodarium/types';
import { getGraphManager, getGraphState } from '../graph-state.svelte';
import { createNodePath } from '../helpers';
import NodeInputEl from './NodeInput.svelte';
type Props = {
node: NodeInstance;
@@ -15,9 +15,9 @@
let { node = $bindable(), input, id, isLast }: Props = $props();
const inputType = node?.state?.type?.inputs?.[id]!;
const inputType = $derived(node?.state?.type?.inputs?.[id]);
const socketId = `${node.id}-${id}`;
const socketId = $derived(`${node.id}-${id}`);
const graphState = getGraphState();
const graphId = graph?.id;
@@ -30,38 +30,44 @@
graphState.setDownSocket({
node,
index: id,
position: graphState.getSocketPosition?.(node, id),
position: graphState.getSocketPosition?.(node, id)
});
}
const leftBump = node.state?.type?.inputs?.[id].internal !== true;
const cornerBottom = isLast ? 5 : 0;
const leftBump = $derived(node.state?.type?.inputs?.[id].internal !== true);
const cornerBottom = $derived(isLast ? 5 : 0);
const aspectRatio = 0.5;
const path = createNodePath({
depth: 7,
height: 20,
y: 50.5,
cornerBottom,
leftBump,
aspectRatio,
});
const pathDisabled = createNodePath({
depth: 6,
height: 18,
y: 50.5,
cornerBottom,
leftBump,
aspectRatio,
});
const pathHover = createNodePath({
depth: 8,
height: 25,
y: 50.5,
cornerBottom,
leftBump,
aspectRatio,
});
const path = $derived(
createNodePath({
depth: 7,
height: 20,
y: 50.5,
cornerBottom,
leftBump,
aspectRatio
})
);
const pathDisabled = $derived(
createNodePath({
depth: 6,
height: 18,
y: 50.5,
cornerBottom,
leftBump,
aspectRatio
})
);
const pathHover = $derived(
createNodePath({
depth: 8,
height: 25,
y: 50.5,
cornerBottom,
leftBump,
aspectRatio
})
);
</script>
<div
@@ -72,16 +78,14 @@
>
{#key id && graphId}
<div class="content" class:disabled={graph?.inputSockets?.has(socketId)}>
{#if inputType.label !== ""}
<label for={elementId} title={input.description}
>{input.label || id}</label
>
{#if inputType?.label !== ''}
<label for={elementId} title={input.description}>{input.label || id}</label>
{/if}
<span
class="absolute i-[tabler--help-circle] size-4 block top-2 right-2 opacity-30"
title={JSON.stringify(input, null, 2)}
></span>
{#if inputType.external !== true}
{#if inputType?.external !== true}
<NodeInputEl {graph} {elementId} bind:node {input} {id} />
{/if}
</div>
@@ -94,7 +98,8 @@
onmousedown={handleMouseDown}
role="button"
tabindex="0"
></div>
>
</div>
{/if}
{/key}
@@ -169,9 +174,7 @@
}
svg path {
transition:
d 0.3s ease,
fill 0.3s ease;
transition: d 0.3s ease, fill 0.3s ease;
fill: var(--layer-1);
stroke: var(--stroke);
stroke-width: var(--stroke-width);

View File

@@ -1 +1,95 @@
{"settings":{"resolution.circle":26,"resolution.curve":39},"nodes":[{"id":9,"position":[220,80],"type":"max/plantarium/output","props":{}},{"id":10,"position":[95,80],"type":"max/plantarium/stem","props":{"amount":5,"length":11,"thickness":0.1}},{"id":14,"position":[195,80],"type":"max/plantarium/gravity","props":{"strength":0.38,"scale":39,"fixBottom":0,"directionalStrength":[1,1,1],"depth":1,"curviness":1}},{"id":15,"position":[120,80],"type":"max/plantarium/noise","props":{"strength":4.9,"scale":2.2,"fixBottom":1,"directionalStrength":[1,1,1],"depth":1,"octaves":1}},{"id":16,"position":[70,80],"type":"max/plantarium/vec3","props":{"0":0,"1":0,"2":0}},{"id":17,"position":[45,80],"type":"max/plantarium/random","props":{"min":-2,"max":2}},{"id":18,"position":[170,80],"type":"max/plantarium/branch","props":{"length":1.6,"thickness":0.69,"amount":36,"offsetSingle":0.5,"lowestBranch":0.46,"highestBranch":1,"depth":1,"rotation":180}},{"id":19,"position":[145,80],"type":"max/plantarium/gravity","props":{"strength":0.38,"scale":39,"fixBottom":0,"directionalStrength":[1,1,1],"depth":1,"curviness":1}},{"id":20,"position":[70,120],"type":"max/plantarium/random","props":{"min":0.073,"max":0.15}}],"edges":[[14,0,9,"input"],[10,0,15,"plant"],[16,0,10,"origin"],[17,0,16,"0"],[17,0,16,"2"],[18,0,14,"plant"],[15,0,19,"plant"],[19,0,18,"plant"],[20,0,10,"thickness"]]}
{
"settings": { "resolution.circle": 26, "resolution.curve": 39 },
"nodes": [
{ "id": 9, "position": [220, 80], "type": "max/plantarium/output", "props": {} },
{
"id": 10,
"position": [95, 80],
"type": "max/plantarium/stem",
"props": { "amount": 5, "length": 11, "thickness": 0.1 }
},
{
"id": 14,
"position": [195, 80],
"type": "max/plantarium/gravity",
"props": {
"strength": 0.38,
"scale": 39,
"fixBottom": 0,
"directionalStrength": [1, 1, 1],
"depth": 1,
"curviness": 1
}
},
{
"id": 15,
"position": [120, 80],
"type": "max/plantarium/noise",
"props": {
"strength": 4.9,
"scale": 2.2,
"fixBottom": 1,
"directionalStrength": [1, 1, 1],
"depth": 1,
"octaves": 1
}
},
{
"id": 16,
"position": [70, 80],
"type": "max/plantarium/vec3",
"props": { "0": 0, "1": 0, "2": 0 }
},
{
"id": 17,
"position": [45, 80],
"type": "max/plantarium/random",
"props": { "min": -2, "max": 2 }
},
{
"id": 18,
"position": [170, 80],
"type": "max/plantarium/branch",
"props": {
"length": 1.6,
"thickness": 0.69,
"amount": 36,
"offsetSingle": 0.5,
"lowestBranch": 0.46,
"highestBranch": 1,
"depth": 1,
"rotation": 180
}
},
{
"id": 19,
"position": [145, 80],
"type": "max/plantarium/gravity",
"props": {
"strength": 0.38,
"scale": 39,
"fixBottom": 0,
"directionalStrength": [1, 1, 1],
"depth": 1,
"curviness": 1
}
},
{
"id": 20,
"position": [70, 120],
"type": "max/plantarium/random",
"props": { "min": 0.073, "max": 0.15 }
}
],
"edges": [
[14, 0, 9, "input"],
[10, 0, 15, "plant"],
[16, 0, 10, "origin"],
[17, 0, 16, "0"],
[17, 0, 16, "2"],
[18, 0, 14, "plant"],
[15, 0, 19, "plant"],
[19, 0, 18, "plant"],
[20, 0, 10, "thickness"]
]
}

View File

@@ -1,11 +1,10 @@
import type { Graph } from "@nodarium/types";
import type { Graph } from '@nodarium/types';
export function grid(width: number, height: number) {
const graph: Graph = {
id: Math.floor(Math.random() * 100000),
edges: [],
nodes: [],
nodes: []
};
const amount = width * height;
@@ -18,19 +17,18 @@ export function grid(width: number, height: number) {
id: i,
position: [x * 30, y * 40],
props: i == 0 ? { value: 0 } : { op_type: 0, a: 1, b: 0.05 },
type: i == 0 ? "max/plantarium/float" : "max/plantarium/math",
type: i == 0 ? 'max/plantarium/float' : 'max/plantarium/math'
});
graph.edges.push([i, 0, i + 1, i === amount - 1 ? "input" : "a",]);
graph.edges.push([i, 0, i + 1, i === amount - 1 ? 'input' : 'a']);
}
graph.nodes.push({
id: amount,
position: [width * 30, (height - 1) * 40],
type: "max/plantarium/output",
props: {},
type: 'max/plantarium/output',
props: {}
});
return graph;
}

View File

@@ -1,8 +1,7 @@
export { grid } from "./grid";
export { tree } from "./tree";
export { plant } from "./plant";
export { default as lottaFaces } from "./lotta-faces.json";
export { default as lottaNodes } from "./lotta-nodes.json";
export { default as defaultPlant } from "./default.json"
export { default as lottaNodesAndFaces } from "./lotta-nodes-and-faces.json";
export { default as defaultPlant } from './default.json';
export { grid } from './grid';
export { default as lottaFaces } from './lotta-faces.json';
export { default as lottaNodesAndFaces } from './lotta-nodes-and-faces.json';
export { default as lottaNodes } from './lotta-nodes.json';
export { plant } from './plant';
export { tree } from './tree';

View File

@@ -1 +1,44 @@
{"settings":{"resolution.circle":64,"resolution.curve":64,"randomSeed":false},"nodes":[{"id":9,"position":[260,0],"type":"max/plantarium/output","props":{}},{"id":18,"position":[185,0],"type":"max/plantarium/stem","props":{"amount":64,"length":12,"thickness":0.15}},{"id":19,"position":[210,0],"type":"max/plantarium/noise","props":{"scale":1.3,"strength":5.4}},{"id":20,"position":[235,0],"type":"max/plantarium/branch","props":{"length":0.8,"thickness":0.8,"amount":3}},{"id":21,"position":[160,0],"type":"max/plantarium/vec3","props":{"0":0.39,"1":0,"2":0.41}},{"id":22,"position":[130,0],"type":"max/plantarium/random","props":{"min":-2,"max":2}}],"edges":[[18,0,19,"plant"],[19,0,20,"plant"],[20,0,9,"input"],[21,0,18,"origin"],[22,0,21,"0"],[22,0,21,"2"]]}
{
"settings": { "resolution.circle": 64, "resolution.curve": 64, "randomSeed": false },
"nodes": [
{ "id": 9, "position": [260, 0], "type": "max/plantarium/output", "props": {} },
{
"id": 18,
"position": [185, 0],
"type": "max/plantarium/stem",
"props": { "amount": 64, "length": 12, "thickness": 0.15 }
},
{
"id": 19,
"position": [210, 0],
"type": "max/plantarium/noise",
"props": { "scale": 1.3, "strength": 5.4 }
},
{
"id": 20,
"position": [235, 0],
"type": "max/plantarium/branch",
"props": { "length": 0.8, "thickness": 0.8, "amount": 3 }
},
{
"id": 21,
"position": [160, 0],
"type": "max/plantarium/vec3",
"props": { "0": 0.39, "1": 0, "2": 0.41 }
},
{
"id": 22,
"position": [130, 0],
"type": "max/plantarium/random",
"props": { "min": -2, "max": 2 }
}
],
"edges": [
[18, 0, 19, "plant"],
[19, 0, 20, "plant"],
[20, 0, 9, "input"],
[21, 0, 18, "origin"],
[22, 0, 21, "0"],
[22, 0, 21, "2"]
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +1,71 @@
export const plant = {
"settings": { "resolution.circle": 26, "resolution.curve": 39 },
"nodes": [
{ "id": 9, "position": [180, 80], "type": "max/plantarium/output", "props": {} },
{ "id": 10, "position": [55, 80], "type": "max/plantarium/stem", "props": { "amount": 1, "length": 11, "thickness": 0.71 } },
{ "id": 11, "position": [80, 80], "type": "max/plantarium/noise", "props": { "strength": 35, "scale": 4.6, "fixBottom": 1, "directionalStrength": [1, 0.74, 0.083], "depth": 1 } },
{ "id": 12, "position": [105, 80], "type": "max/plantarium/branch", "props": { "length": 3, "thickness": 0.6, "amount": 10, "rotation": 180, "offsetSingle": 0.34, "lowestBranch": 0.53, "highestBranch": 1, "depth": 1 } },
{ "id": 13, "position": [130, 80], "type": "max/plantarium/noise", "props": { "strength": 8, "scale": 7.7, "fixBottom": 1, "directionalStrength": [1, 0, 1], "depth": 1 } },
{ "id": 14, "position": [155, 80], "type": "max/plantarium/gravity", "props": { "strength": 0.11, "scale": 39, "fixBottom": 0, "directionalStrength": [1, 1, 1], "depth": 1, "curviness": 1 } }
'settings': { 'resolution.circle': 26, 'resolution.curve': 39 },
'nodes': [
{ 'id': 9, 'position': [180, 80], 'type': 'max/plantarium/output', 'props': {} },
{
'id': 10,
'position': [55, 80],
'type': 'max/plantarium/stem',
'props': { 'amount': 1, 'length': 11, 'thickness': 0.71 }
},
{
'id': 11,
'position': [80, 80],
'type': 'max/plantarium/noise',
'props': {
'strength': 35,
'scale': 4.6,
'fixBottom': 1,
'directionalStrength': [1, 0.74, 0.083],
'depth': 1
}
},
{
'id': 12,
'position': [105, 80],
'type': 'max/plantarium/branch',
'props': {
'length': 3,
'thickness': 0.6,
'amount': 10,
'rotation': 180,
'offsetSingle': 0.34,
'lowestBranch': 0.53,
'highestBranch': 1,
'depth': 1
}
},
{
'id': 13,
'position': [130, 80],
'type': 'max/plantarium/noise',
'props': {
'strength': 8,
'scale': 7.7,
'fixBottom': 1,
'directionalStrength': [1, 0, 1],
'depth': 1
}
},
{
'id': 14,
'position': [155, 80],
'type': 'max/plantarium/gravity',
'props': {
'strength': 0.11,
'scale': 39,
'fixBottom': 0,
'directionalStrength': [1, 1, 1],
'depth': 1,
'curviness': 1
}
}
],
"edges": [[10, 0, 11, "plant"], [11, 0, 12, "plant"], [12, 0, 13, "plant"], [13, 0, 14, "plant"], [14, 0, 9, "input"]]
}
'edges': [
[10, 0, 11, 'plant'],
[11, 0, 12, 'plant'],
[12, 0, 13, 'plant'],
[13, 0, 14, 'plant'],
[14, 0, 9, 'input']
]
};

View File

@@ -1,28 +1,26 @@
import type { Graph, SerializedNode } from "@nodarium/types";
import type { Graph, SerializedNode } from '@nodarium/types';
export function tree(depth: number): Graph {
const nodes: SerializedNode[] = [
{
id: 0,
type: "max/plantarium/output",
type: 'max/plantarium/output',
position: [0, 0]
},
{
id: 1,
type: "max/plantarium/math",
type: 'max/plantarium/math',
position: [-40, -10]
}
]
];
const edges: [number, number, number, string][] = [
[1, 0, 0, "input"]
[1, 0, 0, 'input']
];
for (let d = 0; d < depth; d++) {
const amount = Math.pow(2, d);
for (let i = 0; i < amount; i++) {
const id0 = amount * 2 + i * 2;
const id1 = amount * 2 + i * 2 + 1;
@@ -33,24 +31,22 @@ export function tree(depth: number): Graph {
nodes.push({
id: id0,
type: "max/plantarium/math",
position: [x, y],
type: 'max/plantarium/math',
position: [x, y]
});
edges.push([id0, 0, parent, "a"]);
edges.push([id0, 0, parent, 'a']);
nodes.push({
id: id1,
type: "max/plantarium/math",
position: [x, y + 35],
type: 'max/plantarium/math',
position: [x, y + 35]
});
edges.push([id1, 0, parent, "b"]);
edges.push([id1, 0, parent, 'b']);
}
}
return {
id: Math.floor(Math.random() * 100000),
nodes,
edges
};
}

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { getContext, type Snippet } from "svelte";
import { getContext, type Snippet } from 'svelte';
let index = $state(-1);
let wrapper: HTMLDivElement;
@@ -8,19 +8,17 @@
$effect(() => {
if (index === -1) {
index = getContext<() => number>("registerCell")();
index = getContext<() => number>('registerCell')();
}
});
const sizes = getContext<{ value: string[] }>("sizes");
const sizes = getContext<{ value: string[] }>('sizes');
let downSizes: string[] = [];
let downWidth = 0;
let mouseDown = false;
let startX = 0;
function handleMouseDown(event: MouseEvent) {
downSizes = [...sizes.value];
mouseDown = true;
startX = event.clientX;
downWidth = wrapper.getBoundingClientRect().width;
@@ -45,7 +43,8 @@
role="button"
tabindex="0"
onmousedown={handleMouseDown}
></div>
>
</div>
{/if}
<div class="cell" bind:this={wrapper}>

View File

@@ -1,9 +1,11 @@
<script lang="ts">
import { setContext, type Snippet } from "svelte";
import { onMount, setContext, type Snippet } from 'svelte';
const { children, id } = $props<{ children?: Snippet; id?: string }>();
setContext("grid-id", id);
onMount(() => {
setContext('grid-id', id);
});
</script>
{@render children({ id })}

View File

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

View File

@@ -1,6 +1,6 @@
import { withSubComponents } from "$lib/helpers";
import Grid from "./Grid.svelte";
import Row from "./Row.svelte";
import Cell from "./Cell.svelte";
import { withSubComponents } from '$lib/helpers';
import Cell from './Cell.svelte';
import Grid from './Grid.svelte';
import Row from './Row.svelte';
export default withSubComponents(Grid, { Row, Cell });

View File

@@ -1,38 +1,39 @@
import { derived, get, writable } from "svelte/store";
import { derived, get, writable } from 'svelte/store';
type Shortcut = {
key: string | string[],
shift?: boolean,
ctrl?: boolean,
alt?: boolean,
preventDefault?: boolean,
description?: string,
callback: (event: KeyboardEvent) => void
export type ShortCut = {
key: string | string[];
shift?: boolean;
ctrl?: boolean;
alt?: boolean;
preventDefault?: boolean;
description?: string;
callback: (event: KeyboardEvent) => void;
};
function getShortcutId(shortcut: ShortCut) {
return `${shortcut.key}${shortcut.shift ? '+shift' : ''}${shortcut.ctrl ? '+ctrl' : ''}${
shortcut.alt ? '+alt' : ''
}`;
}
function getShortcutId(shortcut: Shortcut) {
return `${shortcut.key}${shortcut.shift ? "+shift" : ""}${shortcut.ctrl ? "+ctrl" : ""}${shortcut.alt ? "+alt" : ""}`;
}
export function createKeyMap(keys: Shortcut[]) {
export function createKeyMap(keys: ShortCut[]) {
const store = writable(new Map(keys.map(k => [getShortcutId(k), k])));
return {
handleKeyboardEvent: (event: KeyboardEvent) => {
const activeElement = document.activeElement as HTMLElement;
if (activeElement?.tagName === "INPUT" || activeElement?.tagName === "TEXTAREA") return;
if (activeElement?.tagName === 'INPUT' || activeElement?.tagName === 'TEXTAREA') return;
const key = [...get(store).values()].find(k => {
if (Array.isArray(k.key) ? !k.key.includes(event.key) : k.key !== event.key) return false;
if ("shift" in k && k.shift !== event.shiftKey) return false;
if ("ctrl" in k && k.ctrl !== event.ctrlKey) return false;
if ("alt" in k && k.alt !== event.altKey) return false;
if ('shift' in k && k.shift !== event.shiftKey) return false;
if ('ctrl' in k && k.ctrl !== event.ctrlKey) return false;
if ('alt' in k && k.alt !== event.altKey) return false;
return true;
});
if (key && key.preventDefault) event.preventDefault();
key?.callback(event);
},
addShortcut: (shortcut: Shortcut) => {
addShortcut: (shortcut: ShortCut) => {
if (Array.isArray(shortcut.key)) {
for (const k of shortcut.key) {
store.update(shortcuts => {
@@ -52,6 +53,5 @@ export function createKeyMap(keys: Shortcut[]) {
}
},
keys: derived(store, $store => Array.from($store.values()))
}
};
}

View File

@@ -18,7 +18,7 @@ export function animate(duration: number, callback: (progress: number) => void |
} else {
callback(1);
}
}
};
requestAnimationFrame(loop);
}
@@ -30,10 +30,11 @@ export function createNodePath({
cornerBottom = 0,
leftBump = false,
rightBump = false,
aspectRatio = 1,
aspectRatio = 1
} = {}) {
return `M0,${cornerTop}
${cornerTop
${
cornerTop
? ` V${cornerTop}
Q0,0 ${cornerTop * aspectRatio},0
H${100 - cornerTop * aspectRatio}
@@ -42,40 +43,45 @@ export function createNodePath({
: ` V0
H100
`
}
}
V${y - height / 2}
${rightBump
${
rightBump
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
: ` H100`
}
${cornerBottom
}
${
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}`
}
${
leftBump
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${
y - height / 2
}`
: ` H0`
}
Z`.replace(/\s+/g, " ");
}
Z`.replace(/\s+/g, ' ');
}
export const debounce = (fn: Function, ms = 300) => {
export const debounce = (fn: () => void, ms = 300) => {
let timeoutId: ReturnType<typeof setTimeout>;
return function (this: any, ...args: any[]) {
return function(this: unknown, ...args: unknown[]) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), ms);
timeoutId = setTimeout(() => fn.apply(this, args as []), ms);
};
};
export const clone: <T>(v: T) => T = "structedClone" in globalThis ? globalThis.structuredClone : (obj) => JSON.parse(JSON.stringify(obj));
export const clone: <T>(v: T) => T = 'structedClone' in globalThis
? globalThis.structuredClone
: (obj) => JSON.parse(JSON.stringify(obj));
export function withSubComponents<A, B extends Record<string, any>>(
export function withSubComponents<A, B extends Record<string, unknown>>(
component: A,
subcomponents: B
): A & B {
@@ -87,7 +93,7 @@ export function withSubComponents<A, B extends Record<string, any>>(
}
export function humanizeNumber(number: number): string {
const suffixes = ["", "K", "M", "B", "T"];
const suffixes = ['', 'K', 'M', 'B', 'T'];
if (number < 1000) {
return number.toString();
}
@@ -104,11 +110,15 @@ export function humanizeDuration(durationInMilliseconds: number) {
const millisecondsPerHour = 3600000;
const millisecondsPerDay = 86400000;
let days = Math.floor(durationInMilliseconds / millisecondsPerDay);
let hours = Math.floor((durationInMilliseconds % millisecondsPerDay) / millisecondsPerHour);
let minutes = Math.floor((durationInMilliseconds % millisecondsPerHour) / millisecondsPerMinute);
let seconds = Math.floor((durationInMilliseconds % millisecondsPerMinute) / millisecondsPerSecond);
let millis = durationInMilliseconds % millisecondsPerSecond;
const days = Math.floor(durationInMilliseconds / millisecondsPerDay);
const hours = Math.floor((durationInMilliseconds % millisecondsPerDay) / millisecondsPerHour);
const minutes = Math.floor(
(durationInMilliseconds % millisecondsPerHour) / millisecondsPerMinute
);
const seconds = Math.floor(
(durationInMilliseconds % millisecondsPerMinute) / millisecondsPerSecond
);
const millis = durationInMilliseconds % millisecondsPerSecond;
let durationString = '';
@@ -131,32 +141,10 @@ 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 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 {
export function debounceAsyncFunction<T extends (...args: never[]) => Promise<unknown>>(
asyncFn: T
): T {
let isRunning = false;
let latestArgs: Parameters<T> | null = null;
let resolveNext: (() => void) | null = null;
@@ -177,7 +165,7 @@ export function debounceAsyncFunction<T extends (...args: any[]) => Promise<any>
try {
// Execute with the latest arguments
const result = await asyncFn(...latestArgs!);
return result;
return result as ReturnType<T>;
} finally {
// Allow the next execution
isRunning = false;
@@ -190,48 +178,18 @@ export function debounceAsyncFunction<T extends (...args: any[]) => Promise<any>
}) 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 {
export function withArgsChangeOnly<T extends unknown[], R>(
func: (...args: T) => R
): (...args: T) => R {
let lastArgs: T | undefined = undefined;
let lastResult: R;
return (...args: T): R => {
// Check if arguments are the same as last call
if (lastArgs && args.length === lastArgs.length && args.every((val, index) => val === lastArgs?.[index])) {
if (
lastArgs && args.length === lastArgs.length
&& args.every((val, index) => val === lastArgs?.[index])
) {
return lastResult; // Return cached result if arguments haven't changed
}
@@ -241,4 +199,3 @@ export function withArgsChangeOnly<T extends any[], R>(func: (...args: T) => R):
return lastResult; // Return new result
};
}

View File

@@ -1,8 +1,8 @@
import { browser } from "$app/environment";
import { browser } from '$app/environment';
export class LocalStore<T> {
value = $state<T>() as T;
key = "";
key = '';
constructor(key: string, value: T) {
this.key = key;

View File

@@ -1,15 +1,14 @@
import { writable, type Writable } from "svelte/store";
import { type Writable, writable } from 'svelte/store';
function isStore(v: unknown): v is Writable<unknown> {
return v !== null && typeof v === "object" && "subscribe" in v && "set" in v;
return v !== null && typeof v === 'object' && 'subscribe' in v && 'set' in v;
}
const storeIds: Map<string, ReturnType<typeof createLocalStore>> = new Map();
const HAS_LOCALSTORAGE = "localStorage" in globalThis;
const HAS_LOCALSTORAGE = 'localStorage' in globalThis;
function createLocalStore<T>(key: string, initialValue: T | Writable<T>) {
let store: Writable<T>;
if (HAS_LOCALSTORAGE) {
@@ -36,18 +35,15 @@ function createLocalStore<T>(key: string, initialValue: T | Writable<T>) {
subscribe: store.subscribe,
set: store.set,
update: store.update
}
};
}
export default function localStore<T>(key: string, initialValue: T | Writable<T>): Writable<T> {
if (storeIds.has(key)) return storeIds.get(key) as Writable<T>;
const store = createLocalStore(key, initialValue)
const store = createLocalStore(key, initialValue);
storeIds.set(key, store);
return store
return store;
}

View File

@@ -1,6 +1,6 @@
export default <T extends unknown[]>(
callback: (...args: T) => void,
delay: number,
delay: number
) => {
let isWaiting = false;

View File

@@ -1,6 +1,10 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="17" y="8" width="5" height="7" rx="1" stroke="currentColor" stroke-width="2"/>
<rect x="2" y="3" width="5" height="7" rx="1" stroke="currentColor" stroke-width="2"/>
<rect x="2" y="14" width="5" height="7" rx="1" stroke="currentColor" stroke-width="2"/>
<path d="M16 10.5C9.33333 10.5 14.8889 6 8.22222 6H6M16 12.5C8.77778 12.5 14.8889 17 8.22222 17H6" stroke="currentColor" stroke-width="2"/>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="17" y="8" width="5" height="7" rx="1" stroke="currentColor" stroke-width="2" />
<rect x="2" y="3" width="5" height="7" rx="1" stroke="currentColor" stroke-width="2" />
<rect x="2" y="14" width="5" height="7" rx="1" stroke="currentColor" stroke-width="2" />
<path
d="M16 10.5C9.33333 10.5 14.8889 6 8.22222 6H6M16 12.5C8.77778 12.5 14.8889 17 8.22222 17H6"
stroke="currentColor"
stroke-width="2"
/>
</svg>

Before

Width:  |  Height:  |  Size: 496 B

After

Width:  |  Height:  |  Size: 509 B

View File

@@ -8,6 +8,7 @@ export async function getWasm(id: `${string}/${string}/${string}`) {
try {
await fs.access(filePath);
} catch (e) {
console.error(`Failed to read node: ${id}`, e);
return null;
}
@@ -20,9 +21,11 @@ export async function getNodeWasm(id: `${string}/${string}/${string}`) {
const wasmBytes = await getWasm(id);
if (!wasmBytes) return null;
const wrapper = createWasmWrapper(wasmBytes);
return wrapper;
try {
return createWasmWrapper(wasmBytes.buffer);
} catch (error) {
console.error(`Failed to create node wrapper for node: ${id}`, error);
}
}
export async function getNode(id: `${string}/${string}/${string}`) {

View File

@@ -0,0 +1,2 @@
export * from './node-registry-cache';
export * from './node-registry-client';

View File

@@ -1,8 +1,7 @@
import type { AsyncCache } from '@nodarium/types';
import { openDB, type IDBPDatabase } from 'idb';
import { type IDBPDatabase, openDB } from 'idb';
export class IndexDBCache implements AsyncCache<unknown> {
size: number = 100;
db: Promise<IDBPDatabase<unknown>>;
@@ -12,7 +11,7 @@ export class IndexDBCache implements AsyncCache<unknown> {
this.db = openDB<unknown>('cache/' + id, 1, {
upgrade(db) {
db.createObjectStore('keyval');
},
}
});
}
@@ -33,16 +32,16 @@ export class IndexDBCache implements AsyncCache<unknown> {
if (res instanceof ArrayBuffer) {
return res;
}
return
return;
}
async getString(key: string) {
const res = await this.get(key);
if (!res) return;
if (typeof res === "string") {
if (typeof res === 'string') {
return res;
}
return
return;
}
async set(key: string, value: unknown) {
@@ -54,5 +53,4 @@ export class IndexDBCache implements AsyncCache<unknown> {
clear() {
this.db.then(db => db.clear('keyval'));
}
}

View File

@@ -16,7 +16,7 @@ export class RemoteNodeRegistry implements NodeRegistry {
constructor(
private url: string,
public cache?: AsyncCache<ArrayBuffer | string>
) { }
) {}
async fetchJson(url: string, skipCache = false) {
const finalUrl = `${this.url}/${url}`;
@@ -105,7 +105,7 @@ export class RemoteNodeRegistry implements NodeRegistry {
const wasmBuffer = await this.fetchNodeWasm(id);
try {
return await this.register(wasmBuffer);
return await this.register(id, wasmBuffer);
} catch (e) {
console.log('Failed to register: ', id);
console.error(e);
@@ -125,20 +125,30 @@ export class RemoteNodeRegistry implements NodeRegistry {
return nodes;
}
async register(wasmBuffer: ArrayBuffer) {
const wrapper = createWasmWrapper(wasmBuffer);
async register(id: string, wasmBuffer: ArrayBuffer) {
let wrapper: ReturnType<typeof createWasmWrapper> = null!;
try {
wrapper = createWasmWrapper(wasmBuffer);
} catch (error) {
console.error(`Failed to create node wrapper for node: ${id}`, error);
}
const definition = NodeDefinitionSchema.safeParse(wrapper.get_definition());
const rawDefinition = wrapper.get_definition();
const definition = NodeDefinitionSchema.safeParse(rawDefinition);
if (definition.error) {
throw definition.error;
throw new Error(
'Failed to parse node definition from node:+\n' + JSON.stringify(rawDefinition, null, 2)
+ '\n'
+ definition.error
);
}
if (this.cache) {
this.cache.set(definition.data.id, wasmBuffer);
}
let node = {
const node = {
...definition.data,
execute: wrapper.execute
};

View File

@@ -1,16 +1,16 @@
<script lang="ts">
import { Select } from "@nodarium/ui";
import { Select } from '@nodarium/ui';
let activeStore = $state(0);
let { activeId }: { activeId: string } = $props();
const [activeUser, activeCollection, activeNode] = $derived(
activeId.split(`/`),
activeId.split(`/`)
);
</script>
<div class="breadcrumbs">
{#if activeUser}
<Select id="root" options={["root"]} bind:value={activeStore}></Select>
<Select id="root" options={['root']} bind:value={activeStore}></Select>
{#if activeCollection}
<button
onclick={() => {
@@ -35,7 +35,7 @@
<span>{activeUser}</span>
{/if}
{:else}
<Select id="root" options={["root"]} bind:value={activeStore}></Select>
<Select id="root" options={['root']} bind:value={activeStore}></Select>
{/if}
</div>

View File

@@ -1,39 +1,44 @@
<script lang="ts">
import NodeHtml from "$lib/graph-interface/node/NodeHTML.svelte";
import type { NodeDefinition, NodeId, NodeInstance } from "@nodarium/types";
import NodeHtml from '$lib/graph-interface/node/NodeHTML.svelte';
import type { NodeDefinition, NodeId, NodeInstance } from '@nodarium/types';
import { onMount } from 'svelte';
const { node }: { node: NodeDefinition } = $props();
let dragging = $state(false);
let nodeData = $state<NodeInstance>({
id: 0,
type: node.id as unknown as NodeId,
position: [0, 0] as [number, number],
props: {},
state: {
type: node,
},
});
let nodeData = $state<NodeInstance>(null!);
function handleDragStart(e: DragEvent) {
dragging = true;
const box = (e?.target as HTMLElement)?.getBoundingClientRect();
if (e.dataTransfer === null) return;
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("data/node-id", node.id.toString());
if (nodeData.props) {
e.dataTransfer.setData("data/node-props", JSON.stringify(nodeData.props));
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('data/node-id', node.id.toString());
if (nodeData?.props) {
e.dataTransfer.setData('data/node-props', JSON.stringify(nodeData.props));
}
e.dataTransfer.setData(
"data/node-offset-x",
Math.round(box.left - e.clientX).toString(),
'data/node-offset-x',
Math.round(box.left - e.clientX).toString()
);
e.dataTransfer.setData(
"data/node-offset-y",
Math.round(box.top - e.clientY).toString(),
'data/node-offset-y',
Math.round(box.top - e.clientY).toString()
);
}
onMount(() => {
nodeData = {
id: 0,
type: node.id as unknown as NodeId,
position: [0, 0] as [number, number],
props: {},
state: {
type: node
}
};
});
</script>
<div class="node-wrapper" class:dragging>
@@ -46,7 +51,7 @@
tabindex="0"
ondragstart={handleDragStart}
>
<NodeHtml bind:node={nodeData} inView={true} position={"relative"} z={5} />
<NodeHtml bind:node={nodeData} inView={true} position="relative" z={5} />
</div>
</div>

View File

@@ -1,15 +1,15 @@
<script lang="ts">
import BreadCrumbs from "./BreadCrumbs.svelte";
import DraggableNode from "./DraggableNode.svelte";
import type { RemoteNodeRegistry } from "@nodarium/registry";
import type { RemoteNodeRegistry } from '$lib/node-registry/index';
import BreadCrumbs from './BreadCrumbs.svelte';
import DraggableNode from './DraggableNode.svelte';
const { registry }: { registry: RemoteNodeRegistry } = $props();
let activeId = $state("max/plantarium");
let activeId = $state('max/plantarium');
let showBreadCrumbs = false;
const [activeUser, activeCollection, activeNode] = $derived(
activeId.split(`/`),
activeId.split(`/`)
);
</script>
@@ -22,12 +22,14 @@
{#await registry.fetchUsers()}
<div>Loading Users...</div>
{:then users}
{#each users as user}
{#each users as user (user.id)}
<button
onclick={() => {
activeId = user.id;
}}>{user.id}</button
}}
>
{user.id}
</button>
{/each}
{:catch error}
<div>{error.message}</div>
@@ -36,7 +38,7 @@
{#await registry.fetchUser(activeUser)}
<div>Loading User...</div>
{:then user}
{#each user.collections as collection}
{#each user.collections as collection (collection)}
<button
onclick={() => {
activeId = collection.id;
@@ -52,7 +54,7 @@
{#await registry.fetchCollection(`${activeUser}/${activeCollection}`)}
<div>Loading Collection...</div>
{:then collection}
{#each collection.nodes as node}
{#each collection.nodes as node (node.id)}
{#await registry.fetchNodeDefinition(node.id)}
<div>Loading Node... {node.id}</div>
{:then node}

View File

@@ -8,15 +8,15 @@
const total = $derived(values.reduce((acc, v) => acc + v, 0));
let colors = ["red", "green", "blue"];
let colors = ['red', 'green', 'blue'];
</script>
<div class="wrapper">
<div class="bars">
{#each values as value, i}
{#each values as value, i (value)}
<div
class="bar bg-{colors[i]}-400"
style="width: {(value / total) * 100}%;"
style:width={(value / total) * 100 + '%'}
>
{Math.round(value)}ms
</div>
@@ -24,7 +24,7 @@
</div>
<div class="labels mt-2">
{#each values as _label, i}
{#each values as _label, i (_label)}
<div class="text-{colors[i]}-400">{labels[i]}</div>
{/each}
</div>

View File

@@ -9,17 +9,17 @@
let {
points,
type = "ms",
title = "Performance",
type = 'ms',
title = 'Performance',
max,
min,
min
}: Props = $props();
let internalMax = $derived(max ?? Math.max(...points));
let internalMin = $derived(min ?? Math.min(...points))!;
const maxText = $derived.by(() => {
if (type === "%") {
if (type === '%') {
return 100;
}
@@ -40,11 +40,10 @@
points
.map((point, i) => {
const x = (i / (points.length - 1)) * 100;
const y =
100 - ((point - internalMin) / (internalMax - internalMin)) * 100;
const y = 100 - ((point - internalMin) / (internalMax - internalMin)) * 100;
return `${x},${y}`;
})
.join(" "),
.join(' ')
);
</script>

View File

@@ -1,13 +1,13 @@
<script lang="ts">
import Monitor from "./Monitor.svelte";
import { humanizeNumber } from "$lib/helpers";
import { Checkbox } from "@nodarium/ui";
import type { PerformanceData } from "@nodarium/utils";
import BarSplit from "./BarSplit.svelte";
import { humanizeNumber } from '$lib/helpers';
import { Checkbox } from '@nodarium/ui';
import type { PerformanceData } from '@nodarium/utils';
import BarSplit from './BarSplit.svelte';
import Monitor from './Monitor.svelte';
const { data }: { data: PerformanceData } = $props();
let activeType = $state("total");
let activeType = $state('total');
let showAverage = $state(true);
function round(v: number) {
@@ -21,21 +21,21 @@
}
function getTitle(t: string) {
if (t.includes("/")) {
return `Node ${t.split("/").slice(-1).join("/")}`;
if (t.includes('/')) {
return `Node ${t.split('/').slice(-1).join('/')}`;
}
return t
.split("-")
.split('-')
.map((v) => v[0].toUpperCase() + v.slice(1))
.join(" ");
.join(' ');
}
const viewerKeys = [
"total-vertices",
"total-faces",
"update-geometries",
"split-result",
'total-vertices',
'total-faces',
'update-geometries',
'split-result'
];
// --- Small helpers that query `data` directly ---
@@ -64,21 +64,19 @@
const lasts = $derived.by(() => data.at(-1) || {});
const totalPerformance = $derived.by(() => {
const onlyLast =
getLast("runtime") +
getLast("update-geometries") +
getLast("worker-transfer");
const average =
getAverage("runtime") +
getAverage("update-geometries") +
getAverage("worker-transfer");
const onlyLast = getLast('runtime')
+ getLast('update-geometries')
+ getLast('worker-transfer');
const average = getAverage('runtime')
+ getAverage('update-geometries')
+ getAverage('worker-transfer');
return { onlyLast, average };
});
const cacheRatio = $derived.by(() => {
return {
onlyLast: Math.floor(getLast("cache-hit") * 100),
average: Math.floor(getAverage("cache-hit") * 100),
onlyLast: Math.floor(getLast('cache-hit') * 100),
average: Math.floor(getAverage('cache-hit') * 100)
};
});
@@ -87,10 +85,10 @@
return Object.entries(source)
.filter(
([key]) =>
!key.startsWith("node/") &&
key !== "total" &&
!key.includes("cache") &&
!viewerKeys.includes(key),
!key.startsWith('node/')
&& key !== 'total'
&& !key.includes('cache')
&& !viewerKeys.includes(key)
)
.sort((a, b) => b[1] - a[1]);
});
@@ -98,7 +96,7 @@
const nodePerformanceData = $derived.by(() => {
const source = showAverage ? averages : lasts;
return Object.entries(source)
.filter(([key]) => key.startsWith("node/"))
.filter(([key]) => key.startsWith('node/'))
.sort((a, b) => b[1] - a[1]);
});
@@ -107,9 +105,9 @@
return Object.entries(source)
.filter(
([key]) =>
key !== "total-vertices" &&
key !== "total-faces" &&
viewerKeys.includes(key),
key !== 'total-vertices'
&& key !== 'total-faces'
&& viewerKeys.includes(key)
)
.sort((a, b) => b[1] - a[1]);
});
@@ -117,15 +115,15 @@
const splitValues = $derived.by(() => {
if (showAverage) {
return [
getAverage("worker-transfer"),
getAverage("runtime"),
getAverage("update-geometries"),
getAverage('worker-transfer'),
getAverage('runtime'),
getAverage('update-geometries')
];
}
return [
getLast("worker-transfer"),
getLast("runtime"),
getLast("update-geometries"),
getLast('worker-transfer'),
getLast('runtime'),
getLast('update-geometries')
];
});
@@ -133,24 +131,24 @@
if (showAverage) {
return data.map((run) => {
return (
(run["runtime"]?.reduce((acc, v) => acc + v, 0) || 0) +
(run["update-geometries"]?.reduce((acc, v) => acc + v, 0) || 0) +
(run["worker-transfer"]?.reduce((acc, v) => acc + v, 0) || 0)
(run['runtime']?.reduce((acc, v) => acc + v, 0) || 0)
+ (run['update-geometries']?.reduce((acc, v) => acc + v, 0) || 0)
+ (run['worker-transfer']?.reduce((acc, v) => acc + v, 0) || 0)
);
});
}
return data.map((run) => {
return (
(run["runtime"]?.[0] || 0) +
(run["update-geometries"]?.[0] || 0) +
(run["worker-transfer"]?.[0] || 0)
(run['runtime']?.[0] || 0)
+ (run['update-geometries']?.[0] || 0)
+ (run['worker-transfer']?.[0] || 0)
);
});
});
function constructPoints(key: string) {
if (key === "total") {
if (key === 'total') {
return totalPoints;
}
return data.map((run) => {
@@ -166,21 +164,21 @@
}
const computedTotalDisplay = $derived.by(() =>
round(showAverage ? totalPerformance.average : totalPerformance.onlyLast),
round(showAverage ? totalPerformance.average : totalPerformance.onlyLast)
);
const computedFps = $derived.by(() =>
Math.floor(
1000 /
(showAverage
1000
/ (showAverage
? totalPerformance.average || 1
: totalPerformance.onlyLast || 1),
),
: totalPerformance.onlyLast || 1)
)
);
</script>
{#if data.length !== 0}
{#if activeType === "cache-hit"}
{#if activeType === 'cache-hit'}
<Monitor
title="Cache Hits"
points={constructPoints(activeType)}
@@ -202,7 +200,7 @@
</div>
<BarSplit
labels={["worker-transfer", "runtime", "update-geometries"]}
labels={['worker-transfer', 'runtime', 'update-geometries']}
values={splitValues}
/>
@@ -215,14 +213,14 @@
{computedTotalDisplay}<span>ms</span>
</td>
<td
class:active={activeType === "total"}
onclick={() => (activeType = "total")}
class:active={activeType === 'total'}
onclick={() => (activeType = 'total')}
>
total<span>({computedFps}fps)</span>
</td>
</tr>
{#each performanceData as [key, value]}
{#each performanceData as [key, value] (key)}
<tr>
<td>{round(value)}<span>ms</span></td>
<td
@@ -246,27 +244,23 @@
<tbody>
<tr>
<td>{showAverage ? cacheRatio.average : cacheRatio.onlyLast}<span>%</span></td>
<td
>{showAverage ? cacheRatio.average : cacheRatio.onlyLast}<span
>%</span
></td
>
<td
class:active={activeType === "cache-hit"}
onclick={() => (activeType = "cache-hit")}
class:active={activeType === 'cache-hit'}
onclick={() => (activeType = 'cache-hit')}
>
cache hits
</td>
</tr>
{#each nodePerformanceData as [key, value]}
{#each nodePerformanceData as [key, value] (key)}
<tr>
<td>{round(value)}<span>ms</span></td>
<td
class:active={activeType === key}
onclick={() => (activeType = key)}
>
{key.split("/").slice(-1).join("/")}
{key.split('/').slice(-1).join('/')}
</td>
</tr>
{/each}
@@ -278,22 +272,22 @@
<tbody>
<tr>
<td>{humanizeNumber(getLast("total-vertices"))}</td>
<td>{humanizeNumber(getLast('total-vertices'))}</td>
<td>Vertices</td>
</tr>
<tr>
<td>{humanizeNumber(getLast("total-faces"))}</td>
<td>{humanizeNumber(getLast('total-faces'))}</td>
<td>Faces</td>
</tr>
{#each viewerPerformanceData as [key, value]}
{#each viewerPerformanceData as [key, value] (key)}
<tr>
<td>{round(value)}<span>ms</span></td>
<td
class:active={activeType === key}
onclick={() => (activeType = key)}
>
{key.split("/").slice(-1).join("/")}
{key.split('/').slice(-1).join('/')}
</td>
</tr>
{/each}

View File

@@ -10,7 +10,7 @@
const y = 100 - ((point - min) / (max - min)) * 100;
return `${x},${y}`;
})
.join(" ");
.join(' ');
});
</script>

View File

@@ -1,19 +1,19 @@
<script lang="ts">
import { humanizeDuration, humanizeNumber } from "$lib/helpers";
import { localState } from "$lib/helpers/localState.svelte";
import SmallGraph from "./SmallGraph.svelte";
import type { PerformanceData, PerformanceStore } from "@nodarium/utils";
import { humanizeDuration, humanizeNumber } from '$lib/helpers';
import { localState } from '$lib/helpers/localState.svelte';
import type { PerformanceData, PerformanceStore } from '@nodarium/utils';
import SmallGraph from './SmallGraph.svelte';
const { store, fps }: { store: PerformanceStore; fps: number[] } = $props();
const open = localState("node.performance.small.open", {
const open = localState('node.performance.small.open', {
runtime: false,
fps: false,
fps: false
});
const vertices = $derived($store?.at(-1)?.["total-vertices"]?.[0] || 0);
const faces = $derived($store?.at(-1)?.["total-faces"]?.[0] || 0);
const runtime = $derived($store?.at(-1)?.["runtime"]?.[0] || 0);
const vertices = $derived($store?.at(-1)?.['total-vertices']?.[0] || 0);
const faces = $derived($store?.at(-1)?.['total-faces']?.[0] || 0);
const runtime = $derived($store?.at(-1)?.['runtime']?.[0] || 0);
function getPoints(data: PerformanceData, key: string) {
return data?.map((run) => run[key]?.[0] || 0) || [];
@@ -24,25 +24,25 @@
<table>
<tbody>
<tr
style="cursor:pointer;"
style="cursor: pointer"
onclick={() => (open.value.runtime = !open.value.runtime)}
>
<td>{open.value.runtime ? "-" : "+"} runtime </td>
<td>{open.value.runtime ? '-' : '+'} runtime</td>
<td>{humanizeDuration(runtime || 1000)}</td>
</tr>
{#if open.value.runtime}
<tr>
<td colspan="2">
<SmallGraph points={getPoints($store, "runtime")} />
<SmallGraph points={getPoints($store, 'runtime')} />
</td>
</tr>
{/if}
<tr
style="cursor:pointer;"
style="cursor: pointer"
onclick={() => (open.value.fps = !open.value.fps)}
>
<td>{open.value.fps ? "-" : "+"} fps </td>
<td>{open.value.fps ? '-' : '+'} fps</td>
<td>
{Math.floor(fps[fps.length - 1])}fps
</td>
@@ -56,12 +56,12 @@
{/if}
<tr>
<td>vertices </td>
<td>vertices</td>
<td>{humanizeNumber(vertices || 0)}</td>
</tr>
<tr>
<td>faces </td>
<td>faces</td>
<td>{humanizeNumber(faces || 0)}</td>
</tr>
</tbody>

View File

@@ -1 +1 @@
export { default as PerformanceViewer } from "./PerformanceViewer.svelte";
export { default as PerformanceViewer } from './PerformanceViewer.svelte';

View File

@@ -1,40 +1,37 @@
<script lang="ts">
import type { Graph } from "$lib/types";
import { defaultPlant, plant, lottaFaces } from "$lib/graph-templates";
import type { ProjectManager } from "./project-manager.svelte";
import { defaultPlant, lottaFaces, plant } from '$lib/graph-templates';
import type { Graph } from '$lib/types';
import type { ProjectManager } from './project-manager.svelte';
const { projectManager } = $props<{ projectManager: ProjectManager }>();
let showNewProject = $state(false);
let newProjectName = $state("");
let selectedTemplate = $state("defaultPlant");
let newProjectName = $state('');
let selectedTemplate = $state('defaultPlant');
const templates = [
{
name: "Default Plant",
value: "defaultPlant",
graph: defaultPlant as unknown as Graph,
name: 'Default Plant',
value: 'defaultPlant',
graph: defaultPlant as unknown as Graph
},
{ name: "Plant", value: "plant", graph: plant as unknown as Graph },
{ name: 'Plant', value: 'plant', graph: plant as unknown as Graph },
{
name: "Lotta Faces",
value: "lottaFaces",
graph: lottaFaces as unknown as Graph,
},
name: 'Lotta Faces',
value: 'lottaFaces',
graph: lottaFaces as unknown as Graph
}
];
function handleCreate() {
const template =
templates.find((t) => t.value === selectedTemplate) || templates[0];
const template = templates.find((t) => t.value === selectedTemplate) || templates[0];
projectManager.handleCreateProject(template.graph, newProjectName);
newProjectName = "";
newProjectName = '';
showNewProject = false;
}
</script>
<header
class="flex justify-between px-4 h-[70px] border-b-1 border-[var(--outline)] items-center"
>
<header class="flex justify-between px-4 h-[70px] border-b-1 border-[var(--outline)] items-center">
<h3>Project</h3>
<button
class="px-3 py-1 bg-[var(--layer-0)] rounded"
@@ -51,13 +48,13 @@
bind:value={newProjectName}
placeholder="Project name"
class="w-full px-2 py-2 bg-gray-800 border border-gray-700 rounded"
onkeydown={(e) => e.key === "Enter" && handleCreate()}
onkeydown={(e) => e.key === 'Enter' && handleCreate()}
/>
<select
bind:value={selectedTemplate}
class="w-full px-2 py-2 bg-gray-800 border border-gray-700 rounded"
>
{#each templates as template}
{#each templates as template (template.name)}
<option value={template.value}>{template.name}</option>
{/each}
</select>
@@ -79,19 +76,21 @@
{#each projectManager.projects as project (project.id)}
<li>
<div
class="w-full text-left px-3 py-2 rounded cursor-pointer {projectManager
class="
w-full text-left px-3 py-2 rounded cursor-pointer {projectManager
.activeProjectId.value === project.id
? 'bg-blue-600'
: 'bg-gray-800 hover:bg-gray-700'}"
: 'bg-gray-800 hover:bg-gray-700'}
"
onclick={() => projectManager.handleSelectProject(project.id!)}
role="button"
tabindex="0"
onkeydown={(e) =>
e.key === "Enter" &&
projectManager.handleSelectProject(project.id!)}
e.key === 'Enter'
&& projectManager.handleSelectProject(project.id!)}
>
<div class="flex justify-between items-center">
<span>{project.meta?.title || "Untitled"}</span>
<span>{project.meta?.title || 'Untitled'}</span>
<button
class="text-red-400 hover:text-red-300"
onclick={() => {

View File

@@ -31,6 +31,7 @@ export async function getGraph(id: number): Promise<Graph | undefined> {
export async function saveGraph(graph: Graph): Promise<Graph> {
const db = await getDB();
// eslint-disable-next-line svelte/prefer-svelte-reactivity
graph.meta = { ...graph.meta, lastModified: new Date().toISOString() };
await db.put(STORE_NAME, graph);
return graph;

View File

@@ -25,7 +25,7 @@ export class ProjectManager {
this.projects = await db.getGraphs();
if (this.activeProjectId.value !== undefined) {
let loadedGraph = await db.getGraph(this.activeProjectId.value);
const loadedGraph = await db.getGraph(this.activeProjectId.value);
if (loadedGraph) {
this.graph = loadedGraph;
}

View File

@@ -1,11 +1,11 @@
<script lang="ts">
import localStore from "$lib/helpers/localStore";
import { T, useTask } from "@threlte/core";
import { OrbitControls } from "@threlte/extras";
import { onMount } from "svelte";
import { Vector3 } from "three";
import type { PerspectiveCamera, Vector3Tuple } from "three";
import type { OrbitControls as OrbitControlsType } from "three/examples/jsm/controls/OrbitControls.js";
import localStore from '$lib/helpers/localStore';
import { T, useTask } from '@threlte/core';
import { OrbitControls } from '@threlte/extras';
import { onMount } from 'svelte';
import { Vector3 } from 'three';
import type { PerspectiveCamera, Vector3Tuple } from 'three';
import type { OrbitControls as OrbitControlsType } from 'three/examples/jsm/controls/OrbitControls.js';
let camera = $state<PerspectiveCamera>();
let controls = $state<OrbitControlsType>();
@@ -20,9 +20,9 @@
const cameraTransform = localStore<{
camera: Vector3Tuple;
target: Vector3Tuple;
}>("nodes.camera.transform", {
}>('nodes.camera.transform', {
camera: [10, 10, 10],
target: [0, 0, 0],
target: [0, 0, 0]
});
function saveCameraState() {
@@ -33,7 +33,7 @@
if (tPos.some((v) => isNaN(v)) || cPos.some((v) => isNaN(v))) return;
$cameraTransform = {
camera: cPos,
target: tPos,
target: tPos
};
}
@@ -54,13 +54,13 @@
$effect(() => {
if (
center &&
controls &&
centerCamera &&
(center.x !== controls.target.x ||
center.y !== controls.target.y ||
center.z !== controls.target.z) &&
!isRunning
center
&& controls
&& centerCamera
&& (center.x !== controls.target.x
|| center.y !== controls.target.y
|| center.z !== controls.target.z)
&& !isRunning
) {
isRunning = true;
task.start();

View File

@@ -1,23 +1,18 @@
<script lang="ts">
import { T, useTask, useThrelte } from "@threlte/core";
import { colors } from '$lib/graph-interface/graph/colors.svelte';
import { T, useTask, useThrelte } from '@threlte/core';
import { Grid, MeshLineGeometry, MeshLineMaterial, Text } from '@threlte/extras';
import {
Grid,
MeshLineGeometry,
MeshLineMaterial,
Text,
} from "@threlte/extras";
import {
type Group,
type BufferGeometry,
Vector3,
type Vector3Tuple,
Box3,
type BufferGeometry,
type Group,
Mesh,
MeshBasicMaterial,
} from "three";
import { appSettings } from "../settings/app-settings.svelte";
import Camera from "./Camera.svelte";
import { colors } from "$lib/graph-interface/graph/colors.svelte";
Vector3,
type Vector3Tuple
} from 'three';
import { appSettings } from '../settings/app-settings.svelte';
import Camera from './Camera.svelte';
const { renderStage, invalidate: _invalidate } = useThrelte();
@@ -32,7 +27,7 @@
lines,
centerCamera,
fps = $bindable(),
scene = $bindable(),
scene = $bindable()
}: Props = $props();
let geometries = $state.raw<BufferGeometry[]>([]);
@@ -43,13 +38,13 @@
fps.push(1 / delta);
fps = fps.slice(-100);
},
{ stage: renderStage, autoInvalidate: false },
{ stage: renderStage, autoInvalidate: false }
);
export const invalidate = function () {
export const invalidate = function() {
if (scene) {
const geos: BufferGeometry[] = [];
scene.traverse(function (child) {
scene.traverse(function(child) {
if (isMesh(child)) {
geos.push(child.geometry);
}
@@ -67,17 +62,29 @@
_invalidate();
};
function isMesh(child: Mesh | any): child is Mesh {
return child.isObject3D && "material" in child;
function isMesh(child: unknown): child is Mesh {
return (
child !== null
&& typeof child === 'object'
&& 'isObject3D' in child
&& child.isObject3D === true
&& 'material' in child
);
}
function isMatCapMaterial(material: any): material is MeshBasicMaterial {
return material.isMaterial && "matcap" in material;
function isMatCapMaterial(material: unknown): material is MeshBasicMaterial {
return (
material !== null
&& typeof material === 'object'
&& 'isMaterial' in material
&& material.isMaterial === true
&& 'matcap' in material
);
}
$effect(() => {
const wireframe = appSettings.value.debug.wireframe;
scene.traverse(function (child) {
scene.traverse(function(child) {
if (isMesh(child) && isMatCapMaterial(child.material) && child.visible) {
child.material.wireframe = wireframe;
}
@@ -89,7 +96,7 @@
return [
geo.attributes.position.array[i],
geo.attributes.position.array[i + 1],
geo.attributes.position.array[i + 2],
geo.attributes.position.array[i + 2]
] as Vector3Tuple;
}
</script>
@@ -98,12 +105,12 @@
{#if appSettings.value.showGrid}
<Grid
cellColor={colors["outline"]}
cellColor={colors['outline']}
cellThickness={0.7}
infiniteGrid
sectionThickness={0.7}
sectionDistance={2}
sectionColor={colors["outline"]}
sectionColor={colors['outline']}
fadeDistance={50}
fadeStrength={10}
fadeOrigin={new Vector3(0, 0, 0)}
@@ -112,9 +119,9 @@
<T.Group>
{#if geometries}
{#each geometries as geo}
{#each geometries as geo (geo.id)}
{#if appSettings.value.debug.showIndices}
{#each geo.attributes.position.array as _, i}
{#each geo.attributes.position.array, i (i)}
{#if i % 3 === 0}
<Text fontSize={0.25} position={getPosition(geo, i)} />
{/if}
@@ -134,7 +141,7 @@
</T.Group>
{#if appSettings.value.debug.showStemLines && lines}
{#each lines as line}
{#each lines as line (line[0].x + '-' + line[0].y + '-' + '' + line[0].z)}
<T.Mesh>
<MeshLineGeometry points={line} />
<MeshLineMaterial width={0.1} color="red" depthTest={false} />

View File

@@ -1,23 +1,20 @@
<script lang="ts">
import { Canvas } from "@threlte/core";
import Scene from "./Scene.svelte";
import { Vector3 } from "three";
import { decodeFloat, splitNestedArray } from "@nodarium/utils";
import type { PerformanceStore } from "@nodarium/utils";
import { appSettings } from "$lib/settings/app-settings.svelte";
import SmallPerformanceViewer from "$lib/performance/SmallPerformanceViewer.svelte";
import { MeshMatcapMaterial, TextureLoader, type Group } from "three";
import {
createGeometryPool,
createInstancedGeometryPool,
} from "./geometryPool";
import SmallPerformanceViewer from '$lib/performance/SmallPerformanceViewer.svelte';
import { appSettings } from '$lib/settings/app-settings.svelte';
import { decodeFloat, splitNestedArray } from '@nodarium/utils';
import type { PerformanceStore } from '@nodarium/utils';
import { Canvas } from '@threlte/core';
import { Vector3 } from 'three';
import { type Group, MeshMatcapMaterial, TextureLoader } from 'three';
import { createGeometryPool, createInstancedGeometryPool } from './geometryPool';
import Scene from './Scene.svelte';
const loader = new TextureLoader();
const matcap = loader.load("/matcap_green.jpg");
matcap.colorSpace = "srgb";
const matcap = loader.load('/matcap_green.jpg');
matcap.colorSpace = 'srgb';
const material = new MeshMatcapMaterial({
color: 0xffffff,
matcap,
matcap
});
let sceneComponent = $state<ReturnType<typeof Scene>>();
@@ -34,7 +31,7 @@
return {
totalFaces: meshes.totalFaces + faces.totalFaces,
totalVertices: meshes.totalVertices + faces.totalVertices,
totalVertices: meshes.totalVertices + faces.totalVertices
};
}
@@ -64,12 +61,12 @@
}
export const update = function update(result: Int32Array) {
perf.addPoint("split-result");
perf.addPoint('split-result');
const inputs = splitNestedArray(result);
perf.endPoint();
if (appSettings.value.debug.showStemLines) {
perf.addPoint("create-lines");
perf.addPoint('create-lines');
lines = inputs
.map((input) => {
if (input[0] === 0) {
@@ -80,13 +77,13 @@
perf.endPoint();
}
perf.addPoint("update-geometries");
perf.addPoint('update-geometries');
const { totalVertices, totalFaces } = updateGeometries(inputs, scene);
perf.endPoint();
perf.addPoint("total-vertices", totalVertices);
perf.addPoint("total-faces", totalFaces);
perf.addPoint('total-vertices', totalVertices);
perf.addPoint('total-faces', totalFaces);
sceneComponent?.invalidate();
};
</script>

View File

@@ -1,4 +1,4 @@
import { fastHashArrayBuffer } from "@nodarium/utils";
import { fastHashArrayBuffer } from '@nodarium/utils';
import {
BufferAttribute,
BufferGeometry,
@@ -7,14 +7,14 @@ import {
InstancedMesh,
Material,
Matrix4,
Mesh,
} from "three";
Mesh
} from 'three';
function fastArrayHash(arr: Int32Array) {
const sampleDistance = Math.max(Math.floor(arr.length / 1000), 1);
const sampleCount = Math.floor(arr.length / sampleDistance);
let hash = new Int32Array(sampleCount);
const hash = new Int32Array(sampleCount);
for (let i = 0; i < sampleCount; i++) {
const index = i * sampleDistance;
@@ -28,18 +28,18 @@ export function createGeometryPool(parentScene: Group, material: Material) {
const scene = new Group();
parentScene.add(scene);
let meshes: Mesh[] = [];
const meshes: Mesh[] = [];
let totalVertices = 0;
let totalFaces = 0;
function updateSingleGeometry(
data: Int32Array,
existingMesh: Mesh | null = null,
existingMesh: Mesh | null = null
) {
let hash = fastArrayHash(data);
const hash = fastArrayHash(data);
let geometry = existingMesh ? existingMesh.geometry : new BufferGeometry();
const geometry = existingMesh ? existingMesh.geometry : new BufferGeometry();
if (existingMesh) {
existingMesh.visible = true;
}
@@ -65,8 +65,8 @@ export function createGeometryPool(parentScene: Group, material: Material) {
const vertices = new Float32Array(data.buffer, index * 4, vertexCount * 3);
index = index + vertexCount * 3;
let posAttribute = geometry.getAttribute(
"position",
const posAttribute = geometry.getAttribute(
'position'
) as BufferAttribute | null;
if (posAttribute && posAttribute.count === vertexCount) {
@@ -74,8 +74,8 @@ export function createGeometryPool(parentScene: Group, material: Material) {
posAttribute.needsUpdate = true;
} else {
geometry.setAttribute(
"position",
new Float32BufferAttribute(vertices, 3),
'position',
new Float32BufferAttribute(vertices, 3)
);
}
@@ -83,27 +83,27 @@ export function createGeometryPool(parentScene: Group, material: Material) {
index = index + vertexCount * 3;
if (
geometry.userData?.faceCount !== faceCount ||
geometry.userData?.vertexCount !== vertexCount
geometry.userData?.faceCount !== faceCount
|| geometry.userData?.vertexCount !== vertexCount
) {
// Add data to geometry
geometry.setIndex([...indices]);
}
const normalsAttribute = geometry.getAttribute(
"normal",
'normal'
) as BufferAttribute | null;
if (normalsAttribute && normalsAttribute.count === vertexCount) {
normalsAttribute.set(normals, 0);
normalsAttribute.needsUpdate = true;
} else {
geometry.setAttribute("normal", new Float32BufferAttribute(normals, 3));
geometry.setAttribute('normal', new Float32BufferAttribute(normals, 3));
}
geometry.userData = {
vertexCount,
faceCount,
hash,
hash
};
if (!existingMesh) {
@@ -119,7 +119,7 @@ export function createGeometryPool(parentScene: Group, material: Material) {
totalFaces = 0;
for (let i = 0; i < Math.max(newData.length, meshes.length); i++) {
const existingMesh = meshes[i];
let input = newData[i];
const input = newData[i];
if (input) {
updateSingleGeometry(input, existingMesh || null);
} else if (existingMesh) {
@@ -127,13 +127,13 @@ export function createGeometryPool(parentScene: Group, material: Material) {
}
}
return { totalVertices, totalFaces };
},
}
};
}
export function createInstancedGeometryPool(
parentScene: Group,
material: Material,
material: Material
) {
const scene = new Group();
parentScene.add(scene);
@@ -144,11 +144,11 @@ export function createInstancedGeometryPool(
function updateSingleInstance(
data: Int32Array,
existingInstance: InstancedMesh | null = null,
existingInstance: InstancedMesh | null = null
) {
let hash = fastArrayHash(data);
const hash = fastArrayHash(data);
let geometry = existingInstance
const geometry = existingInstance
? existingInstance.geometry
: new BufferGeometry();
@@ -169,8 +169,8 @@ export function createInstancedGeometryPool(
const indices = data.subarray(index, indicesEnd);
index = indicesEnd;
if (
geometry.userData?.faceCount !== faceCount ||
geometry.userData?.vertexCount !== vertexCount
geometry.userData?.faceCount !== faceCount
|| geometry.userData?.vertexCount !== vertexCount
) {
// Add data to geometry
geometry.setIndex([...indices]);
@@ -179,34 +179,34 @@ export function createInstancedGeometryPool(
// Vertices
const vertices = new Float32Array(data.buffer, index * 4, vertexCount * 3);
index = index + vertexCount * 3;
let posAttribute = geometry.getAttribute(
"position",
const posAttribute = geometry.getAttribute(
'position'
) as BufferAttribute | null;
if (posAttribute && posAttribute.count === vertexCount) {
posAttribute.set(vertices, 0);
posAttribute.needsUpdate = true;
} else {
geometry.setAttribute(
"position",
new Float32BufferAttribute(vertices, 3),
'position',
new Float32BufferAttribute(vertices, 3)
);
}
const normals = new Float32Array(data.buffer, index * 4, vertexCount * 3);
index = index + vertexCount * 3;
const normalsAttribute = geometry.getAttribute(
"normal",
'normal'
) as BufferAttribute | null;
if (normalsAttribute && normalsAttribute.count === vertexCount) {
normalsAttribute.set(normals, 0);
normalsAttribute.needsUpdate = true;
} else {
geometry.setAttribute("normal", new Float32BufferAttribute(normals, 3));
geometry.setAttribute('normal', new Float32BufferAttribute(normals, 3));
}
if (
existingInstance &&
instanceCount > existingInstance.geometry.userData.count
existingInstance
&& instanceCount > existingInstance.geometry.userData.count
) {
scene.remove(existingInstance);
instances.splice(instances.indexOf(existingInstance), 1);
@@ -226,12 +226,12 @@ export function createInstancedGeometryPool(
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),
matrices.subarray(i * 16, i * 16 + 16)
);
existingInstance.setMatrixAt(i, matrix);
}
@@ -241,9 +241,9 @@ export function createInstancedGeometryPool(
faceCount,
count: Math.max(
instanceCount,
existingInstance.geometry.userData.count || 0,
existingInstance.geometry.userData.count || 0
),
hash,
hash
};
existingInstance.instanceMatrix.needsUpdate = true;
@@ -255,7 +255,7 @@ export function createInstancedGeometryPool(
totalFaces = 0;
for (let i = 0; i < Math.max(newData.length, instances.length); i++) {
const existingMesh = instances[i];
let input = newData[i];
const input = newData[i];
if (input) {
updateSingleInstance(input, existingMesh || null);
} else if (existingMesh) {
@@ -263,6 +263,6 @@ export function createInstancedGeometryPool(
}
}
return { totalVertices, totalFaces };
},
}
};
}

View File

@@ -1,4 +1,3 @@
export * from "./runtime-executor"
export * from "./runtime-executor-cache"
export * from "./worker-runtime-executor"
export * from './runtime-executor';
export * from './runtime-executor-cache';
export * from './worker-runtime-executor';

View File

@@ -1,18 +1,18 @@
import type { Graph, RuntimeExecutor } from "@nodarium/types";
import type { Graph, RuntimeExecutor } from '@nodarium/types';
export class RemoteRuntimeExecutor implements RuntimeExecutor {
constructor(private url: string) {}
constructor(private url: string) { }
async execute(graph: Graph, settings: Record<string, any>): Promise<Int32Array> {
const res = await fetch(this.url, { method: "POST", body: JSON.stringify({ graph, settings }) });
async execute(graph: Graph, settings: Record<string, unknown>): Promise<Int32Array> {
const res = await fetch(this.url, {
method: 'POST',
body: JSON.stringify({ graph, settings })
});
if (!res.ok) {
throw new Error(`Failed to execute graph`);
}
return new Int32Array(await res.arrayBuffer());
}
}

View File

@@ -1,4 +1,4 @@
import { type SyncCache } from "@nodarium/types";
import { type SyncCache } from '@nodarium/types';
export class MemoryRuntimeCache implements SyncCache {
private map = new Map<string, unknown>();

View File

@@ -4,47 +4,47 @@ import type {
NodeInput,
NodeRegistry,
RuntimeExecutor,
SyncCache,
} from "@nodarium/types";
SyncCache
} from '@nodarium/types';
import {
concatEncodedArrays,
createLogger,
encodeFloat,
fastHashArrayBuffer,
type PerformanceStore,
} from "@nodarium/utils";
import type { RuntimeNode } from "./types";
type PerformanceStore
} from '@nodarium/utils';
import type { RuntimeNode } from './types';
const log = createLogger("runtime-executor");
const log = createLogger('runtime-executor');
log.mute();
function getValue(input: NodeInput, value?: unknown) {
if (value === undefined && "value" in input) {
if (value === undefined && 'value' in input) {
value = input.value;
}
if (input.type === "float") {
if (input.type === 'float') {
return encodeFloat(value as number);
}
if (Array.isArray(value)) {
if (input.type === "vec3") {
if (input.type === 'vec3') {
return [
0,
value.length + 1,
...value.map((v) => encodeFloat(v)),
1,
1,
1
] as number[];
}
return [0, value.length + 1, ...value, 1, 1] as number[];
}
if (typeof value === "boolean") {
if (typeof value === 'boolean') {
return value ? 1 : 0;
}
if (typeof value === "number") {
if (typeof value === 'number') {
return value;
}
@@ -64,14 +64,14 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
constructor(
private registry: NodeRegistry,
public cache?: SyncCache<Int32Array>,
public cache?: SyncCache<Int32Array>
) {
this.cache = undefined;
}
private async getNodeDefinitions(graph: Graph) {
if (this.registry.status !== "ready") {
throw new Error("Node registry is not ready");
if (this.registry.status !== 'ready') {
throw new Error('Node registry is not ready');
}
await this.registry.load(graph.nodes.map((node) => node.type));
@@ -98,26 +98,23 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
depth: 0,
children: [],
parents: [],
inputNodes: {},
}
return n
})
inputNodes: {}
};
return n;
});
const outputNode = graphNodes.find((node) =>
node.type.endsWith("/output"),
);
const outputNode = graphNodes.find((node) => node.type.endsWith('/output'));
if (!outputNode) {
throw new Error("No output node found");
throw new Error('No output node found');
}
const nodeMap = new Map(
graphNodes.map((node) => [node.id, node]),
graphNodes.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) {
const [parentId, _parentOutput, childId, childInput] = edge;
const [parentId, /*_parentOutput*/, childId, childInput] = edge;
const parent = nodeMap.get(parentId);
const child = nodeMap.get(childId);
if (parent && child) {
@@ -146,7 +143,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
}
async execute(graph: Graph, settings: Record<string, unknown>) {
this.perf?.addPoint("runtime");
this.perf?.addPoint('runtime');
let a = performance.now();
@@ -154,7 +151,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
const [outputNode, nodes] = await this.addMetaData(graph);
let b = performance.now();
this.perf?.addPoint("collect-metadata", b - a);
this.perf?.addPoint('collect-metadata', b - a);
/*
* Here we sort the nodes into buckets, which we then execute one by one
@@ -169,13 +166,13 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
// we execute the nodes from the bottom up
const sortedNodes = nodes.sort(
(a, b) => (b.state?.depth || 0) - (a.state?.depth || 0),
(a, b) => (b.state?.depth || 0) - (a.state?.depth || 0)
);
// here we store the intermediate results of the nodes
const results: Record<string, Int32Array> = {};
if (settings["randomSeed"]) {
if (settings['randomSeed']) {
this.seed = Math.floor(Math.random() * 100000000);
}
@@ -192,7 +189,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
// Collect the inputs for the node
const inputs = Object.entries(node_type.inputs || {}).map(
([key, input]) => {
if (input.type === "seed") {
if (input.type === 'seed') {
return this.seed;
}
@@ -206,7 +203,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
if (inputNode) {
if (results[inputNode.id] === undefined) {
throw new Error(
`Node ${node.type} is missing input from node ${inputNode.type}`,
`Node ${node.type} is missing input from node ${inputNode.type}`
);
}
return results[inputNode.id];
@@ -218,45 +215,45 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
}
return getValue(input);
},
}
);
b = performance.now();
this.perf?.addPoint("collected-inputs", b - a);
this.perf?.addPoint('collected-inputs', b - a);
try {
a = performance.now();
const encoded_inputs = concatEncodedArrays(inputs);
b = performance.now();
this.perf?.addPoint("encoded-inputs", b - a);
this.perf?.addPoint('encoded-inputs', b - a);
a = performance.now();
let inputHash = `node-${node.id}-${fastHashArrayBuffer(encoded_inputs)}`;
const inputHash = `node-${node.id}-${fastHashArrayBuffer(encoded_inputs)}`;
b = performance.now();
this.perf?.addPoint("hash-inputs", b - a);
this.perf?.addPoint('hash-inputs', b - a);
let cachedValue = this.cache?.get(inputHash);
const cachedValue = this.cache?.get(inputHash);
if (cachedValue !== undefined) {
log.log(`Using cached value for ${node_type.id || node.id}`);
this.perf?.addPoint("cache-hit", 1);
this.perf?.addPoint('cache-hit', 1);
results[node.id] = cachedValue as Int32Array;
continue;
}
this.perf?.addPoint("cache-hit", 0);
this.perf?.addPoint('cache-hit', 0);
log.group(`executing ${node_type.id}-${node.id}`);
log.log(`Inputs:`, inputs);
a = performance.now();
results[node.id] = node_type.execute(encoded_inputs);
log.log("Executed", node.type, node.id)
log.log('Executed', node.type, node.id);
b = performance.now();
if (this.cache && node.id !== outputNode.id) {
this.cache.set(inputHash, results[node.id]);
}
this.perf?.addPoint("node/" + node_type.id, b - a);
log.log("Result:", results[node.id]);
this.perf?.addPoint('node/' + node_type.id, b - a);
log.log('Result:', results[node.id]);
log.groupEnd();
} catch (e) {
log.groupEnd();
@@ -271,7 +268,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
this.cache.size = sortedNodes.length * 2;
}
this.perf?.endPoint("runtime");
this.perf?.endPoint('runtime');
return res as unknown as Int32Array;
}

View File

@@ -1,10 +1,10 @@
import type { SerializedNode } from "@nodarium/types";
import type { SerializedNode } from '@nodarium/types';
type RuntimeState = {
depth: number
parents: RuntimeNode[],
children: RuntimeNode[],
inputNodes: Record<string, RuntimeNode>
}
depth: number;
parents: RuntimeNode[];
children: RuntimeNode[];
inputNodes: Record<string, RuntimeNode>;
};
export type RuntimeNode = SerializedNode & { state: RuntimeState }
export type RuntimeNode = SerializedNode & { state: RuntimeState };

View File

@@ -1,13 +1,13 @@
import { MemoryRuntimeExecutor } from "./runtime-executor";
import { RemoteNodeRegistry, IndexDBCache } from "@nodarium/registry";
import type { Graph } from "@nodarium/types";
import { createPerformanceStore } from "@nodarium/utils";
import { MemoryRuntimeCache } from "./runtime-executor-cache";
import { IndexDBCache, RemoteNodeRegistry } from '$lib/node-registry/index';
import type { Graph } from '@nodarium/types';
import { createPerformanceStore } from '@nodarium/utils';
import { MemoryRuntimeExecutor } from './runtime-executor';
import { MemoryRuntimeCache } from './runtime-executor-cache';
const indexDbCache = new IndexDBCache("node-registry");
const nodeRegistry = new RemoteNodeRegistry("", indexDbCache);
const indexDbCache = new IndexDBCache('node-registry');
const nodeRegistry = new RemoteNodeRegistry('', indexDbCache);
const cache = new MemoryRuntimeCache()
const cache = new MemoryRuntimeCache();
const executor = new MemoryRuntimeExecutor(nodeRegistry, cache);
const performanceStore = createPerformanceStore();
@@ -31,11 +31,11 @@ export async function setUseRuntimeCache(useCache: boolean) {
export async function executeGraph(
graph: Graph,
settings: Record<string, unknown>,
settings: Record<string, unknown>
): Promise<Int32Array> {
await nodeRegistry.load(graph.nodes.map((n) => n.type));
performanceStore.startRun();
let res = await executor.execute(graph, settings);
const res = await executor.execute(graph, settings);
performanceStore.stopRun();
return res;
}

View File

@@ -1,9 +1,10 @@
/// <reference types="vite-plugin-comlink/client" />
import type { Graph, RuntimeExecutor } from "@nodarium/types";
import type { Graph, RuntimeExecutor } from '@nodarium/types';
export class WorkerRuntimeExecutor implements RuntimeExecutor {
private worker = new ComlinkWorker<typeof import('./worker-runtime-executor-backend.ts')>(new URL(`./worker-runtime-executor-backend.ts`, import.meta.url));
private worker = new ComlinkWorker<typeof import('./worker-runtime-executor-backend.ts')>(
new URL(`./worker-runtime-executor-backend.ts`, import.meta.url)
);
async execute(graph: Graph, settings: Record<string, unknown>) {
return this.worker.executeGraph(graph, settings);
@@ -18,4 +19,3 @@ export class WorkerRuntimeExecutor implements RuntimeExecutor {
this.worker.setUseRegistryCache(useCache);
}
}

View File

@@ -1,10 +1,11 @@
<script lang="ts">
import NestedSettings from "./NestedSettings.svelte";
import { localState } from "$lib/helpers/localState.svelte";
import type { NodeInput } from "@nodarium/types";
import Input from "@nodarium/ui";
import { localState } from '$lib/helpers/localState.svelte';
import type { NodeInput } from '@nodarium/types';
import Input from '@nodarium/ui';
import { onMount } from 'svelte';
import NestedSettings from './NestedSettings.svelte';
type Button = { type: "button"; callback: () => void; label?: string };
type Button = { type: 'button'; label?: string };
type InputType = NodeInput | Button;
@@ -12,7 +13,7 @@
interface SettingsGroup {
title?: string;
[key: string]: any;
[key: string]: unknown;
}
type SettingsType = Record<string, SettingsNode>;
@@ -31,44 +32,49 @@
};
// Local persistent state for <details> sections
const openSections = localState<Record<string, boolean>>("open-details", {});
const openSections = localState<Record<string, boolean>>('open-details', {});
let { id, key = "", value = $bindable(), type, depth = 0 }: Props = $props();
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;
return !!v && typeof v === 'object' && 'type' in v;
}
function getDefaultValue(): unknown {
if (key === "" || key === "title") return;
function getDefaultValue(): NodeInput['value'] | undefined {
if (key === '' || key === 'title') return;
const node = type[key];
const node = type[key] as SettingsNode;
const inputValue = value[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]);
if ('options' in node && Array.isArray(node.options)) {
if (typeof inputValue === 'string') {
return node.options.indexOf(inputValue);
}
return 0;
}
if (value?.[key] !== undefined) return value[key];
// If the component is supplied with a default value use that
if (inputValue !== undefined && typeof inputValue !== 'object') {
return inputValue;
}
if ("value" in node && anyNode.value !== undefined) {
return anyNode.value;
if ('value' in node) {
const nodeValue = node.value;
if (nodeValue !== null && nodeValue !== undefined) {
return nodeValue;
}
}
switch (node.type) {
case "boolean":
case 'boolean':
return 0;
case "float":
case 'float':
return 0.5;
case "integer":
case "select":
case 'integer':
case 'select':
return 0;
default:
return 0;
@@ -77,52 +83,63 @@
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;
}
});
}
let open = $state(false);
// Sync internalValue back into `value`
$effect(() => {
if (key === "" || internalValue === undefined) return;
if (key === '' || internalValue === undefined) return;
const node = type[key];
if (
isNodeInput(node) &&
Array.isArray((node as any).options) &&
typeof internalValue === "number"
isNodeInput(node)
&& 'options' in node
&& Array.isArray(node.options)
&& typeof internalValue === 'number'
) {
value[key] = (node as any)?.options?.[internalValue] as any;
} else {
value[key] = internalValue as any;
value[key] = node?.options?.[internalValue];
} else if (internalValue) {
value[key] = internalValue;
}
});
function handleClick() {
const callback = value[key] as unknown as () => void;
callback();
}
onMount(() => {
open = openSections.value[id];
// Persist <details> open/closed state for groups
if (depth > 0 && !isNodeInput(type[key!])) {
$effect(() => {
if (open !== undefined) {
openSections.value[id] = open;
}
});
}
});
</script>
{#if key && isNodeInput(type?.[key])}
{@const inputType = type[key]}
<!-- Leaf input -->
<div class="input input-{type[key].type}" class:first-level={depth === 1}>
{#if type[key].type === "button"}
<button onclick={() => "callback" in type[key] && type[key].callback()}>
{type[key].label || key}
<div class="input input-{inputType.type}" class:first-level={depth === 1}>
{#if inputType.type === 'button'}
<button onclick={handleClick}>
{inputType.label || key}
</button>
{:else}
{#if type[key].label !== ""}
<label for={id}>{type[key].label || key}</label>
{#if inputType.label !== ''}
<label for={id}>{inputType.label || key}</label>
{/if}
<Input {id} input={type[key]} bind:value={internalValue} />
<Input {id} input={inputType} 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}
{#each Object.keys(type ?? {}).filter((k) => k !== 'title') as childKey (childKey)}
<NestedSettings
id={`${id}.${childKey}`}
key={childKey}
@@ -140,7 +157,7 @@
<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}
{#each Object.keys(type[key] as SettingsGroup).filter((k) => k !== 'title') as childKey (childKey)}
<NestedSettings
id={`${id}.${childKey}`}
key={childKey}

View File

@@ -1,169 +1,163 @@
import { localState } from "$lib/helpers/localState.svelte";
import { localState } from '$lib/helpers/localState.svelte';
const themes = [
"dark",
"light",
"catppuccin",
"solarized",
"high-contrast",
"nord",
"dracula",
'dark',
'light',
'catppuccin',
'solarized',
'high-contrast',
'nord',
'dracula'
] as const;
export const AppSettingTypes = {
theme: {
type: "select",
type: 'select',
options: themes,
label: "Theme",
value: themes[0],
label: 'Theme',
value: themes[0]
},
showGrid: {
type: "boolean",
label: "Show Grid",
value: true,
type: 'boolean',
label: 'Show Grid',
value: true
},
centerCamera: {
type: "boolean",
label: "Center Camera",
value: true,
type: 'boolean',
label: 'Center Camera',
value: true
},
nodeInterface: {
title: "Node Interface",
title: 'Node Interface',
showNodeGrid: {
type: "boolean",
label: "Show Grid",
value: true,
type: 'boolean',
label: 'Show Grid',
value: true
},
snapToGrid: {
type: "boolean",
label: "Snap to Grid",
value: true,
type: 'boolean',
label: 'Snap to Grid',
value: true
},
showHelp: {
type: "boolean",
label: "Show Help",
value: false,
},
type: 'boolean',
label: 'Show Help',
value: false
}
},
debug: {
title: "Debug",
title: 'Debug',
wireframe: {
type: "boolean",
label: "Wireframe",
value: false,
type: 'boolean',
label: 'Wireframe',
value: false
},
useWorker: {
type: "boolean",
label: "Execute in WebWorker",
value: true,
type: 'boolean',
label: 'Execute in WebWorker',
value: true
},
showIndices: {
type: "boolean",
label: "Show Indices",
value: false,
type: 'boolean',
label: 'Show Indices',
value: false
},
showPerformancePanel: {
type: "boolean",
label: "Show Performance Panel",
value: false,
type: 'boolean',
label: 'Show Performance Panel',
value: false
},
showBenchmarkPanel: {
type: "boolean",
label: "Show Benchmark Panel",
value: false,
type: 'boolean',
label: 'Show Benchmark Panel',
value: false
},
showVertices: {
type: "boolean",
label: "Show Vertices",
value: false,
type: 'boolean',
label: 'Show Vertices',
value: false
},
showStemLines: {
type: "boolean",
label: "Show Stem Lines",
value: false,
type: 'boolean',
label: 'Show Stem Lines',
value: false
},
showGraphJson: {
type: "boolean",
label: "Show Graph Source",
value: false,
type: 'boolean',
label: 'Show Graph Source',
value: false
},
cache: {
title: "Cache",
title: 'Cache',
useRuntimeCache: {
type: "boolean",
label: "Node Results",
value: true,
type: 'boolean',
label: 'Node Results',
value: true
},
useRegistryCache: {
type: "boolean",
label: "Node Source",
value: true,
},
type: 'boolean',
label: 'Node Source',
value: true
}
},
stressTest: {
title: "Stress Test",
title: 'Stress Test',
amount: {
type: "integer",
type: 'integer',
min: 2,
max: 15,
value: 4,
value: 4
},
loadGrid: {
type: "button",
label: "Load Grid",
type: 'button',
label: 'Load Grid'
},
loadTree: {
type: "button",
label: "Load Tree",
type: 'button',
label: 'Load Tree'
},
lottaFaces: {
type: "button",
label: "Load 'lots of faces'",
type: 'button',
label: "Load 'lots of faces'"
},
lottaNodes: {
type: "button",
label: "Load 'lots of nodes'",
type: 'button',
label: "Load 'lots of nodes'"
},
lottaNodesAndFaces: {
type: "button",
label: "Load 'lots of nodes and faces'",
},
},
},
type: 'button',
label: "Load 'lots of nodes and faces'"
}
}
}
} as const;
type SettingsToStore<T> =
T extends { value: infer V }
? V extends readonly string[]
? V[number]
: V
: T extends any[]
? {}
: T extends object
? {
[K in keyof T as T[K] extends object ? K : never]:
SettingsToStore<T[K]>
}
type SettingsToStore<T> = T extends { type: 'button' } ? () => void
: T extends { value: infer V } ? V extends readonly string[] ? V[number]
: V
: T extends object ? {
-readonly [K in keyof T as T[K] extends object ? K : never]: SettingsToStore<T[K]>;
}
: never;
export function settingsToStore<T>(settings: T): SettingsToStore<T> {
const result = {} as any;
const result = {} as Record<string, unknown>;
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);
}
}
}
return result;
return result as SettingsToStore<T>;
}
export let appSettings = localState(
"app-settings",
settingsToStore(AppSettingTypes),
export const appSettings = localState(
'app-settings',
settingsToStore(AppSettingTypes)
);
$effect.root(() => {
@@ -173,7 +167,7 @@ $effect.root(() => {
const newClassName = `theme-${theme}`;
if (classes) {
for (const className of classes) {
if (className.startsWith("theme-") && className !== newClassName) {
if (className.startsWith('theme-') && className !== newClassName) {
classes.remove(className);
}
}

View File

@@ -1,6 +1,6 @@
import type { NodeInput } from "@nodarium/types";
import type { NodeInput } from '@nodarium/types';
type Button = { type: "button"; label?: string };
type Button = { type: 'button'; label?: string };
export type SettingsStore = {
[key: string]: SettingsStore | string | number | boolean;
@@ -23,5 +23,5 @@ export type SettingsValue = Record<
>;
export function isNodeInput(v: SettingsNode | undefined): v is InputType {
return !!v && "type" in v;
return !!v && 'type' in v;
}

View File

@@ -1,14 +1,14 @@
<script lang="ts">
import { type Snippet } from "svelte";
import { panelState } from "./PanelState.svelte";
import { type Snippet } from 'svelte';
import { type Panel, panelState } from './PanelState.svelte';
const {
id,
icon = "",
title = "",
classes = "",
icon = '',
title = '',
classes = '',
hidden,
children,
children
} = $props<{
id: string;
icon?: string;
@@ -18,7 +18,13 @@
children?: Snippet;
}>();
const panel = panelState.registerPanel(id, icon, classes, hidden);
let panel = $state<Panel>(null!);
$effect(() => {
panelState.unregisterPanel(id);
panel = panelState.registerPanel(id, icon, classes, hidden);
});
$effect(() => {
panel.hidden = hidden;
});

View File

@@ -1,6 +1,6 @@
import { localState } from '$lib/helpers/localState.svelte';
type Panel = {
export type Panel = {
icon: string;
classes: string;
hidden?: boolean;
@@ -14,6 +14,10 @@ class PanelState {
return Object.keys(this.panels);
}
public unregisterPanel(id: string) {
delete this.panels[id];
}
public registerPanel(id: string, icon: string, classes: string, hidden: boolean): Panel {
const state = $state({
icon: icon,

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { type Snippet } from "svelte";
import { panelState as state } from "./PanelState.svelte";
import { type Snippet } from 'svelte';
import { panelState as state } from './PanelState.svelte';
const { children } = $props<{ children?: Snippet }>();
</script>
@@ -19,8 +19,7 @@
class:active={panelId === state.activePanel.value}
onclick={() => (state.activePanel.value = panelId)}
>
<span class={`block w-6 h-6 iconify ${state.panels[panelId].icon}`}
></span>
<span class={`block w-6 h-6 iconify ${state.panels[panelId].icon}`}></span>
</button>
{/if}
{/each}
@@ -40,9 +39,7 @@
height: 100%;
right: 0px;
transform: translateX(calc(100% - 30px));
transition:
transform 0.2s,
background 0.2s ease;
transition: transform 0.2s, background 0.2s ease;
width: 30%;
min-width: 350px;
}

View File

@@ -1,7 +1,12 @@
<script lang="ts">
import type { NodeInstance, NodeInput } from "@nodarium/types";
import NestedSettings from "$lib/settings/NestedSettings.svelte";
import type { GraphManager } from "$lib/graph-interface/graph-manager.svelte";
import type { GraphManager } from '$lib/graph-interface/graph-manager.svelte';
import NestedSettings from '$lib/settings/NestedSettings.svelte';
import type { NodeId, NodeInput, NodeInstance } from '@nodarium/types';
type InternalNodeInput = NodeInput & {
__node_type?: NodeId;
__node_input: string;
};
type Props = {
manager: GraphManager;
@@ -11,40 +16,44 @@
const { manager, node = $bindable() }: Props = $props();
function filterInputs(inputs?: Record<string, NodeInput>) {
const _inputs = $state.snapshot(inputs);
const _inputs = $state.snapshot(
inputs as Record<string, InternalNodeInput>
);
return Object.fromEntries(
Object.entries(structuredClone(_inputs ?? {}))
.filter(([_key, value]) => {
.filter(([, value]) => {
return value.hidden === true;
})
.map(([key, value]) => {
//@ts-ignore
value.__node_type = node.state?.type.id;
//@ts-ignore
value.__node_type = node.state.type?.id;
value.__node_input = key;
return [key, value];
}),
})
);
}
const nodeDefinition = filterInputs(node.state?.type?.inputs);
const nodeDefinition = filterInputs(node.state.type?.inputs);
type Store = Record<string, number | number[]>;
let store = $state<Store>(createStore(node?.props, nodeDefinition));
function createStore(
props: NodeInstance["props"],
inputs: Record<string, NodeInput>,
props: NodeInstance['props'],
inputs: Record<string, NodeInput>
): Store {
const store: Store = {};
Object.keys(inputs).forEach((key) => {
if (props) {
//@ts-ignore
store[key] = props[key] || inputs[key].value;
const value = props[key] || inputs[key].value;
if (Array.isArray(value) || typeof value === 'number') {
store[key] = value;
} else {
console.error('Wrong error');
}
}
});
return store;
}
let lastPropsHash = "";
let lastPropsHash = '';
function updateNode() {
if (!node || !store) return;
let needsUpdate = false;
@@ -53,7 +62,10 @@
const key = _key as keyof typeof store;
if (node && store) {
needsUpdate = true;
node.props[key] = store[key];
const value = store[key];
if (value !== undefined) {
node.props[key] = value;
}
}
});

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import type { NodeInstance } from "@nodarium/types";
import type { GraphManager } from "$lib/graph-interface/graph-manager.svelte";
import ActiveNodeSelected from "./ActiveNodeSelected.svelte";
import type { GraphManager } from '$lib/graph-interface/graph-manager.svelte';
import type { NodeInstance } from '@nodarium/types';
import ActiveNodeSelected from './ActiveNodeSelected.svelte';
type Props = {
manager: GraphManager;

View File

@@ -1,148 +1,148 @@
<script lang="ts" module>
let result:
| { stdev: number; avg: number; duration: number; samples: number[] }
| undefined = $state();
let result:
| { stdev: number; avg: number; duration: number; samples: number[] }
| undefined = $state();
</script>
<script lang="ts">
import { humanizeDuration } from '$lib/helpers';
import { localState } from '$lib/helpers/localState.svelte';
import Monitor from '$lib/performance/Monitor.svelte';
import { Number } from '@nodarium/ui';
import { writable } from 'svelte/store';
import { humanizeDuration } from '$lib/helpers';
import { localState } from '$lib/helpers/localState.svelte';
import Monitor from '$lib/performance/Monitor.svelte';
import { Float } from '@nodarium/ui';
import { writable } from 'svelte/store';
function calculateStandardDeviation(array: number[]) {
const n = array.length;
const mean = array.reduce((a, b) => a + b) / n;
return Math.sqrt(
array.map((x) => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n
);
}
type Props = {
run: () => Promise<any>;
};
function calculateStandardDeviation(array: number[]) {
const n = array.length;
const mean = array.reduce((a, b) => a + b) / n;
return Math.sqrt(
array.map((x) => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n
);
}
type Props = {
run: () => Promise<unknown>;
};
const { run }: Props = $props();
const { run }: Props = $props();
let isRunning = $state(false);
let amount = localState<number>('nodes.benchmark.samples', 500);
let samples = $state(0);
let warmUp = writable(0);
let warmUpAmount = 10;
let status = '';
let isRunning = $state(false);
let amount = localState<number>('nodes.benchmark.samples', 500);
let samples = $state(0);
let warmUp = writable(0);
let warmUpAmount = 10;
let status = '';
const copyContent = async (text?: string | number) => {
if (!text) return;
if (typeof text !== 'string') {
text = (Math.floor(text * 100) / 100).toString();
}
try {
await navigator.clipboard.writeText(text);
} catch (err) {
console.error('Failed to copy: ', err);
}
};
const copyContent = async (text?: string | number) => {
if (!text) return;
if (typeof text !== 'string') {
text = (Math.floor(text * 100) / 100).toString();
}
try {
await navigator.clipboard.writeText(text);
} catch (err) {
console.error('Failed to copy: ', err);
}
};
async function benchmark() {
if (isRunning) return;
isRunning = true;
result = undefined;
samples = 0;
$warmUp = 0;
async function benchmark() {
if (isRunning) return;
isRunning = true;
result = undefined;
samples = 0;
$warmUp = 0;
await new Promise((r) => setTimeout(r, 50));
await new Promise((r) => setTimeout(r, 50));
// warm up
for (let i = 0; i < warmUpAmount; i++) {
await run();
$warmUp = i + 1;
}
// warm up
for (let i = 0; i < warmUpAmount; i++) {
await run();
$warmUp = i + 1;
}
let a = performance.now();
let results = [];
let a = performance.now();
let results = [];
// perform run
for (let i = 0; i < amount.value; i++) {
const a = performance.now();
await run();
samples = i;
const b = performance.now();
await new Promise((r) => setTimeout(r, 20));
results.push(b - a);
}
result = {
stdev: calculateStandardDeviation(results),
samples: results,
duration: performance.now() - a,
avg: results.reduce((a, b) => a + b) / results.length
};
}
// perform run
for (let i = 0; i < amount.value; i++) {
const a = performance.now();
await run();
samples = i;
const b = performance.now();
await new Promise((r) => setTimeout(r, 20));
results.push(b - a);
}
result = {
stdev: calculateStandardDeviation(results),
samples: results,
duration: performance.now() - a,
avg: results.reduce((a, b) => a + b) / results.length
};
}
</script>
{status}
<div class="wrapper" class:running={isRunning}>
{#if result}
<h3>Finished ({humanizeDuration(result.duration)})</h3>
<div class="monitor-wrapper">
<Monitor points={result.samples} />
</div>
<label for="bench-avg">Average </label>
<button
id="bench-avg"
onkeydown={(ev) => ev.key === 'Enter' && copyContent(result?.avg)}
onclick={() => copyContent(result?.avg)}
>
{Math.floor(result.avg * 100) / 100}
</button>
<i
role="button"
tabindex="0"
onkeydown={(ev) => ev.key === 'Enter' && copyContent(result?.avg)}
onclick={() => copyContent(result?.avg)}
>(click to copy)</i>
<label for="bench-stdev">Standard Deviation σ</label>
<button id="bench-stdev" onclick={() => copyContent(result?.stdev)}>
{Math.floor(result.stdev * 100) / 100}
</button>
<i
role="button"
tabindex="0"
onkeydown={(ev) => ev.key === 'Enter' && copyContent(result?.avg)}
onclick={() => copyContent(result?.stdev + '')}
>(click to copy)</i>
<div>
<button onclick={() => (isRunning = false)}>reset</button>
</div>
{:else if isRunning}
<p>WarmUp ({$warmUp}/{warmUpAmount})</p>
<progress value={$warmUp} max={warmUpAmount}>
{Math.floor(($warmUp / warmUpAmount) * 100)}%
</progress>
<p>Progress ({samples}/{amount.value})</p>
<progress value={samples} max={amount.value}>
{Math.floor((samples / amount.value) * 100)}%
</progress>
{:else}
<label for="bench-samples">Samples</label>
<Number id="bench-sample" bind:value={amount.value} max={1000} />
<button onclick={benchmark} disabled={isRunning}>start</button>
{/if}
{#if result}
<h3>Finished ({humanizeDuration(result.duration)})</h3>
<div class="monitor-wrapper">
<Monitor points={result.samples} />
</div>
<label for="bench-avg">Average </label>
<button
id="bench-avg"
onkeydown={(ev) => ev.key === 'Enter' && copyContent(result?.avg)}
onclick={() => copyContent(result?.avg)}
>
{Math.floor(result.avg * 100) / 100}
</button>
<i
role="button"
tabindex="0"
onkeydown={(ev) => ev.key === 'Enter' && copyContent(result?.avg)}
onclick={() => copyContent(result?.avg)}
>(click to copy)</i>
<label for="bench-stdev">Standard Deviation σ</label>
<button id="bench-stdev" onclick={() => copyContent(result?.stdev)}>
{Math.floor(result.stdev * 100) / 100}
</button>
<i
role="button"
tabindex="0"
onkeydown={(ev) => ev.key === 'Enter' && copyContent(result?.avg)}
onclick={() => copyContent(result?.stdev + '')}
>(click to copy)</i>
<div>
<button onclick={() => (isRunning = false)}>reset</button>
</div>
{:else if isRunning}
<p>WarmUp ({$warmUp}/{warmUpAmount})</p>
<progress value={$warmUp} max={warmUpAmount}>
{Math.floor(($warmUp / warmUpAmount) * 100)}%
</progress>
<p>Progress ({samples}/{amount.value})</p>
<progress value={samples} max={amount.value}>
{Math.floor((samples / amount.value) * 100)}%
</progress>
{:else}
<label for="bench-samples">Samples</label>
<Float id="bench-sample" bind:value={amount.value} max={1000} />
<button onclick={benchmark} disabled={isRunning}>start</button>
{/if}
</div>
<style>
.wrapper {
padding: 1em;
display: flex;
flex-direction: column;
gap: 1em;
}
.monitor-wrapper {
border: solid thin var(--outline);
border-bottom: none;
}
i {
opacity: 0.5;
font-size: 0.8em;
}
.wrapper {
padding: 1em;
display: flex;
flex-direction: column;
gap: 1em;
}
.monitor-wrapper {
border: solid thin var(--outline);
border-bottom: none;
}
i {
opacity: 0.5;
font-size: 0.8em;
}
</style>

View File

@@ -1,27 +1,26 @@
<script lang="ts">
import type { Group } from "three";
import type { OBJExporter } from "three/addons/exporters/OBJExporter.js";
import type { GLTFExporter } from "three/addons/exporters/GLTFExporter.js";
import FileSaver from "file-saver";
import FileSaver from 'file-saver';
import type { Group } from 'three';
import type { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js';
import type { OBJExporter } from 'three/addons/exporters/OBJExporter.js';
// Download
const download = (
data: ArrayBuffer | string,
name: string,
mimetype: string,
extension: string,
extension: string
) => {
const blob = new Blob([data], { type: mimetype + ";charset=utf-8" });
FileSaver.saveAs(blob, name + "." + extension);
const blob = new Blob([data], { type: mimetype + ';charset=utf-8' });
FileSaver.saveAs(blob, name + '.' + extension);
};
const { scene } = $props<{ scene: Group }>();
let gltfExporter: GLTFExporter;
async function exportGltf() {
const exporter =
gltfExporter ||
(await import("three/addons/exporters/GLTFExporter.js").then((m) => {
const exporter = gltfExporter
|| (await import('three/addons/exporters/GLTFExporter.js').then((m) => {
gltfExporter = new m.GLTFExporter();
return gltfExporter;
}));
@@ -30,30 +29,29 @@
scene,
(gltf) => {
// download .gltf file
download(gltf as ArrayBuffer, "plant", "text/plain", "gltf");
download(gltf as ArrayBuffer, 'plant', 'text/plain', 'gltf');
},
(err) => {
console.log(err);
},
}
);
}
let objExporter: OBJExporter;
async function exportObj() {
const exporter =
objExporter ||
(await import("three/addons/exporters/OBJExporter.js").then((m) => {
const exporter = objExporter
|| (await import('three/addons/exporters/OBJExporter.js').then((m) => {
objExporter = new m.OBJExporter();
return objExporter;
}));
const result = exporter.parse(scene);
// download .obj file
download(result, "plant", "text/plain", "obj");
download(result, 'plant', 'text/plain', 'obj');
}
</script>
<div class="p-4">
<button onclick={exportObj}> export obj </button>
<button onclick={exportGltf}> export gltf </button>
<button onclick={exportObj}>export obj</button>
<button onclick={exportGltf}>export gltf</button>
</div>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import type { Graph } from "$lib/types";
import type { Graph } from '$lib/types';
const { graph }: { graph?: Graph } = $props();
@@ -7,14 +7,14 @@
return JSON.stringify(
{
...g,
nodes: g.nodes.map((n: any) => ({ ...n, tmp: undefined })),
nodes: g.nodes.map((n: object) => ({ ...n, tmp: undefined }))
},
null,
2,
2
);
}
</script>
<pre>
{graph ? convert(graph) : 'No graph loaded'}
{graph ? convert(graph) : "No graph loaded"}
</pre>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import type { createKeyMap } from "$lib/helpers/createKeyMap";
import { ShortCut } from "@nodarium/ui";
import { get } from "svelte/store";
import type { createKeyMap, ShortCut } from '$lib/helpers/createKeyMap';
import { ShortCut as ShortCutEl } from '@nodarium/ui';
import { get } from 'svelte/store';
type Props = {
keymaps: {
@@ -11,22 +11,26 @@
};
let { keymaps }: Props = $props();
function getKeyKey(key: ShortCut) {
return (key?.alt ? 'alt-' : '') + (key?.ctrl ? 'ctrl-' : '') + key.key;
}
</script>
<div class="p-4">
<table class="wrapper">
<tbody>
{#each keymaps as keymap}
{#each keymaps as keymap (keymap.title)}
<tr>
<td colspan="2">
<h3>{keymap.title}</h3>
</td>
</tr>
{#each get(keymap.keymap?.keys) as key}
{#each get(keymap.keymap?.keys) as key (getKeyKey(key))}
<tr>
{#if key.description}
<td class="command-wrapper">
<ShortCut
<ShortCutEl
alt={key.alt}
ctrl={key.ctrl}
shift={key.shift}

View File

@@ -1,6 +1,2 @@
import type {
Graph,
NodeDefinition,
NodeInput,
} from "@nodarium/types";
import type { Graph, NodeDefinition, NodeInput } from '@nodarium/types';
export type { Graph, NodeDefinition, NodeInput };

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import "@nodarium/ui/app.css";
import "../app.css";
import type { Snippet } from "svelte";
import * as config from "$lib/config";
import '@nodarium/ui/app.css';
import '../app.css';
import * as config from '$lib/config';
import type { Snippet } from 'svelte';
const { children } = $props<{ children?: Snippet }>();
</script>
@@ -10,6 +10,7 @@
<svelte:head>
{#if config.ANALYTIC_SCRIPT}
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html config.ANALYTIC_SCRIPT}
{/if}
</svelte:head>

View File

@@ -1,2 +1,2 @@
export const prerender = true
export const ssr = false
export const prerender = true;
export const ssr = false;

View File

@@ -1,40 +1,35 @@
<script lang="ts">
import Grid from "$lib/grid";
import GraphInterface from "$lib/graph-interface";
import * as templates from "$lib/graph-templates";
import type { Graph, NodeInstance } from "@nodarium/types";
import Viewer from "$lib/result-viewer/Viewer.svelte";
import {
appSettings,
AppSettingTypes,
} from "$lib/settings/app-settings.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/sidebar/panels/ActiveNodeSettings.svelte";
import PerformanceViewer from "$lib/performance/PerformanceViewer.svelte";
import Panel from "$lib/sidebar/Panel.svelte";
import NestedSettings from "$lib/settings/NestedSettings.svelte";
import type { Group } from "three";
import ExportSettings from "$lib/sidebar/panels/ExportSettings.svelte";
import {
MemoryRuntimeCache,
WorkerRuntimeExecutor,
MemoryRuntimeExecutor,
} from "$lib/runtime";
import { IndexDBCache, RemoteNodeRegistry } from "@nodarium/registry";
import { createPerformanceStore } from "@nodarium/utils";
import BenchmarkPanel from "$lib/sidebar/panels/BenchmarkPanel.svelte";
import { debounceAsyncFunction } from "$lib/helpers";
import GraphSource from "$lib/sidebar/panels/GraphSource.svelte";
import { ProjectManager } from "$lib/project-manager/project-manager.svelte";
import ProjectManagerEl from "$lib/project-manager/ProjectManager.svelte";
import GraphInterface from '$lib/graph-interface';
import * as templates from '$lib/graph-templates';
import Grid from '$lib/grid';
import { debounceAsyncFunction } from '$lib/helpers';
import { createKeyMap } from '$lib/helpers/createKeyMap';
import { IndexDBCache, RemoteNodeRegistry } from '$lib/node-registry/index';
import NodeStore from '$lib/node-store/NodeStore.svelte';
import PerformanceViewer from '$lib/performance/PerformanceViewer.svelte';
import { ProjectManager } from '$lib/project-manager/project-manager.svelte';
import ProjectManagerEl from '$lib/project-manager/ProjectManager.svelte';
import Viewer from '$lib/result-viewer/Viewer.svelte';
import { MemoryRuntimeCache, MemoryRuntimeExecutor, WorkerRuntimeExecutor } from '$lib/runtime';
import type { SettingsValue } from '$lib/settings';
import { appSettings, AppSettingTypes } from '$lib/settings/app-settings.svelte';
import NestedSettings from '$lib/settings/NestedSettings.svelte';
import Panel from '$lib/sidebar/Panel.svelte';
import ActiveNodeSettings from '$lib/sidebar/panels/ActiveNodeSettings.svelte';
import BenchmarkPanel from '$lib/sidebar/panels/BenchmarkPanel.svelte';
import ExportSettings from '$lib/sidebar/panels/ExportSettings.svelte';
import GraphSource from '$lib/sidebar/panels/GraphSource.svelte';
import Keymap from '$lib/sidebar/panels/Keymap.svelte';
import Sidebar from '$lib/sidebar/Sidebar.svelte';
import type { Graph, NodeInstance } from '@nodarium/types';
import { createPerformanceStore } from '@nodarium/utils';
import { onMount } from 'svelte';
import type { Group } from 'three';
let performanceStore = createPerformanceStore();
const registryCache = new IndexDBCache("node-registry");
const nodeRegistry = new RemoteNodeRegistry("", registryCache);
const registryCache = new IndexDBCache('node-registry');
const nodeRegistry = new RemoteNodeRegistry('', registryCache);
const workerRuntime = new WorkerRuntimeExecutor();
const runtimeCache = new MemoryRuntimeCache();
const memoryRuntime = new MemoryRuntimeExecutor(nodeRegistry, runtimeCache);
@@ -42,14 +37,12 @@
const pm = new ProjectManager();
const runtime = $derived(
appSettings.value.debug.useWorker ? workerRuntime : memoryRuntime,
appSettings.value.debug.useWorker ? workerRuntime : memoryRuntime
);
$effect(() => {
workerRuntime.useRegistryCache =
appSettings.value.debug.cache.useRuntimeCache;
workerRuntime.useRuntimeCache =
appSettings.value.debug.cache.useRegistryCache;
workerRuntime.useRegistryCache = appSettings.value.debug.cache.useRuntimeCache;
workerRuntime.useRuntimeCache = appSettings.value.debug.cache.useRegistryCache;
if (appSettings.value.debug.cache.useRegistryCache) {
nodeRegistry.cache = registryCache;
@@ -80,15 +73,15 @@
let applicationKeymap = createKeyMap([
{
key: "r",
description: "Regenerate the plant model",
callback: () => randomGenerate(),
},
key: 'r',
description: 'Regenerate the plant model',
callback: () => randomGenerate()
}
]);
let graphSettings = $state<Record<string, any>>({});
let graphSettings = $state<SettingsValue>({});
let graphSettingTypes = $state({
randomSeed: { type: "boolean", value: false },
randomSeed: { type: 'boolean', value: false }
});
$effect(() => {
if (graphSettings && graphSettingTypes) {
@@ -98,7 +91,7 @@
async function update(
g: Graph,
s: Record<string, any> = $state.snapshot(graphSettings),
s: Record<string, unknown> = $state.snapshot(graphSettings)
) {
performanceStore.startRun();
try {
@@ -114,14 +107,14 @@
delete lastRun.total;
performanceStore.mergeData(lastRun);
performanceStore.addPoint(
"worker-transfer",
b - a - lastRun.runtime[0],
'worker-transfer',
b - a - lastRun.runtime[0]
);
}
}
viewerComponent?.update(graphResult);
} catch (error) {
console.log("errors", error);
console.log('errors', error);
} finally {
performanceStore.stopRun();
}
@@ -129,31 +122,29 @@
const handleUpdate = debounceAsyncFunction(update);
$effect(() => {
//@ts-ignore
AppSettingTypes.debug.stressTest.loadGrid.callback = () => {
manager.load(
templates.grid(
appSettings.value.debug.stressTest.amount,
appSettings.value.debug.stressTest.amount,
),
);
};
//@ts-ignore
AppSettingTypes.debug.stressTest.loadTree.callback = () => {
manager.load(templates.tree(appSettings.value.debug.stressTest.amount));
};
//@ts-ignore
AppSettingTypes.debug.stressTest.lottaFaces.callback = () => {
manager.load(templates.lottaFaces as unknown as Graph);
};
//@ts-ignore
AppSettingTypes.debug.stressTest.lottaNodes.callback = () => {
manager.load(templates.lottaNodes as unknown as Graph);
};
//@ts-ignore
AppSettingTypes.debug.stressTest.lottaNodesAndFaces.callback = () => {
manager.load(templates.lottaNodesAndFaces as unknown as Graph);
onMount(() => {
appSettings.value.debug.stressTest = {
...appSettings.value.debug.stressTest,
loadGrid: () => {
manager.load(
templates.grid(
appSettings.value.debug.stressTest.amount,
appSettings.value.debug.stressTest.amount
)
);
},
loadTree: () => {
manager.load(templates.tree(appSettings.value.debug.stressTest.amount));
},
lottaFaces: () => {
manager.load(templates.lottaFaces as unknown as Graph);
},
lottaNodes: () => {
manager.load(templates.lottaNodes as unknown as Graph);
},
lottaNodesAndFaces: () => {
manager.load(templates.lottaNodesAndFaces as unknown as Graph);
}
};
});
</script>
@@ -184,7 +175,7 @@
bind:settings={graphSettings}
bind:settingTypes={graphSettingTypes}
onsave={(g) => pm.saveGraph(g)}
onresult={(result) => handleUpdate(result)}
onresult={(result) => handleUpdate(result as Graph)}
/>
{/if}
<Sidebar>
@@ -202,8 +193,8 @@
>
<Keymap
keymaps={[
{ keymap: applicationKeymap, title: "Application" },
{ keymap: graphInterface?.keymap, title: "Node-Editor" },
{ keymap: applicationKeymap, title: 'Application' },
{ keymap: graphInterface?.keymap, title: 'Node-Editor' }
]}
/>
</Panel>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import type { Snippet } from "svelte";
import type { Snippet } from 'svelte';
const { children } = $props<{ children?: Snippet }>();
</script>

View File

@@ -1,24 +1,20 @@
<script lang="ts">
import NodeHTML from "$lib/graph-interface/node/NodeHTML.svelte";
import { localState } from "$lib/helpers/localState.svelte";
import Panel from "$lib/sidebar/Panel.svelte";
import Sidebar from "$lib/sidebar/Sidebar.svelte";
import { IndexDBCache, RemoteNodeRegistry } from "@nodarium/registry";
import { type NodeId, type NodeInstance } from "@nodarium/types";
import Code from "./Code.svelte";
import Grid from "$lib/grid";
import {
concatEncodedArrays,
createWasmWrapper,
encodeNestedArray,
} from "@nodarium/utils";
import NodeHTML from '$lib/graph-interface/node/NodeHTML.svelte';
import Grid from '$lib/grid';
import { localState } from '$lib/helpers/localState.svelte';
import { IndexDBCache, RemoteNodeRegistry } from '$lib/node-registry/index';
import Panel from '$lib/sidebar/Panel.svelte';
import Sidebar from '$lib/sidebar/Sidebar.svelte';
import { type NodeId, type NodeInstance } from '@nodarium/types';
import { concatEncodedArrays, createWasmWrapper, encodeNestedArray } from '@nodarium/utils';
import Code from './Code.svelte';
const registryCache = new IndexDBCache("node-registry");
const nodeRegistry = new RemoteNodeRegistry("", registryCache);
const registryCache = new IndexDBCache('node-registry');
const nodeRegistry = new RemoteNodeRegistry('', registryCache);
let activeNode = localState<NodeId | undefined>(
"node.dev.activeNode",
undefined,
'node.dev.activeNode',
undefined
);
let nodeWasm = $state<ArrayBuffer>();
@@ -32,17 +28,21 @@
if (!nodeId) return;
const data = await nodeRegistry.fetchNodeDefinition(nodeId);
nodeWasm = await nodeRegistry.fetchArrayBuffer("nodes/" + nodeId + ".wasm");
nodeWasm = await nodeRegistry.fetchArrayBuffer('nodes/' + nodeId + '.wasm');
nodeInstance = {
id: 0,
type: nodeId,
position: [0, 0] as [number, number],
props: {},
state: {
type: data,
},
type: data
}
};
nodeWasmWrapper = createWasmWrapper(nodeWasm);
try {
nodeWasmWrapper = createWasmWrapper(nodeWasm);
} catch (e) {
console.error(`Failed to create node wrapper for ${nodeId}`, e);
}
}
$effect(() => {
@@ -52,8 +52,8 @@
$effect(() => {
if (nodeInstance?.props && nodeWasmWrapper) {
const keys = Object.keys(nodeInstance.state.type?.inputs || {});
let ins = Object.values(nodeInstance.props) as number[];
if (keys[0] === "plant") {
let ins = Object.values(nodeInstance.props) as (number[] | number)[];
if (keys[0] === 'plant') {
ins = [[0, 0, 0, 0, 0, 0, 0, 0], ...ins];
}
const inputs = concatEncodedArrays(encodeNestedArray(ins));
@@ -94,16 +94,20 @@
icon="i-[tabler--database]"
>
<div class="p-4 flex flex-col gap-2">
{#await nodeRegistry.fetchCollection("max/plantarium")}
{#await nodeRegistry.fetchCollection('max/plantarium')}
<p>Loading Nodes...</p>
{:then result}
{#each result.nodes as n}
{#each result.nodes as n (n.id)}
<button
class="cursor-pointer p-2 bg-layer-1 {activeNode.value === n.id
class="
cursor-pointer p-2 bg-layer-1 {activeNode.value === n.id
? 'outline outline-offset-1'
: ''}"
onclick={() => (activeNode.value = n.id)}>{n.id}</button
: ''}
"
onclick={() => (activeNode.value = n.id)}
>
{n.id}
</button>
{/each}
{/await}
</div>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import wabtInit from "wabt";
import wabtInit from 'wabt';
const { wasm } = $props<{ wasm: ArrayBuffer }>();
@@ -7,7 +7,7 @@
const wabt = await wabtInit();
const module = wabt.readWasm(new Uint8Array(arrayBuffer), {
readDebugNames: true,
readDebugNames: true
});
module.generateNames();

View File

@@ -1,22 +1,18 @@
import { json } from "@sveltejs/kit";
import type { EntryGenerator, RequestHandler } from "./$types";
import * as registry from "$lib/node-registry";
import * as registry from '$lib/node-registry';
import { json } from '@sveltejs/kit';
import type { EntryGenerator, RequestHandler } from './$types';
export const prerender = true;
export const entries: EntryGenerator = async () => {
const users = await registry.getUsers();
return users.map(user => {
return { user: user.id }
return { user: user.id };
}).flat(2);
}
};
export const GET: RequestHandler = async function GET({ params }) {
const namespaces = await registry.getUser(params.user)
const namespaces = await registry.getUser(params.user);
return json(namespaces);
}
};

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