15 Commits

Author SHA1 Message Date
9b94159f8e register works 2026-01-23 11:23:07 +01:00
aa4d7f73a8 setup zig node 2026-01-23 10:24:21 +01:00
1efb94b09c Add zig to flake packages 2026-01-23 09:07:05 +01:00
Felix Hungenberg
5570d975f5 feat: unmigrate number into universal float, inherit step if unset
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m1s
2026-01-22 23:57:56 +01:00
Felix Hungenberg
8c1ba2ee65 feat: move add context menu within view if outside
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m59s
2026-01-22 23:26:56 +01:00
Felix Hungenberg
3e019e4e21 feat: don't move graph on right click drag 2026-01-22 23:26:26 +01:00
Felix Hungenberg
a58b19e935 Merge branch 'main' of github.com:jim-fx/nodarium
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m57s
2026-01-22 14:06:44 +01:00
Max Richter
6f5c5bb46e feat: change initial camera position so that nodes are visible
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m3s
2026-01-22 12:07:37 +01:00
7f2214f15c fix(utils): make sure we do not build a .wasm file for utils
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m54s
2026-01-21 17:24:54 +01:00
43ef563ae7 feat: show all nodes in add menu
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m57s
2026-01-21 17:08:47 +01:00
Felix Hungenberg
714d01da94 chore: move pnpm links to workspace (auto link) 2026-01-21 16:39:54 +01:00
Felix Hungenberg
92308fc43a chore: run ui debug server from root
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m57s
2026-01-21 16:36:20 +01:00
Felix Hungenberg
5adf67ed52 Merge branch 'main' of git.max-richter.dev:max/nodarium
Some checks failed
Deploy to GitHub Pages / build_site (push) Has been cancelled
2026-01-21 16:35:34 +01:00
Felix Hungenberg
f54cde734e fix: integer width 2026-01-21 16:35:13 +01:00
Felix Hungenberg
2a90d5de3f chore: dev scripts & linting 2026-01-21 16:31:26 +01:00
34 changed files with 706 additions and 526 deletions

View File

@@ -1,12 +1,14 @@
{
"$schema": "https://dprint.dev/schemas/v0.json",
"indentWidth": 2,
"lineWidth": 100,
"typescript": {
// https://dprint.dev/plugins/typescript/config/
"quoteStyle": "preferSingle",
"trailingCommas": "never"
"trailingCommas": "never",
},
"json": {
// https://dprint.dev/plugins/json/config/
},
"markdown": {
},
@@ -21,6 +23,9 @@
"malva": {
},
"markup": {
// https://dprint.dev/plugins/markup_fmt/config/
"scriptIndent": true,
"styleIndent": true,
},
"yaml": {
},
@@ -28,19 +33,24 @@
},
"exec": {
"cwd": "${configDir}",
"commands": [{
"command": "rustfmt --edition 2024",
"command": "rustfmt",
"exts": ["rs"],
// add the config files for automatic cache invalidation when the rust version or rustfmt config changes
"cacheKeyFiles": [
"rustfmt.toml",
"rust-toolchain.toml"
]
}]
"rust-toolchain.toml",
],
}],
},
"excludes": [
"**/node_modules",
"**/*-lock.yaml"
"**/build",
"**/.svelte-kit",
"**/package",
"**/*-lock.yaml",
"**/yaml.lock",
"**/.DS_Store",
],
"plugins": [
"https://plugins.dprint.dev/typescript-0.95.13.wasm",
@@ -54,6 +64,7 @@
"https://plugins.dprint.dev/g-plane/markup_fmt-v0.25.3.wasm",
"https://plugins.dprint.dev/g-plane/pretty_yaml-v0.5.1.wasm",
"https://plugins.dprint.dev/g-plane/pretty_graphql-v0.2.3.wasm",
"https://plugins.dprint.dev/exec-0.6.0.json@a054130d458f124f9b5c91484833828950723a5af3f8ff2bd1523bd47b83b364"
]
"https://plugins.dprint.dev/exec-0.6.0.json@a054130d458f124f9b5c91484833828950723a5af3f8ff2bd1523bd47b83b364",
"https://plugins.dprint.dev/biome-0.11.10.wasm",
],
}

View File

@@ -6,7 +6,10 @@ members = [
"packages/types",
"packages/utils",
]
exclude = ["nodes/max/plantarium/.template"]
exclude = [
"nodes/max/plantarium/.template",
"nodes/max/plantarium/zig"
]
[profile.release]
lto = true

View File

@@ -28,7 +28,7 @@
"devDependencies": {
"@iconify-json/tabler": "^1.2.26",
"@iconify/tailwind4": "^1.2.1",
"@nodarium/types": "link:../packages/types",
"@nodarium/types": "workspace:",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tsconfig/svelte": "^5.0.6",

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import { HTML } from "@threlte/extras";
import { onMount } from "svelte";
import type { NodeInstance, NodeId } from "@nodarium/types";
import { getGraphManager, getGraphState } from "../graph-state.svelte";
import type { NodeId, NodeInstance } from '@nodarium/types';
import { HTML } from '@threlte/extras';
import { onMount } from 'svelte';
import { getGraphManager, getGraphState } from '../graph-state.svelte';
type Props = {
onnode: (n: NodeInstance) => void;
@@ -14,6 +14,7 @@
const graphState = getGraphState();
let input: HTMLInputElement;
let wrapper: HTMLDivElement;
let value = $state<string>();
let activeNodeId = $state<NodeId>();
@@ -22,10 +23,10 @@
: graph.getNodeDefinitions();
function filterNodes() {
return allNodes.filter((node) => node.id.includes(value ?? ""));
return allNodes.filter((node) => node.id.includes(value ?? ''));
}
const nodes = $derived(value === "" ? allNodes : filterNodes());
const nodes = $derived(value === '' ? allNodes : filterNodes());
$effect(() => {
if (nodes) {
if (activeNodeId === undefined) {
@@ -39,38 +40,38 @@
}
});
function handleNodeCreation(nodeType: NodeInstance["type"]) {
function handleNodeCreation(nodeType: NodeInstance['type']) {
if (!graphState.addMenuPosition) return;
onnode?.({
id: -1,
type: nodeType,
position: [...graphState.addMenuPosition],
props: {},
state: {},
state: {}
});
}
function handleKeyDown(event: KeyboardEvent) {
event.stopImmediatePropagation();
if (event.key === "Escape") {
if (event.key === 'Escape') {
graphState.addMenuPosition = null;
return;
}
if (event.key === "ArrowDown") {
if (event.key === 'ArrowDown') {
const index = nodes.findIndex((node) => node.id === activeNodeId);
activeNodeId = nodes[(index + 1) % nodes.length].id;
return;
}
if (event.key === "ArrowUp") {
if (event.key === 'ArrowUp') {
const index = nodes.findIndex((node) => node.id === activeNodeId);
activeNodeId = nodes[(index - 1 + nodes.length) % nodes.length].id;
return;
}
if (event.key === "Enter") {
if (event.key === 'Enter') {
if (activeNodeId && graphState.addMenuPosition) {
handleNodeCreation(activeNodeId);
}
@@ -81,6 +82,16 @@
onMount(() => {
input.disabled = false;
setTimeout(() => input.focus(), 50);
const rect = wrapper.getBoundingClientRect();
const deltaY = rect.bottom - window.innerHeight;
const deltaX = rect.right - window.innerWidth;
if (deltaY > 0) {
wrapper.style.marginTop = `-${deltaY + 30}px`;
}
if (deltaX > 0) {
wrapper.style.marginLeft = `-${deltaX + 30}px`;
}
});
</script>
@@ -89,7 +100,7 @@
position.z={graphState.addMenuPosition?.[1]}
transform={false}
>
<div class="add-menu-wrapper">
<div class="add-menu-wrapper" bind:this={wrapper}>
<div class="header">
<input
id="add-menu"
@@ -112,7 +123,7 @@
tabindex="0"
aria-selected={node.id === activeNodeId}
onkeydown={(event) => {
if (event.key === "Enter") {
if (event.key === 'Enter') {
handleNodeCreation(node.id);
}
}}
@@ -125,7 +136,7 @@
activeNodeId = node.id;
}}
>
{node.id.split("/").at(-1)}
{node.id.split('/').at(-1)}
</div>
{/each}
</div>
@@ -167,6 +178,8 @@
min-height: none;
width: 100%;
color: var(--text-color);
max-height: 300px;
overflow-y: auto;
}
.result {

View File

@@ -1,4 +1,5 @@
import throttle from '$lib/helpers/throttle';
import { RemoteNodeRegistry } from '@nodarium/registry';
import type {
Edge,
Graph,
@@ -18,6 +19,8 @@ import { HistoryManager } from './history-manager';
const logger = createLogger('graph-manager');
logger.mute();
const remoteRegistry = new RemoteNodeRegistry('');
const clone = 'structuredClone' in self
? self.structuredClone
: (args: any) => JSON.parse(JSON.stringify(args));
@@ -173,7 +176,9 @@ export class GraphManager extends EventEmitter<{
return areSocketsCompatible(edgeOutputSocketType, accepted);
});
const bestOutputIdx = draggedOutputs.findIndex(outputType => areSocketsCompatible(outputType, targetAcceptedTypes));
const bestOutputIdx = draggedOutputs.findIndex(outputType =>
areSocketsCompatible(outputType, targetAcceptedTypes)
);
if (!bestInputEntry || bestOutputIdx === -1) {
logger.error('Could not find compatible sockets for drop');
@@ -308,6 +313,21 @@ export class GraphManager extends EventEmitter<{
const nodeIds = Array.from(new Set([...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}`>();
for (const id of nodeIds) {
const [user, collection] = id.split('/');
allCollections.add(`${user}/${collection}`);
}
for (const collection of allCollections) {
remoteRegistry
.fetchCollection(collection)
.then((collection: { nodes: { id: NodeId }[] }) => {
const ids = collection.nodes.map((n) => n.id);
return this.registry.load(ids);
});
}
logger.info('loaded node types', this.registry.getAllNodes());
for (const node of this.graph.nodes) {

View File

@@ -58,11 +58,13 @@ export class GraphState {
wrapper = $state<HTMLDivElement>(null!);
rect: DOMRect = $derived(
(this.wrapper && this.width && this.height) ? this.wrapper.getBoundingClientRect() : new DOMRect(0, 0, 0, 0)
(this.wrapper && this.width && this.height)
? this.wrapper.getBoundingClientRect()
: new DOMRect(0, 0, 0, 0)
);
camera = $state<OrthographicCamera>(null!);
cameraPosition: [number, number, number] = $state([0, 0, 100]);
cameraPosition: [number, number, number] = $state([140, 100, 3.5]);
clipboard: null | {
nodes: NodeInstance[];
@@ -187,13 +189,13 @@ export class GraphState {
}
const height = 5
+ 10
* Object.keys(node.inputs).filter(
(p) =>
p !== 'seed'
&& node?.inputs
&& !('setting' in node?.inputs?.[p])
&& node.inputs[p].hidden !== true
).length;
* Object.keys(node.inputs).filter(
(p) =>
p !== 'seed'
&& node?.inputs
&& !('setting' in node?.inputs?.[p])
&& node.inputs[p].hidden !== true
).length;
this.nodeHeightCache[nodeTypeId] = height;
return height;
}
@@ -333,4 +335,8 @@ export class GraphState {
&& node.position[1] < this.cameraBounds[3]
);
}
openNodePalette() {
this.addMenuPosition = [this.mousePosition[0], this.mousePosition[1]];
}
}

View File

@@ -1,23 +1,23 @@
<script lang="ts">
import type { Edge, NodeInstance } from "@nodarium/types";
import { createKeyMap } from "../../helpers/createKeyMap";
import AddMenu from "../components/AddMenu.svelte";
import Background from "../background/Background.svelte";
import BoxSelection from "../components/BoxSelection.svelte";
import EdgeEl from "../edges/Edge.svelte";
import NodeEl from "../node/Node.svelte";
import Camera from "../components/Camera.svelte";
import { Canvas } from "@threlte/core";
import HelpView from "../components/HelpView.svelte";
import { getGraphManager, getGraphState } from "../graph-state.svelte";
import { HTML } from "@threlte/extras";
import { maxZoom, minZoom } from "./constants";
import Debug from "../debug/Debug.svelte";
import { FileDropEventManager } from "./drop.events";
import { MouseEventManager } from "./mouse.events";
import type { Edge, NodeInstance } from '@nodarium/types';
import { Canvas } from '@threlte/core';
import { HTML } from '@threlte/extras';
import { createKeyMap } from '../../helpers/createKeyMap';
import Background from '../background/Background.svelte';
import AddMenu from '../components/AddMenu.svelte';
import BoxSelection from '../components/BoxSelection.svelte';
import Camera from '../components/Camera.svelte';
import HelpView from '../components/HelpView.svelte';
import Debug from '../debug/Debug.svelte';
import EdgeEl from '../edges/Edge.svelte';
import { getGraphManager, getGraphState } from '../graph-state.svelte';
import NodeEl from '../node/Node.svelte';
import { maxZoom, minZoom } from './constants';
import { FileDropEventManager } from './drop.events';
import { MouseEventManager } from './mouse.events';
const {
keymap,
keymap
}: {
keymap: ReturnType<typeof createKeyMap>;
} = $props();
@@ -45,19 +45,18 @@
const newNode = graph.createNode({
type: node.type,
position: node.position,
props: node.props,
props: node.props
});
if (!newNode) return;
if (graphState.activeSocket) {
if (typeof graphState.activeSocket.index === "number") {
const socketType =
graphState.activeSocket.node.state?.type?.outputs?.[
graphState.activeSocket.index
];
if (typeof graphState.activeSocket.index === 'number') {
const socketType = graphState.activeSocket.node.state?.type?.outputs?.[
graphState.activeSocket.index
];
const input = Object.entries(newNode?.state?.type?.inputs || {}).find(
(inp) => inp[1].type === socketType,
(inp) => inp[1].type === socketType
);
if (input) {
@@ -65,14 +64,13 @@
graphState.activeSocket.node,
graphState.activeSocket.index,
newNode,
input[0],
input[0]
);
}
} else {
const socketType =
graphState.activeSocket.node.state?.type?.inputs?.[
graphState.activeSocket.index
];
const socketType = graphState.activeSocket.node.state?.type?.inputs?.[
graphState.activeSocket.index
];
const output = newNode.state?.type?.outputs?.find((out) => {
if (socketType?.type === out) return true;
@@ -85,7 +83,7 @@
newNode,
output.indexOf(output),
graphState.activeSocket.node,
graphState.activeSocket.index,
graphState.activeSocket.index
);
}
}
@@ -97,15 +95,15 @@
</script>
<svelte:window
onmousemove={(ev) => mouseEvents.handleMouseMove(ev)}
onmouseup={(ev) => mouseEvents.handleMouseUp(ev)}
onmousemove={(ev) => mouseEvents.handleWindowMouseMove(ev)}
onmouseup={(ev) => mouseEvents.handleWindowMouseUp(ev)}
/>
<div
onwheel={(ev) => mouseEvents.handleMouseScroll(ev)}
bind:this={graphState.wrapper}
class="graph-wrapper"
style="height: 100%;"
style="height: 100%"
class:is-panning={graphState.isPanning}
class:is-hovering={graphState.hoveredNodeId !== -1}
aria-label="Graph"
@@ -115,6 +113,7 @@
bind:clientHeight={graphState.height}
onkeydown={(ev) => keymap.handleKeyboardEvent(ev)}
onmousedown={(ev) => mouseEvents.handleMouseDown(ev)}
oncontextmenu={(ev) => mouseEvents.handleContextMenu(ev)}
{...fileDropEvents.getEventListenerProps()}
>
<input
@@ -147,20 +146,18 @@
<BoxSelection
cameraPosition={graphState.cameraPosition}
p1={{
x:
graphState.cameraPosition[0] +
(graphState.mouseDown[0] - graphState.width / 2) /
graphState.cameraPosition[2],
y:
graphState.cameraPosition[1] +
(graphState.mouseDown[1] - graphState.height / 2) /
graphState.cameraPosition[2],
x: graphState.cameraPosition[0]
+ (graphState.mouseDown[0] - graphState.width / 2)
/ graphState.cameraPosition[2],
y: graphState.cameraPosition[1]
+ (graphState.mouseDown[1] - graphState.height / 2)
/ graphState.cameraPosition[2]
}}
p2={{ x: graphState.mousePosition[0], y: graphState.mousePosition[1] }}
/>
{/if}
{#if graph.status === "idle"}
{#if graph.status === 'idle'}
{#if graphState.addMenuPosition}
<AddMenu onnode={handleNodeCreation} />
{/if}
@@ -207,9 +204,9 @@
{/each}
</div>
</HTML>
{:else if graph.status === "loading"}
{:else if graph.status === 'loading'}
<span>Loading</span>
{:else if graph.status === "error"}
{:else if graph.status === 'error'}
<span>Error</span>
{/if}
</Canvas>

View File

@@ -1,7 +1,7 @@
import { animate, lerp } from '$lib/helpers';
import { type NodeInstance } from '@nodarium/types';
import type { GraphManager } from '../graph-manager.svelte';
import type { GraphState } from '../graph-state.svelte';
import { type GraphState } from '../graph-state.svelte';
import { snapToGrid as snapPointToGrid } from '../helpers';
import { maxZoom, minZoom, zoomSpeed } from './constants';
import { EdgeInteractionManager } from './edge.events';
@@ -16,7 +16,7 @@ export class MouseEventManager {
this.edgeInteractionManager = new EdgeInteractionManager(graph, state);
}
handleMouseUp(event: MouseEvent) {
handleWindowMouseUp(event: MouseEvent) {
this.edgeInteractionManager.handleMouseUp();
this.state.isPanning = false;
if (!this.state.mouseDown) return;
@@ -151,7 +151,19 @@ export class MouseEventManager {
this.state.addMenuPosition = null;
}
handleContextMenu(event: MouseEvent) {
if (!this.state.addMenuPosition) {
event.preventDefault();
this.state.openNodePalette();
}
}
handleMouseDown(event: MouseEvent) {
// Right click
if (event.button === 2) {
return;
}
if (this.state.mouseDown) return;
this.state.edgeEndPosition = null;
@@ -229,7 +241,7 @@ export class MouseEventManager {
this.state.edgeEndPosition = null;
}
handleMouseMove(event: MouseEvent) {
handleWindowMouseMove(event: MouseEvent) {
let mx = event.clientX - this.state.rect.x;
let my = event.clientY - this.state.rect.y;
@@ -245,7 +257,7 @@ export class MouseEventManager {
for (const socket of this.state.possibleSockets) {
const dist = Math.sqrt(
(socket.position[0] - this.state.mousePosition[0]) ** 2
+ (socket.position[1] - this.state.mousePosition[1]) ** 2
+ (socket.position[1] - this.state.mousePosition[1]) ** 2
);
if (dist < smallestDist) {
smallestDist = dist;
@@ -377,9 +389,9 @@ export class MouseEventManager {
// Update camera position and zoom level
this.state.cameraPosition[0] = this.state.mousePosition[0]
- (this.state.mousePosition[0] - this.state.cameraPosition[0])
/ zoomRatio;
/ 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

@@ -59,9 +59,7 @@ export function setupKeymaps(keymap: Keymap, graph: GraphManager, graphState: Gr
key: 'A',
shift: true,
description: 'Add new Node',
callback: () => {
graphState.addMenuPosition = [graphState.mousePosition[0], graphState.mousePosition[1]];
}
callback: () => graphState.openNodePalette()
});
keymap.addShortcut({

View File

@@ -1,147 +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 { Number } 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<any>;
};
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>
<Number 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

@@ -4,7 +4,7 @@
};
outputs = {nixpkgs, ...}: let
systems = ["aarch64-darwin" "x86_64-linux"];
systems = ["aarch64-darwin" "x86_64-darwin" "aarch64-linux" "x86_64-linux"];
eachSystem = function:
nixpkgs.lib.genAttrs systems (system:
function {
@@ -19,14 +19,15 @@
pkgs.nodejs_24
pkgs.pnpm_10
# wasm/rust stuff
# wasm stuff
pkgs.rustc
pkgs.cargo
pkgs.rust-analyzer
pkgs.rustfmt
pkgs.wasm-bindgen-cli
pkgs.wasm-pack
pkgs.binaryen
pkgs.lld
pkgs.zig
pkgs.zls
# frontend
pkgs.vscode-langservers-extracted
@@ -35,6 +36,10 @@
pkgs.tailwindcss-language-server
pkgs.svelte-language-server
];
shellHook = ''
unset ZIG_GLOBAL_CACHE_DIR
'';
};
});
};

2
nodes/max/plantarium/zig/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.zig-cache/
zig-out/

View File

@@ -0,0 +1,19 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.resolveTargetQuery(.{ .os_tag = .freestanding, .abi = .none, .cpu_arch = .wasm32 });
const release = b.option(bool, "release", "To build a wasm release") orelse false;
const exe = b.addExecutable(.{
.name = "zig",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = if (release) .ReleaseSmall else .Debug,
}),
});
exe.rdynamic = true;
exe.entry = .disabled;
b.installArtifact(exe);
}

View File

@@ -0,0 +1,81 @@
.{
// This is the default name used by packages depending on this one. For
// example, when a user runs `zig fetch --save <url>`, this field is used
// as the key in the `dependencies` table. Although the user can choose a
// different name, most users will stick with this provided value.
//
// It is redundant to include "zig" in this name because it is already
// within the Zig package namespace.
.name = .math,
// This is a [Semantic Version](https://semver.org/).
// In a future version of Zig it will be used for package deduplication.
.version = "0.0.0",
// Together with name, this represents a globally unique package
// identifier. This field is generated by the Zig toolchain when the
// package is first created, and then *never changes*. This allows
// unambiguous detection of one package being an updated version of
// another.
//
// When forking a Zig project, this id should be regenerated (delete the
// field and run `zig build`) if the upstream project is still maintained.
// Otherwise, the fork is *hostile*, attempting to take control over the
// original project's identity. Thus it is recommended to leave the comment
// on the following line intact, so that it shows up in code reviews that
// modify the field.
.fingerprint = 0xa927044d8d610b01, // Changing this has security and trust implications.
// Tracks the earliest Zig version that the package considers to be a
// supported use case.
.minimum_zig_version = "0.15.2",
// This field is optional.
// Each dependency must either provide a `url` and `hash`, or a `path`.
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
// Once all dependencies are fetched, `zig build` no longer requires
// internet connectivity.
.dependencies = .{
// See `zig fetch --save <url>` for a command-line interface for adding dependencies.
//.example = .{
// // When updating this field to a new URL, be sure to delete the corresponding
// // `hash`, otherwise you are communicating that you expect to find the old hash at
// // the new URL. If the contents of a URL change this will result in a hash mismatch
// // which will prevent zig from using it.
// .url = "https://example.com/foo.tar.gz",
//
// // This is computed from the file contents of the directory of files that is
// // obtained after fetching `url` and applying the inclusion rules given by
// // `paths`.
// //
// // This field is the source of truth; packages do not come from a `url`; they
// // come from a `hash`. `url` is just one of many possible mirrors for how to
// // obtain a package matching this `hash`.
// //
// // Uses the [multihash](https://multiformats.io/multihash/) format.
// .hash = "...",
//
// // When this is provided, the package is found in a directory relative to the
// // build root. In this case the package's hash is irrelevant and therefore not
// // computed. This field and `url` are mutually exclusive.
// .path = "foo",
//
// // When this is set to `true`, a package is declared to be lazily
// // fetched. This makes the dependency only get fetched if it is
// // actually used.
// .lazy = false,
//},
},
// Specifies the set of files and directories that are included in this package.
// Only files and directories listed here are included in the `hash` that
// is computed for this package. Only files listed here will remain on disk
// when using the zig package manager. As a rule of thumb, one should list
// files required for compilation plus any license(s).
// Paths are relative to the build root. Use the empty string (`""`) to refer to
// the build root itself.
// A directory listed here means that all files within, recursively, are included.
.paths = .{
"build.zig",
"build.zig.zon",
"src",
// For example...
//"LICENSE",
//"README.md",
},
}

View File

@@ -0,0 +1,27 @@
{
"id": "max/nodarium/zig",
"outputs": [
"float"
],
"inputs": {
"op_type": {
"label": "type",
"type": "select",
"options": [
"add",
"subtract",
"multiply",
"divide"
],
"internal": true
},
"a": {
"type": "float",
"value": 2
},
"b": {
"type": "float",
"value": 2
}
}
}

View File

@@ -0,0 +1,29 @@
const std = @import("std");
const def = @embedFile("input.json");
export fn execute(ptr: *anyopaque, len: c_int) c_int {
_ = ptr; // autofix
_ = len; // autofix
return 0;
}
export fn __alloc(len: c_int) ?*anyopaque {
if (len < 0) return null;
const mem = std.heap.wasm_allocator.alloc(u8, @intCast(len)) catch return null;
return mem.ptr;
}
export fn __free(ptr: *anyopaque, len: c_int) void {
if (len < 1) return;
const mem: [*]u8 = @ptrCast(@alignCast(ptr));
std.heap.wasm_allocator.free(mem[0..@intCast(len)]);
}
export fn getDefinitionPtr() *const anyopaque {
return def.ptr;
}
export fn getDefinitionLen() usize {
return def.len;
}

View File

@@ -1,13 +1,18 @@
{
"scripts": {
"postinstall": "pnpm run -r --filter 'ui' build",
"build": "pnpm build:nodes && pnpm build:app",
"build:story": "pnpm -r --filter 'ui' story:build",
"build:app": "BASE_PATH=/ui pnpm -r --filter 'ui' build && pnpm -r --filter 'app' build",
"build:nodes": "cargo build --workspace --target wasm32-unknown-unknown --release && rm -rf ./app/static/nodes/max/plantarium/ && mkdir -p ./app/static/nodes/max/plantarium/ && cp -R ./target/wasm32-unknown-unknown/release/*.wasm ./app/static/nodes/max/plantarium/",
"build:deploy": "pnpm build",
"dev:all": "pnpm -r dev",
"dev:nodes": "pnpm -r --parallel --filter './nodes/**' dev",
"dev": "pnpm -r --filter 'app' --filter './packages/ui' dev"
"dev:nodes": "chokidar './nodes/**' --initial -i '/pkg/' -c 'pnpm build:nodes'",
"dev:app_ui": "pnpm -r --parallel --filter 'app' --filter './packages/ui' dev",
"dev_ui": "pnpm -r --filter 'ui' dev:ui",
"dev": "pnpm run \"/^dev:.*/\""
},
"packageManager": "pnpm@10.28.1+sha512.7d7dbbca9e99447b7c3bf7a73286afaaf6be99251eb9498baefa7d406892f67b879adb3a1d7e687fc4ccc1a388c7175fbaae567a26ab44d1067b54fcb0d6a316"
"packageManager": "pnpm@10.28.1+sha512.7d7dbbca9e99447b7c3bf7a73286afaaf6be99251eb9498baefa7d406892f67b879adb3a1d7e687fc4ccc1a388c7175fbaae567a26ab44d1067b54fcb0d6a316",
"devDependencies": {
"chokidar-cli": "catalog:"
}
}

View File

@@ -10,8 +10,8 @@
"author": "",
"license": "ISC",
"dependencies": {
"@nodarium/types": "link:../types",
"@nodarium/utils": "link:../utils",
"@nodarium/types": "workspace:",
"@nodarium/utils": "workspace:",
"idb": "^8.0.3"
}
}

View File

@@ -1,32 +1,31 @@
import {
NodeDefinitionSchema,
type AsyncCache,
type NodeDefinition,
type NodeRegistry,
} from "@nodarium/types";
import { createLogger, createWasmWrapper } from "@nodarium/utils";
NodeDefinitionSchema,
type NodeRegistry
} from '@nodarium/types';
import { createLogger, createWasmWrapper } from '@nodarium/utils';
const log = createLogger("node-registry");
const log = createLogger('node-registry');
log.mute();
export class RemoteNodeRegistry implements NodeRegistry {
status: "loading" | "ready" | "error" = "loading";
status: 'loading' | 'ready' | 'error' = 'loading';
private nodes: Map<string, NodeDefinition> = new Map();
constructor(
private url: string,
public cache?: AsyncCache<ArrayBuffer | string>,
public cache?: AsyncCache<ArrayBuffer | string>
) { }
async fetchJson(url: string, skipCache = false) {
const finalUrl = `${this.url}/${url}`;
if (!skipCache && this.cache) {
const cachedValue = await this.cache?.get<string>(finalUrl);
if (cachedValue) {
// fetch again in the background, maybe implement that only refetch after a certain time
this.fetchJson(url, true)
this.fetchJson(url, true);
return JSON.parse(cachedValue);
}
}
@@ -46,14 +45,13 @@ export class RemoteNodeRegistry implements NodeRegistry {
}
async fetchArrayBuffer(url: string, skipCache = false) {
const finalUrl = `${this.url}/${url}`;
if (!skipCache && this.cache) {
const cachedNode = await this.cache?.get<ArrayBuffer>(finalUrl);
if (cachedNode) {
// fetch again in the background, maybe implement that only refetch after a certain time
this.fetchArrayBuffer(url, true)
this.fetchArrayBuffer(url, true);
return cachedNode;
}
}
@@ -79,7 +77,7 @@ export class RemoteNodeRegistry implements NodeRegistry {
async fetchCollection(userCollectionId: `${string}/${string}`) {
const col = await this.fetchJson(`nodes/${userCollectionId}.json`);
return col
return col;
}
async fetchNodeDefinition(nodeId: `${string}/${string}/${string}`) {
@@ -87,7 +85,6 @@ export class RemoteNodeRegistry implements NodeRegistry {
}
private async fetchNodeWasm(nodeId: `${string}/${string}/${string}`) {
const node = await this.fetchArrayBuffer(`nodes/${nodeId}.wasm`);
if (!node) {
throw new Error(`Failed to load node wasm ${nodeId}`);
@@ -99,7 +96,7 @@ export class RemoteNodeRegistry implements NodeRegistry {
async load(nodeIds: `${string}/${string}/${string}`[]) {
const a = performance.now();
const nodes = await Promise.all(
const nodes = (await Promise.all(
[...new Set(nodeIds).values()].map(async (id) => {
if (this.nodes.has(id)) {
return this.nodes.get(id)!;
@@ -107,17 +104,23 @@ export class RemoteNodeRegistry implements NodeRegistry {
const wasmBuffer = await this.fetchNodeWasm(id);
return this.register(wasmBuffer);
}),
);
try {
return await this.register(wasmBuffer);
} catch (e) {
console.log('Failed to register: ', id);
console.error(e);
return;
}
})
)).filter(Boolean) as NodeDefinition[];
const duration = performance.now() - a;
log.group("loaded nodes in", duration, "ms");
log.group('loaded nodes in', duration, 'ms');
log.info(nodeIds);
log.info(nodes);
log.groupEnd();
this.status = "ready";
this.status = 'ready';
return nodes;
}
@@ -128,17 +131,16 @@ export class RemoteNodeRegistry implements NodeRegistry {
const definition = NodeDefinitionSchema.safeParse(wrapper.get_definition());
if (definition.error) {
console.error(definition.error);
throw definition.error;
}
if (this.cache) {
await this.cache.set(definition.data.id, wasmBuffer);
this.cache.set(definition.data.id, wasmBuffer);
}
let node = {
...definition.data,
execute: wrapper.execute,
execute: wrapper.execute
};
this.nodes.set(definition.data.id, node);

View File

@@ -3,6 +3,7 @@
"version": "0.0.1",
"scripts": {
"dev": "chokidar './src/**' --initial -c 'pnpm build'",
"dev:ui": "vite",
"build": "vite build && npm run package",
"preview": "vite preview",
"package": "svelte-kit sync && svelte-package && publint",
@@ -10,6 +11,7 @@
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test": "vitest",
"lint-dprint": "dprint check -c '../../.dprint.jsonc' .",
"lint": "eslint ."
},
"exports": {
@@ -28,7 +30,7 @@
"svelte": "^4.0.0"
},
"devDependencies": {
"@nodarium/types": "link:../types",
"@nodarium/types": "workspace:",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.50.0",
"@sveltejs/package": "^2.5.7",

View File

@@ -1,10 +1,7 @@
<script lang="ts">
import type { NodeInput } from '@nodarium/types';
import Checkbox from './inputs/Checkbox.svelte';
import Number from './inputs/Number.svelte';
import Select from './inputs/Select.svelte';
import Vec3 from './inputs/Vec3.svelte';
import { Checkbox, Number, Select, Vec3 } from './index.js';
interface Props {
input: NodeInput;
@@ -16,7 +13,7 @@
</script>
{#if input.type === 'float'}
<Number bind:value min={input?.min} max={input?.max} step={0.01} />
<Number bind:value min={input?.min} max={input?.max} step={input?.step} />
{:else if input.type === 'integer'}
<Number bind:value min={input?.min} max={input?.max} />
{:else if input.type === 'boolean'}

View File

@@ -1,8 +1,7 @@
export { default as Input } from './Input.svelte';
export { default as Checkbox } from './inputs/Checkbox.svelte';
export { default as Float } from './inputs/Float.svelte';
export { default as Float, default as Number } from './inputs/Float.svelte';
export { default as Integer } from './inputs/Integer.svelte';
export { default as Number } from './inputs/Number.svelte';
export { default as Select } from './inputs/Select.svelte';
export { default as Vec3 } from './inputs/Vec3.svelte';

View File

@@ -9,7 +9,7 @@
let {
value = $bindable(0.5),
step = 0.01,
step,
min = $bindable(0),
max = $bindable(1),
id
@@ -22,8 +22,11 @@
max = value;
}
// svelte-ignore state_referenced_locally only use initial values
const precision = ((step || value).toString().split('.')[1] || '').length;
function strip(input: number) {
return +parseFloat(input + '').toPrecision(2);
return +parseFloat(input + '').toFixed(precision);
}
let inputEl: HTMLInputElement | undefined = $state();
@@ -94,14 +97,22 @@
} else {
value = Math.max(Math.min(min + (max - min) * vx, max), min);
}
(ev.target as HTMLElement)?.blur();
value = strip(value);
// With ctrl + outside of input ev.target becomes HTMLDocument
if (ev.target instanceof HTMLElement) {
ev.target?.blur();
}
}
$effect(() => {
if ((value || 0).toString().length > 5) {
value = strip(value || 0);
if (value.toString().length > 5) {
value = strip(value);
}
value !== undefined && handleChange();
});
let width = $derived(
Number.isFinite(value) ? Math.max((value?.toString().length ?? 1) * 8, 50) + 'px' : '20px'
);
@@ -137,12 +148,12 @@
border-radius: var(--border-radius, 2px);
}
input[type='number']::-webkit-inner-spin-button,
input[type='number']::-webkit-outer-spin-button {
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
}
input[type='number'] {
input[type="number"] {
box-sizing: border-box;
-webkit-appearance: textfield;
-moz-appearance: textfield;

View File

@@ -1,3 +1,8 @@
<!--
@component
@deprecated use Float.svelte
-->
<script lang="ts">
interface Props {
value?: number;
@@ -112,23 +117,23 @@
onmousedown={handleMouseDown}
onmouseup={handleMouseUp}
>
<div class="">
<button onclick={() => handleChange(-step)}>-</button>
<input
bind:value
bind:this={inputEl}
{id}
{step}
{max}
{min}
type="number"
style={`width:${width};`}
/>
<button onclick={() => handleChange(-step)}>-</button>
<input
bind:value
bind:this={inputEl}
{id}
{step}
{max}
{min}
type="number"
style={`width:${width};`}
/>
<button onclick={() => handleChange(+step)}>+</button>
</div>
<button onclick={() => handleChange(+step)}>+</button>
{#if typeof min !== 'undefined' && typeof max !== 'undefined'}
<span class="overlay" style={`width: ${Math.min((value - min) / (max - min), 1) * 100}%`}
<span
class="overlay"
style={`width: ${Math.min((value - min) / (max - min), 1) * 100}%`}
></span>
{/if}
</div>
@@ -146,12 +151,12 @@
border-radius: var(--border-radius, 2px);
}
input[type='number']::-webkit-inner-spin-button,
input[type='number']::-webkit-outer-spin-button {
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
}
input[type='number'] {
input[type="number"] {
-webkit-appearance: textfield;
-moz-appearance: textfield;
appearance: textfield;
@@ -198,7 +203,7 @@
margin-inline: 6px;
}
div input[type='number'] {
div input[type="number"] {
color: var(--text-color);
background-color: transparent;
padding: var(--padding, 6px);

View File

@@ -1,177 +0,0 @@
<script lang="ts">
interface Props {
value?: number;
step?: number;
min?: number;
max?: number;
id?: string;
}
let {
value = $bindable(1),
step = 1,
min = $bindable(0),
max = $bindable(1),
id: _id
}: Props = $props();
const uid = $props.id();
const id = $derived(_id || uid);
if (min > max) {
[min, max] = [max, min];
}
if (value > max) {
max = value;
}
function strip(input: number) {
return +parseFloat(input + '').toPrecision(2);
}
let inputEl = $state() as HTMLInputElement;
let prev = -1;
function update() {
if (prev === value) return;
if (value.toString().length > 5) {
value = strip(value);
}
prev = value;
}
function handleChange(change: number) {
value = Math.max(min ?? -Infinity, Math.min(+value + change, max ?? Infinity));
}
function handleKeyDown(ev: KeyboardEvent) {
if (ev.key === 'Escape' || ev.key === 'Enter') {
inputEl?.blur();
}
}
$effect(() => {
update();
});
let ratio = $derived(((value - min) / (max - min)) * 100);
</script>
<div>
<div class="component-wrapper">
<button onclick={() => handleChange(-step)}>-</button>
<input
bind:value
bind:this={inputEl}
{id}
{step}
{max}
{min}
type="number"
onkeydown={handleKeyDown}
/>
<button onclick={() => handleChange(+step)}>+</button>
</div>
<div class="slider">
<input
type="range"
bind:value
{min}
{max}
{step}
style={`background: linear-gradient(90deg, var(--text-color) ${ratio}%, var(--layer-2, #4b4b4b) ${ratio}%)`}
/>
</div>
</div>
<style>
:root {
--slider-height: 4px;
}
.component-wrapper {
display: flex;
background-color: var(--layer-2, #4b4b4b);
user-select: none;
transition: box-shadow 0.3s ease;
border: solid 1px var(--outline);
overflow: hidden;
border-radius: 0 var(--border-radius, 2px); /* only top */
}
input[type='number']::-webkit-inner-spin-button,
input[type='number']::-webkit-outer-spin-button {
-webkit-appearance: none;
}
input[type='number'] {
-webkit-appearance: textfield;
-moz-appearance: textfield;
appearance: textfield;
cursor: pointer;
font-family: var(--font-family);
font-variant-numeric: tabular-nums;
color: var(--text-color);
background-color: transparent;
padding: var(--padding, 6px);
font-size: 1em;
padding-inline: 10px;
text-align: center;
border: none;
border-style: none;
flex: 1;
width: 72%;
}
button {
background-color: transparent;
border: none;
cursor: pointer;
line-height: 0px;
margin: 0;
color: var(--text-color);
margin-inline: 6px;
}
div input[type='number'] {
color: var(--text-color);
background-color: transparent;
padding: var(--padding, 6px);
padding-inline: 0px;
text-align: center;
border: none;
border-style: none;
}
.slider {
position: relative;
margin-top: -1px; /* hide edge */
}
input[type='range'] {
position: absolute;
appearance: none;
width: 100%;
height: var(--slider-height);
background: var(--layer-2, #4b4b4b);
cursor: pointer;
}
/* Thumb: for Chrome, Safari, Edge */
input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 12px;
height: var(--slider-height);
background: var(--text-color);
box-shadow: none;
}
/* Thumb: for Firefox */
input[type='range']::-moz-range-thumb {
border: none;
width: 12px;
height: var(--slider-height);
background: var(--text-color);
box-shadow: none;
}
</style>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Number from './Number.svelte';
import { Number } from '$lib/index.js';
interface Props {
value?: any;

View File

@@ -1,10 +1,12 @@
<script lang="ts">
import '$lib/app.css';
import { Checkbox, Details, Float, Integer, Number, Select, ShortCut, Vec3 } from '$lib/index.js';
import { Checkbox, Details, Number, Select, ShortCut, Vec3 } from '$lib/index.js';
import Section from './Section.svelte';
let intValue = $state(0);
let floatValue = $state(0.2);
let float2Value = $state(0.02);
let float3Value = $state(1);
let vecValue = $state([0.2, 0.3, 0.4]);
const options = ['strawberry', 'raspberry', 'chickpeas'];
let selectValue = $state(0);
@@ -30,22 +32,22 @@
<Select bind:value={themeIndex} options={themes}></Select>
</div>
<Section title="Integer" value={intValue}>
<Integer bind:value={intValue} />
<Section title="Integer (step inherit)" value={intValue}>
<Number bind:value={intValue} max={2} />
</Section>
<Section title="Float" value={floatValue}>
<Float bind:value={floatValue} />
</Section>
<Section title="Number" value={intValue}>
<Number bind:value={intValue} />
</Section>
<Section title="Number (float)" value={floatValue}>
<Section title="Float (step inherit)" value={floatValue}>
<Number bind:value={floatValue} />
</Section>
<Section title="Float 2 (step inherit)" value={intValue}>
<Number bind:value={float2Value} />
</Section>
<Section title="Float (0.01 step)" value={floatValue}>
<Number bind:value={float3Value} step={0.01} max={3} />
</Section>
<Section title="Vec3" value={JSON.stringify(vecValue)}>
<Vec3 bind:value={vecValue} />
</Section>

View File

@@ -2,15 +2,15 @@
name = "nodarium_utils"
version = "0.1.0"
edition = "2021"
description = "A collection of utilities for Nodarium"
license = "MIT"
repository = "https://github.com/jim-fx/nodes"
description = "A collection of utilities for Nodarium"
[lib]
crate-type = ["rlib"]
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
glam = "0.30.10"
noise = "0.9.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }

View File

@@ -10,7 +10,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"@nodarium/types": "link:../types"
"@nodarium/types": "workspace:"
},
"devDependencies": {
"vite": "^7.3.1",

View File

@@ -33,3 +33,19 @@ macro_rules! log {
}};
}
#[allow(dead_code)]
#[rustfmt::skip]
fn test_split_args(){
let inputs = vec![
vec![0, 1, 0, 4, 1056964608, 1065353216, 1056964608, 1, 4, 1080872141, 1054951342, 32, 1, 1 ],
vec![0, 4, 1056964608, 1065353216, 1056964608, 1, 4],
vec![0, 1, 0, 3, 0, 0, 0, 5, 0, 0, 1073741824, 1073741824, 1, 1, 1, 1, 1, 4, 1065353216, 1054615798, 5, 1, 1 ],
vec![ 0, 1, 0, 3, 0, 0, 0, 1, 4, 1073741824, 1073741824, 32, 1, 1 ],
vec![0, 1, 0, 1, 0, 14, 0, 1056964608, 1056964608, 1056964608, 1058810102, 1056964608, 1069547520, 1056964608, 1050421494, 1056964608, 1075838976, 1056964608, 0, 1, 1, 1, 2, 13, 1, 1],
vec![ 0, 1, 0, 2, 0, 0, 5, 0, 0, 1073741824, 1073741824, 1, 2, 0, 1, 4, 1088212173, 1083388723, 20, 1, 1 ]
];
for input in inputs {
println!("RESULT: {:?}", split_args(&input));
}
}

View File

@@ -1,36 +0,0 @@
use nodarium_utils::{
geometry::{create_multiple_paths, wrap_multiple_paths},
split_args,
};
#[allow(dead_code)]
#[rustfmt::skip]
fn test_split_args(){
let inputs = vec![
vec![0, 1, 0, 4, 1056964608, 1065353216, 1056964608, 1, 4, 1080872141, 1054951342, 32, 1, 1 ],
vec![0, 4, 1056964608, 1065353216, 1056964608, 1, 4],
vec![0, 1, 0, 3, 0, 0, 0, 5, 0, 0, 1073741824, 1073741824, 1, 1, 1, 1, 1, 4, 1065353216, 1054615798, 5, 1, 1 ],
vec![ 0, 1, 0, 3, 0, 0, 0, 1, 4, 1073741824, 1073741824, 32, 1, 1 ],
vec![0, 1, 0, 1, 0, 14, 0, 1056964608, 1056964608, 1056964608, 1058810102, 1056964608, 1069547520, 1056964608, 1050421494, 1056964608, 1075838976, 1056964608, 0, 1, 1, 1, 2, 13, 1, 1],
vec![ 0, 1, 0, 2, 0, 0, 5, 0, 0, 1073741824, 1073741824, 1, 2, 0, 1, 4, 1088212173, 1083388723, 20, 1, 1 ]
];
for input in inputs {
println!("RESULT: {:?}", split_args(&input));
}
}
fn test_path() {
// let path_data = create_path(3, 2);
// println!("{:?}", path_data);
let mut multiple_paths = create_multiple_paths(1, 4, 1);
let mut wrapped_paths = wrap_multiple_paths(&mut multiple_paths);
wrapped_paths[0].points[0] = 1.0;
println!("{:?}", wrapped_paths);
println!("{:?}", multiple_paths);
}
fn main() {
test_path()
}

View File

@@ -3,6 +3,8 @@ interface NodariumExports extends WebAssembly.Exports {
execute: (ptr: number, len: number) => number;
__free: (ptr: number, len: number) => void;
__alloc: (len: number) => number;
getDefinitionPtr: () => number;
getDefinitionLen: () => number;
}
export function createWasmWrapper(buffer: ArrayBuffer) {
@@ -19,8 +21,8 @@ export function createWasmWrapper(buffer: ArrayBuffer) {
if (!exports) return;
const view = new Uint8Array(exports.memory.buffer, ptr, len);
console.log("RUST:", new TextDecoder().decode(view));
}
}
},
},
};
const module = new WebAssembly.Module(buffer);
@@ -43,12 +45,22 @@ export function createWasmWrapper(buffer: ArrayBuffer) {
}
function get_definition() {
const sections = WebAssembly.Module.customSections(module, "nodarium_definition");
const decoder = new TextDecoder();
const sections = WebAssembly.Module.customSections(
module,
"nodarium_definition",
);
if (sections.length > 0) {
const decoder = new TextDecoder();
const jsonString = decoder.decode(sections[0]);
return JSON.parse(jsonString);
}
const ptr = exports.getDefinitionPtr();
const len = exports.getDefinitionLen();
const view = new Uint8Array(exports.memory.buffer, ptr, len);
const jsonString = decoder.decode(view);
return JSON.parse(jsonString);
}
return { execute, get_definition };

127
pnpm-lock.yaml generated
View File

@@ -4,9 +4,19 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
catalogs:
default:
chokidar-cli:
specifier: github:open-cli-tools/chokidar-cli#semver:v4.0.0
version: 4.0.0
importers:
.: {}
.:
devDependencies:
chokidar-cli:
specifier: 'catalog:'
version: https://codeload.github.com/open-cli-tools/chokidar-cli/tar.gz/8dd8a1e8631d377de600f628d819a0cda46c102f
app:
dependencies:
@@ -60,7 +70,7 @@ importers:
specifier: ^1.2.1
version: 1.2.1(tailwindcss@4.1.18)
'@nodarium/types':
specifier: link:../packages/types
specifier: 'workspace:'
version: link:../packages/types
'@sveltejs/adapter-static':
specifier: ^3.0.10
@@ -108,10 +118,10 @@ importers:
packages/registry:
dependencies:
'@nodarium/types':
specifier: link:../types
specifier: 'workspace:'
version: link:../types
'@nodarium/utils':
specifier: link:../utils
specifier: 'workspace:'
version: link:../utils
idb:
specifier: ^8.0.3
@@ -145,7 +155,7 @@ importers:
version: 4.1.18
devDependencies:
'@nodarium/types':
specifier: link:../types
specifier: 'workspace:'
version: link:../types
'@sveltejs/adapter-static':
specifier: ^3.0.10
@@ -205,7 +215,7 @@ importers:
packages/utils:
dependencies:
'@nodarium/types':
specifier: link:../types
specifier: 'workspace:'
version: link:../types
devDependencies:
vite:
@@ -1229,6 +1239,10 @@ packages:
resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==}
engines: {node: '>=6'}
ansi-regex@6.2.2:
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
engines: {node: '>=12'}
ansi-styles@3.2.1:
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
engines: {node: '>=4'}
@@ -1237,6 +1251,10 @@ packages:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
ansi-styles@6.2.3:
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
engines: {node: '>=12'}
anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
@@ -1331,6 +1349,12 @@ packages:
engines: {node: '>= 8.10.0'}
hasBin: true
chokidar-cli@https://codeload.github.com/open-cli-tools/chokidar-cli/tar.gz/8dd8a1e8631d377de600f628d819a0cda46c102f:
resolution: {tarball: https://codeload.github.com/open-cli-tools/chokidar-cli/tar.gz/8dd8a1e8631d377de600f628d819a0cda46c102f}
version: 4.0.0
engines: {node: '>= 20.0.0'}
hasBin: true
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
@@ -1349,6 +1373,10 @@ packages:
cliui@5.0.0:
resolution: {integrity: sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==}
cliui@9.0.1:
resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==}
engines: {node: '>=20'}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
@@ -1533,6 +1561,9 @@ packages:
earcut@2.2.4:
resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==}
emoji-regex@10.6.0:
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
emoji-regex@7.0.3:
resolution: {integrity: sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==}
@@ -1581,6 +1612,10 @@ packages:
engines: {node: '>=18'}
hasBin: true
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
@@ -1719,6 +1754,10 @@ packages:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
get-east-asian-width@1.4.0:
resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==}
engines: {node: '>=18'}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
@@ -2393,10 +2432,18 @@ packages:
resolution: {integrity: sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==}
engines: {node: '>=6'}
string-width@7.2.0:
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
engines: {node: '>=18'}
strip-ansi@5.2.0:
resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==}
engines: {node: '>=6'}
strip-ansi@7.1.2:
resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
engines: {node: '>=12'}
strip-json-comments@3.1.1:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
@@ -2736,6 +2783,10 @@ packages:
resolution: {integrity: sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==}
engines: {node: '>=6'}
wrap-ansi@9.0.2:
resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==}
engines: {node: '>=18'}
ws@8.19.0:
resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==}
engines: {node: '>=10.0.0'}
@@ -2762,6 +2813,10 @@ packages:
y18n@4.0.3:
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
yaml@1.10.2:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
engines: {node: '>= 6'}
@@ -2769,9 +2824,17 @@ packages:
yargs-parser@13.1.2:
resolution: {integrity: sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==}
yargs-parser@22.0.0:
resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==}
engines: {node: ^20.19.0 || ^22.12.0 || >=23}
yargs@13.3.2:
resolution: {integrity: sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==}
yargs@18.0.0:
resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=23}
yn@3.1.1:
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
engines: {node: '>=6'}
@@ -3696,6 +3759,8 @@ snapshots:
ansi-regex@4.1.1: {}
ansi-regex@6.2.2: {}
ansi-styles@3.2.1:
dependencies:
color-convert: 1.9.3
@@ -3704,6 +3769,8 @@ snapshots:
dependencies:
color-convert: 2.0.1
ansi-styles@6.2.3: {}
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
@@ -3796,6 +3863,13 @@ snapshots:
lodash.throttle: 4.1.1
yargs: 13.3.2
chokidar-cli@https://codeload.github.com/open-cli-tools/chokidar-cli/tar.gz/8dd8a1e8631d377de600f628d819a0cda46c102f:
dependencies:
chokidar: 3.6.0
lodash.debounce: 4.0.8
lodash.throttle: 4.1.1
yargs: 18.0.0
chokidar@3.6.0:
dependencies:
anymatch: 3.1.3
@@ -3826,6 +3900,12 @@ snapshots:
strip-ansi: 5.2.0
wrap-ansi: 5.1.0
cliui@9.0.1:
dependencies:
string-width: 7.2.0
strip-ansi: 7.1.2
wrap-ansi: 9.0.2
clsx@2.1.1: {}
color-convert@1.9.3:
@@ -3987,6 +4067,8 @@ snapshots:
earcut@2.2.4: {}
emoji-regex@10.6.0: {}
emoji-regex@7.0.3: {}
enhanced-resolve@5.18.4:
@@ -4082,6 +4164,8 @@ snapshots:
'@esbuild/win32-ia32': 0.27.2
'@esbuild/win32-x64': 0.27.2
escalade@3.2.0: {}
escape-string-regexp@4.0.0: {}
eslint-plugin-svelte@3.14.0(eslint@9.39.2(jiti@2.6.1))(svelte@5.46.4)(ts-node@10.9.2(@types/node@22.8.6)(typescript@5.9.3)):
@@ -4242,6 +4326,8 @@ snapshots:
get-caller-file@2.0.5: {}
get-east-asian-width@1.4.0: {}
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -4900,10 +4986,20 @@ snapshots:
is-fullwidth-code-point: 2.0.0
strip-ansi: 5.2.0
string-width@7.2.0:
dependencies:
emoji-regex: 10.6.0
get-east-asian-width: 1.4.0
strip-ansi: 7.1.2
strip-ansi@5.2.0:
dependencies:
ansi-regex: 4.1.1
strip-ansi@7.1.2:
dependencies:
ansi-regex: 6.2.2
strip-json-comments@3.1.1: {}
supports-color@7.2.0:
@@ -5232,6 +5328,12 @@ snapshots:
string-width: 3.1.0
strip-ansi: 5.2.0
wrap-ansi@9.0.2:
dependencies:
ansi-styles: 6.2.3
string-width: 7.2.0
strip-ansi: 7.1.2
ws@8.19.0:
optional: true
@@ -5248,6 +5350,8 @@ snapshots:
y18n@4.0.3: {}
y18n@5.0.8: {}
yaml@1.10.2: {}
yargs-parser@13.1.2:
@@ -5255,6 +5359,8 @@ snapshots:
camelcase: 5.3.1
decamelize: 1.2.0
yargs-parser@22.0.0: {}
yargs@13.3.2:
dependencies:
cliui: 5.0.0
@@ -5268,6 +5374,15 @@ snapshots:
y18n: 4.0.3
yargs-parser: 13.1.2
yargs@18.0.0:
dependencies:
cliui: 9.0.1
escalade: 3.2.0
get-caller-file: 2.0.5
string-width: 7.2.0
y18n: 5.0.8
yargs-parser: 22.0.0
yn@3.1.1:
optional: true

View File

@@ -1,6 +1,9 @@
packages:
- app
- packages/*
- nodes/**/**/**/*
- '!nodes/**/**/**/pkg'
- '!**/.template'
- "app"
- "packages/*"
- "nodes/**"
- "!**/.template/**"
- "!**/pkg/**"
catalog:
chokidar-cli: github:open-cli-tools/chokidar-cli#semver:v4.0.0