feat: node store interface

This commit is contained in:
max_richter 2024-04-20 02:41:18 +02:00
parent 1d203c687c
commit 78c88e4d66
51 changed files with 772 additions and 552 deletions

View File

@ -36,7 +36,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
history: HistoryManager = new HistoryManager(); history: HistoryManager = new HistoryManager();
constructor(private nodeRegistry: NodeRegistry) { constructor(public registry: NodeRegistry) {
super(); super();
this.nodes.subscribe((nodes) => { this.nodes.subscribe((nodes) => {
this._nodes = nodes; this._nodes = nodes;
@ -82,7 +82,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
} }
getNodeTypes() { getNodeTypes() {
return this.nodeRegistry.getAllNodes(); return this.registry.getAllNodes();
} }
getLinkedNodes(node: Node) { getLinkedNodes(node: Node) {
@ -122,7 +122,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
private _init(graph: Graph) { private _init(graph: Graph) {
const nodes = new Map(graph.nodes.map(node => { const nodes = new Map(graph.nodes.map(node => {
const nodeType = this.nodeRegistry.getNode(node.type); const nodeType = this.registry.getNode(node.type);
if (nodeType) { if (nodeType) {
node.tmp = { node.tmp = {
random: (Math.random() - 0.5) * 2, random: (Math.random() - 0.5) * 2,
@ -164,10 +164,10 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
this.id.set(graph.id); this.id.set(graph.id);
const nodeIds = Array.from(new Set([...graph.nodes.map(n => n.type)])); const nodeIds = Array.from(new Set([...graph.nodes.map(n => n.type)]));
await this.nodeRegistry.load(nodeIds); await this.registry.load(nodeIds);
for (const node of this.graph.nodes) { for (const node of this.graph.nodes) {
const nodeType = this.nodeRegistry.getNode(node.type); const nodeType = this.registry.getNode(node.type);
if (!nodeType) { if (!nodeType) {
logger.error(`Node type not found: ${node.type}`); logger.error(`Node type not found: ${node.type}`);
this.status.set("error"); this.status.set("error");
@ -222,7 +222,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
} }
getNodeType(id: string) { getNodeType(id: string) {
return this.nodeRegistry.getNode(id); return this.registry.getNode(id);
} }
getChildrenOfNode(node: Node) { getChildrenOfNode(node: Node) {
@ -303,7 +303,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
nodes = nodes.map((node, i) => { nodes = nodes.map((node, i) => {
const id = startId + i; const id = startId + i;
idMap.set(node.id, id); idMap.set(node.id, id);
const type = this.nodeRegistry.getNode(node.type); const type = this.registry.getNode(node.type);
if (!type) { if (!type) {
throw new Error(`Node type not found: ${node.type}`); throw new Error(`Node type not found: ${node.type}`);
} }
@ -343,7 +343,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
createNode({ type, position, props = {} }: { type: Node["type"], position: Node["position"], props: Node["props"] }) { createNode({ type, position, props = {} }: { type: Node["type"], position: Node["position"], props: Node["props"] }) {
const nodeType = this.nodeRegistry.getNode(type); const nodeType = this.registry.getNode(type);
if (!nodeType) { if (!nodeType) {
logger.error(`Node type not found: ${type}`); logger.error(`Node type not found: ${type}`);
return; return;

View File

@ -20,6 +20,7 @@
import { createKeyMap } from "../../helpers/createKeyMap"; import { createKeyMap } from "../../helpers/createKeyMap";
import BoxSelection from "../BoxSelection.svelte"; import BoxSelection from "../BoxSelection.svelte";
import AddMenu from "../AddMenu.svelte"; import AddMenu from "../AddMenu.svelte";
import { get } from "svelte/store";
export let graph: GraphManager; export let graph: GraphManager;
@ -78,7 +79,7 @@
} }
function updateNodePosition(node: NodeType) { function updateNodePosition(node: NodeType) {
if (node?.tmp?.ref) { if (node?.tmp?.ref && node?.tmp?.mesh) {
if (node.tmp["x"] !== undefined && node.tmp["y"] !== undefined) { if (node.tmp["x"] !== undefined && node.tmp["y"] !== undefined) {
node.tmp.ref.style.setProperty("--nx", `${node.tmp.x * 10}px`); node.tmp.ref.style.setProperty("--nx", `${node.tmp.x * 10}px`);
node.tmp.ref.style.setProperty("--ny", `${node.tmp.y * 10}px`); node.tmp.ref.style.setProperty("--ny", `${node.tmp.y * 10}px`);
@ -758,6 +759,34 @@
addMenuPosition = null; addMenuPosition = null;
} }
function handleDrop(event: DragEvent) {
if (!event.dataTransfer) return;
const nodeId = event.dataTransfer.getData("data/node-id");
let mx = event.clientX - rect.x;
let my = event.clientY - rect.y;
let nodeOffsetX = event.dataTransfer.getData("data/node-offset-x");
let nodeOffsetY = event.dataTransfer.getData("data/node-offset-y");
if (nodeOffsetX && nodeOffsetY) {
mx += parseInt(nodeOffsetX);
my += parseInt(nodeOffsetY);
}
const pos = projectScreenToWorld(mx, my);
graph.registry.load([nodeId]).then(() => {
graph.createNode({
type: nodeId,
props: {},
position: pos,
});
});
console.log({ nodeId });
}
function handlerDragOver(e: DragEvent) {
e.preventDefault();
}
onMount(() => { onMount(() => {
if (localStorage.getItem("cameraPosition")) { if (localStorage.getItem("cameraPosition")) {
const cPosition = JSON.parse(localStorage.getItem("cameraPosition")!); const cPosition = JSON.parse(localStorage.getItem("cameraPosition")!);
@ -779,6 +808,8 @@
tabindex="0" tabindex="0"
bind:clientWidth={width} bind:clientWidth={width}
bind:clientHeight={height} bind:clientHeight={height}
on:dragover={handlerDragOver}
on:drop={handleDrop}
on:keydown={keymap.handleKeyboardEvent} on:keydown={keymap.handleKeyboardEvent}
on:mousedown={handleMouseDown} on:mousedown={handleMouseDown}
> >

View File

@ -11,7 +11,7 @@
export let graph: Graph; export let graph: Graph;
export let settings: Writable<Record<string, any>> | undefined; export let settings: Writable<Record<string, any>> | undefined;
const manager = new GraphManager(registry); export const manager = new GraphManager(registry);
export const status = manager.status; export const status = manager.status;

View File

@ -9,6 +9,7 @@
import { Color, type Mesh } from "three"; import { Color, type Mesh } from "three";
import NodeFrag from "./Node.frag"; import NodeFrag from "./Node.frag";
import NodeVert from "./Node.vert"; import NodeVert from "./Node.vert";
import NodeHtml from "./NodeHTML.svelte";
export let node: Node; export let node: Node;
export let inView = true; export let inView = true;
@ -22,32 +23,20 @@
const getNodeHeight = getContext<(n: string) => number>("getNodeHeight"); const getNodeHeight = getContext<(n: string) => number>("getNodeHeight");
const type = node?.tmp?.type;
const zOffset = (node.tmp?.random || 0) * 0.5;
const zLimit = 2 - zOffset;
const parameters = Object.entries(type?.inputs || {})
.filter((p) => p[1].type !== "seed")
.filter((p) => !("setting" in p[1]));
let ref: HTMLDivElement;
let meshRef: Mesh; let meshRef: Mesh;
const height = getNodeHeight(node.type); const height = getNodeHeight?.(node.type);
$: if (node && ref && meshRef) { $: if (node && meshRef) {
node.tmp = node.tmp || {}; node.tmp = node.tmp || {};
node.tmp.ref = ref;
node.tmp.mesh = meshRef; node.tmp.mesh = meshRef;
updateNodePosition(node); updateNodePosition?.(node);
} }
onMount(() => { onMount(() => {
node.tmp = node.tmp || {}; node.tmp = node.tmp || {};
node.tmp.ref = ref;
node.tmp.mesh = meshRef; node.tmp.mesh = meshRef;
updateNodePosition(node); updateNodePosition?.(node);
}); });
</script> </script>
@ -83,51 +72,4 @@
/> />
</T.Mesh> </T.Mesh>
<div <NodeHtml {node} {inView} {isActive} {isSelected} {z} />
class="node"
class:active={isActive}
style:--cz={z + zOffset}
style:display={inView && z > zLimit ? "block" : "none"}
class:selected={isSelected}
class:out-of-view={!inView}
data-node-id={node.id}
bind:this={ref}
>
<NodeHeader {node} />
{#each parameters as [key, value], i}
<NodeParameter
bind:node
id={key}
input={value}
isLast={i == parameters.length - 1}
/>
{/each}
</div>
<style>
.node {
position: absolute;
box-sizing: border-box;
user-select: none !important;
cursor: pointer;
width: 200px;
color: var(--text-color);
transform: translate3d(var(--nx), var(--ny), 0);
z-index: 1;
opacity: calc((var(--cz) - 2.5) / 3.5);
font-weight: 300;
--stroke: var(--outline);
--stroke-width: 2px;
}
.node.active {
--stroke: var(--active);
--stroke-width: 2px;
}
.node.selected {
--stroke: var(--selected);
--stroke-width: 2px;
}
</style>

View File

@ -0,0 +1,86 @@
<script lang="ts">
import type { Node } from "@nodes/types";
import NodeHeader from "./NodeHeader.svelte";
import NodeParameter from "./NodeParameter.svelte";
import { getContext, onMount } from "svelte";
export let isActive = false;
export let isSelected = false;
export let inView = true;
export let z = 2;
let ref: HTMLDivElement;
export let node: Node;
export let position = "absolute";
const zOffset = (node.tmp?.random || 0) * 0.5;
const zLimit = 2 - zOffset;
const type = node?.tmp?.type;
const parameters = Object.entries(type?.inputs || {})
.filter((p) => p[1].type !== "seed")
.filter((p) => !("setting" in p[1]));
const updateNodePosition =
getContext<(n: Node) => void>("updateNodePosition");
$: if (node && ref) {
node.tmp = node.tmp || {};
node.tmp.ref = ref;
updateNodePosition?.(node);
}
onMount(() => {
node.tmp = node.tmp || {};
node.tmp.ref = ref;
updateNodePosition?.(node);
});
</script>
<div
class="node {position}"
class:active={isActive}
style:--cz={z + zOffset}
style:display={inView && z > zLimit ? "block" : "none"}
class:selected={isSelected}
class:out-of-view={!inView}
data-node-id={node.id}
bind:this={ref}
>
<NodeHeader {node} />
{#each parameters as [key, value], i}
<NodeParameter
bind:node
id={key}
input={value}
isLast={i == parameters.length - 1}
/>
{/each}
</div>
<style>
.node {
box-sizing: border-box;
user-select: none !important;
cursor: pointer;
width: 200px;
color: var(--text-color);
transform: translate3d(var(--nx), var(--ny), 0);
z-index: 1;
opacity: calc((var(--cz) - 2.5) / 3.5);
font-weight: 300;
--stroke: var(--outline);
--stroke-width: 2px;
}
.node.active {
--stroke: var(--active);
--stroke-width: 2px;
}
.node.selected {
--stroke: var(--selected);
--stroke-width: 2px;
}
</style>

View File

@ -14,10 +14,10 @@
function handleMouseDown(event: MouseEvent) { function handleMouseDown(event: MouseEvent) {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
setDownSocket({ setDownSocket?.({
node, node,
index: 0, index: 0,
position: getSocketPosition(node, 0), position: getSocketPosition?.(node, 0),
}); });
} }

View File

@ -15,9 +15,11 @@
$: if (node?.props?.[id] !== value) { $: if (node?.props?.[id] !== value) {
node.props = { ...node.props, [id]: value }; node.props = { ...node.props, [id]: value };
if (graph) {
graph.save(); graph.save();
graph.execute(); graph.execute();
} }
}
</script> </script>
<Input id="input-{elementId}" {input} bind:value /> <Input id="input-{elementId}" {input} bind:value />

View File

@ -20,8 +20,8 @@
const socketId = `${node.id}-${id}`; const socketId = `${node.id}-${id}`;
const graph = getGraphManager(); const graph = getGraphManager();
const graphId = graph.id; const graphId = graph?.id;
const inputSockets = graph.inputSockets; const inputSockets = graph?.inputSockets;
const elementId = `input-${Math.random().toString(36).substring(7)}`; const elementId = `input-${Math.random().toString(36).substring(7)}`;
@ -34,10 +34,10 @@
function handleMouseDown(ev: MouseEvent) { function handleMouseDown(ev: MouseEvent) {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
setDownSocket({ setDownSocket?.({
node, node,
index: id, index: id,
position: getSocketPosition(node, id), position: getSocketPosition?.(node, id),
}); });
} }
@ -76,7 +76,7 @@
class:disabled={$possibleSocketIds && !$possibleSocketIds.has(socketId)} class:disabled={$possibleSocketIds && !$possibleSocketIds.has(socketId)}
> >
{#key id && graphId} {#key id && graphId}
<div class="content" class:disabled={$inputSockets.has(socketId)}> <div class="content" class:disabled={$inputSockets?.has(socketId)}>
{#if inputType.label !== false} {#if inputType.label !== false}
<label for={elementId}>{input.label || id}</label> <label for={elementId}>{input.label || id}</label>
{/if} {/if}

View File

@ -1,4 +1,4 @@
import { get, writable } from "svelte/store"; import { derived, get, writable } from "svelte/store";
type Shortcut = { type Shortcut = {
key: string | string[], key: string | string[],
@ -9,35 +9,45 @@ type Shortcut = {
callback: (event: KeyboardEvent) => void callback: (event: KeyboardEvent) => void
} }
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(keys); const store = writable(new Map(keys.map(k => [getShortcutId(k), k])));
return { return {
handleKeyboardEvent: (event: KeyboardEvent) => { handleKeyboardEvent: (event: KeyboardEvent) => {
const key = get(store).find(k => { const key = [...get(store).values()].find(k => {
if (Array.isArray(k.key) ? !k.key.includes(event.key) : k.key !== event.key) return false; 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 ("shift" in k && k.shift !== event.shiftKey) return false;
if ("ctrl" in k && k.ctrl !== event.ctrlKey) return false; if ("ctrl" in k && k.ctrl !== event.ctrlKey) return false;
if ("alt" in k && k.alt !== event.altKey) return false; if ("alt" in k && k.alt !== event.altKey) return false;
return true; return true;
}); });
console.log({ keys: get(store), out: key, key: event.key });
key?.callback(event); key?.callback(event);
}, },
addShortcut: (shortcut: Shortcut) => { addShortcut: (shortcut: Shortcut) => {
if (Array.isArray(shortcut.key)) { if (Array.isArray(shortcut.key)) {
for (const k of shortcut.key) { for (const k of shortcut.key) {
store.update(keys => { store.update(shortcuts => {
if (keys.find(kk => kk.key === k)) return keys; const id = getShortcutId({ ...shortcut, key: k });
return [...keys, { ...shortcut, key: k }]; shortcuts.delete(id);
shortcuts.set(id, { ...shortcut, key: k });
return shortcuts;
}); });
} }
} else { } else {
store.update(keys => [...keys, shortcut]); store.update(shortcuts => {
const id = getShortcutId(shortcut);
shortcuts.delete(id);
shortcuts.set(id, shortcut);
return shortcuts;
});
} }
}, },
keys: store keys: derived(store, $store => Array.from($store.values()))
} }
} }

View File

@ -0,0 +1,93 @@
import type { NodeRegistry, NodeType } from "@nodes/types";
import { createWasmWrapper } from "@nodes/utils";
import { createLogger } from "./helpers";
const log = createLogger("node-registry");
export class RemoteNodeRegistry implements NodeRegistry {
status: "loading" | "ready" | "error" = "loading";
private nodes: Map<string, NodeType> = new Map();
constructor(private url: string) { }
private async loadNode(id: `${string}/${string}/${string}`) {
const wasmResponse = await this.fetchNode(id);
// Setup Wasm wrapper
const wrapper = createWasmWrapper();
const module = new WebAssembly.Module(wasmResponse);
const instance = new WebAssembly.Instance(module, { ["./index_bg.js"]: wrapper });
wrapper.setInstance(instance);
const definition = wrapper.get_definition();
return {
...definition,
id,
execute: wrapper.execute
};
}
async fetchUsers() {
const response = await fetch(`${this.url}/nodes/users.json`);
if (!response.ok) {
throw new Error(`Failed to load users`);
}
return response.json();
}
async fetchUser(userId: `${string}`) {
const response = await fetch(`${this.url}/nodes/${userId}.json`);
if (!response.ok) {
throw new Error(`Failed to load user ${userId}`);
}
return response.json();
}
async fetchCollection(userCollectionId: `${string}/${string}`) {
const response = await fetch(`${this.url}/nodes/${userCollectionId}.json`);
if (!response.ok) {
throw new Error(`Failed to load collection ${userCollectionId}`);
}
return response.json();
}
async fetchNode(nodeId: `${string}/${string}/${string}`) {
const response = await fetch(`${this.url}/nodes/${nodeId}.wasm`);
if (!response.ok) {
throw new Error(`Failed to load node wasm ${nodeId}`);
}
return response.arrayBuffer();
}
async fetchNodeDefinition(nodeId: `${string}/${string}/${string}`) {
const response = await fetch(`${this.url}/nodes/${nodeId}.json`);
if (!response.ok) {
throw new Error(`Failed to load node definition ${nodeId}`);
}
return response.json()
}
async load(nodeIds: `${string}/${string}/${string}`[]) {
const a = performance.now();
const nodes = await Promise.all(nodeIds.map(id => this.loadNode(id)));
for (const node of nodes) {
this.nodes.set(node.id, node);
}
const duration = performance.now() - a;
log.log("loaded nodes in", duration, "ms");
this.status = "ready";
}
getNode(id: string) {
return this.nodes.get(id);
}
getAllNodes() {
return [...this.nodes.values()];
}
}

View File

@ -1,120 +1,101 @@
import type { NodeRegistry, NodeType } from "@nodes/types"; import { createWasmWrapper } from "@nodes/utils"
import { createWasmWrapper } from "@nodes/utils"; import fs from "fs/promises"
import path from "path"
import { createLogger } from "./helpers"; export async function getWasm(id: `${string}/${string}/${string}`) {
const filePath = path.resolve(`../nodes/${id}/pkg/index_bg.wasm`);
const nodeTypes: NodeType[] = [ try {
{ await fs.access(filePath);
id: "max/plantarium/float", } catch (e) {
inputs: { return null
"value": { type: "float", value: 0.1, internal: true },
},
outputs: ["float"],
execute: (value) => { return value; }
},
{
id: "max/plantarium/math",
inputs: {
"op_type": { label: "type", type: "select", options: ["add", "subtract", "multiply", "divide"], value: 0 },
"a": { type: "float" },
"b": { type: "float" },
},
outputs: ["float"],
execute: ([op_type, a, b]: number[]) => {
switch (op_type) {
case 0: return a + b;
case 1: return a - b;
case 2: return a * b;
case 3: return a / b;
}
}
},
{
id: "max/plantarium/output",
inputs: {
"input": { type: "float" },
},
outputs: [],
}
]
const log = createLogger("node-registry");
export class RemoteNodeRegistry implements NodeRegistry {
status: "loading" | "ready" | "error" = "loading";
private nodes: Map<string, NodeType> = new Map();
constructor(private url: string) { }
private async loadNode(id: string) {
const nodeUrl = `${this.url}/n/${id}`;
const [response, wasmResponse] = await Promise.all([fetch(nodeUrl), fetch(`${nodeUrl}/wasm`)]);
if (!wasmResponse.ok || !response.ok) {
this.status = "error";
throw new Error(`Failed to load node ${id}`);
} }
// Setup Wasm wrapper const file = await fs.readFile(filePath);
const bytes = new Uint8Array(file);
return bytes;
}
export async function getNodeWasm(id: `${string}/${string}/${string}`) {
const wasmBytes = await getWasm(id);
if (!wasmBytes) return null;
const wrapper = createWasmWrapper(); const wrapper = createWasmWrapper();
const module = new WebAssembly.Module(await wasmResponse.arrayBuffer()); const module = new WebAssembly.Module(wasmBytes);
const instance = new WebAssembly.Instance(module, { ["./index_bg.js"]: wrapper }); const instance = new WebAssembly.Instance(module, { ["./index_bg.js"]: wrapper });
wrapper.setInstance(instance); wrapper.setInstance(instance)
const node = await response.json(); return wrapper;
node.execute = wrapper.execute;
return node;
} }
async load(nodeIds: string[]) {
const a = performance.now();
nodeIds.push("max/plantarium/random"); export async function getNode(id: `${string}/${string}/${string}`) {
nodeIds.push("max/plantarium/float");
nodeIds.push("max/plantarium/triangle");
nodeIds.push("max/plantarium/vec3");
nodeIds.push("max/plantarium/output");
nodeIds.push("max/plantarium/array");
nodeIds.push("max/plantarium/sum");
nodeIds.push("max/plantarium/stem");
nodeIds.push("max/plantarium/box");
nodeIds.push("max/plantarium/math");
const nodes = await Promise.all(nodeIds.map(id => this.loadNode(id))); const wrapper = await getNodeWasm(id);
for (const node of nodes) { const definition = wrapper?.get_definition?.();
this.nodes.set(node.id, node);
if (!definition) return null;
const { inputs, outputs } = definition;
try {
return { id, inputs, outputs }
} catch (e) {
console.log("Failed to parse input types for node", { id });
} }
const duration = performance.now() - a;
log.log("loaded nodes in", duration, "ms");
this.status = "ready";
} }
getNode(id: string) { export async function getCollectionNodes(userId: `${string}/${string}`) {
return this.nodes.get(id); const nodes = await fs.readdir(path.resolve(`../nodes/${userId}`));
return nodes
.filter(n => n !== "pkg" && n !== ".template")
.map(n => {
return {
id: `${userId}/${n}`,
}
})
} }
getAllNodes() { export async function getCollection(userId: `${string}/${string}`) {
return [...this.nodes.values()]; const nodes = await getCollectionNodes(userId);
return {
id: userId,
nodes,
} }
} }
export class MemoryNodeRegistry implements NodeRegistry { export async function getUserCollections(userId: string) {
const collections = await fs.readdir(path.resolve(`../nodes/${userId}`));
status: "loading" | "ready" | "error" = "ready"; return Promise.all(collections.map(async n => {
const nodes = await getCollectionNodes(`${userId}/${n}`);
async load(nodeIds: string[]) { return {
// Do nothing id: `${userId}/${n}`,
nodes,
}
}));
} }
getNode(id: string) { export async function getUser(userId: string) {
return nodeTypes.find((nodeType) => nodeType.id === id); const collections = await getUserCollections(userId);
} return {
getAllNodes() { id: userId,
return [...nodeTypes]; collections
} }
} }
export async function getUsers() {
const nodes = await fs.readdir(path.resolve("../nodes"));
const users = await Promise.all(nodes.map(async n => {
const collections = await getUserCollections(n);
return {
id: n,
collections
}
}))
return users;
}

View File

@ -0,0 +1,81 @@
<script lang="ts">
import type { Writable } from "svelte/store";
export let activeId: Writable<string>;
$: [activeUser, activeCollection, activeNode] = $activeId.split(`/`);
</script>
<div class="breadcrumbs">
{#if activeUser}
<button
on:click={() => {
$activeId = "";
}}
>
root
</button>
{#if activeCollection}
<button
on:click={() => {
$activeId = activeUser;
}}
>
{activeUser}
</button>
{#if activeNode}
<button
on:click={() => {
$activeId = `${activeUser}/${activeCollection}`;
}}
>
{activeCollection}
</button>
<span>{activeNode}</span>
{:else}
<span>{activeCollection}</span>
{/if}
{:else}
<span>{activeUser}</span>
{/if}
{:else}
<span>root</span>
{/if}
</div>
<style>
.breadcrumbs {
display: flex;
align-items: center;
padding: 0.4em;
gap: 0.8em;
height: 1em;
border-bottom: solid thin var(--outline);
}
.breadcrumbs > button {
position: relative;
background: none;
font-family: var(--font-family);
border: none;
font-size: 1em;
padding: 0px;
cursor: pointer;
}
.breadcrumbs > button::after {
content: "/";
position: absolute;
right: -11px;
opacity: 0.5;
white-space: pre;
text-decoration: none !important;
}
.breadcrumbs > button:hover {
text-decoration: underline;
}
.breadcrumbs > span {
font-size: 1em;
opacity: 0.5;
}
</style>

View File

@ -0,0 +1,74 @@
<script lang="ts">
import NodeHtml from "$lib/graph-interface/node/NodeHTML.svelte";
import type { NodeType } from "@nodes/types";
export let node: NodeType;
let dragging = false;
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);
e.dataTransfer.setData(
"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(),
);
}
</script>
<div class="node-wrapper" class:dragging>
<div
on:dragend={() => {
dragging = false;
}}
draggable={true}
role="button"
tabindex="0"
on:dragstart={handleDragStart}
>
<NodeHtml
inView={true}
position={"relative"}
z={5}
node={{
id: 0,
type: node.id,
position: [0, 0],
tmp: {
type: node,
},
}}
/>
</div>
</div>
<style>
.node-wrapper {
width: fit-content;
border-radius: 5px;
box-sizing: border-box;
border: solid 2px transparent;
padding: 5px;
margin-left: -5px;
}
.dragging {
border: dashed 2px var(--outline);
}
.node-wrapper > div {
opacity: 1;
display: block;
pointer-events: all;
transition: opacity 0.2s;
}
.dragging > div {
opacity: 0.2;
}
</style>

View File

@ -0,0 +1,92 @@
<script lang="ts">
import type { GraphManager } from "$lib/graph-interface/graph-manager";
import Node from "$lib/graph-interface/node/Node.svelte";
import localStore from "$lib/helpers/localStore";
import type { RemoteNodeRegistry } from "$lib/node-registry-client";
import { Canvas } from "@threlte/core";
import BreadCrumbs from "./BreadCrumbs.svelte";
import NodeHtml from "$lib/graph-interface/node/NodeHTML.svelte";
import DraggableNode from "./DraggableNode.svelte";
export let nodeRegistry: RemoteNodeRegistry;
export let manager: GraphManager;
function handleImport() {
nodeRegistry.load([$activeId]);
}
const activeId = localStore<
`${string}` | `${string}/${string}` | `${string}/${string}/${string}`
>("nodes.store.activeId", "");
$: [activeUser, activeCollection, activeNode] = $activeId.split(`/`);
</script>
<BreadCrumbs {activeId} />
<div class="wrapper">
{#if !activeUser}
<h3>Users</h3>
{#await nodeRegistry.fetchUsers()}
<div>Loading...</div>
{:then users}
{#each users as user}
<button
on:click={() => {
$activeId = user.id;
}}>{user.id}</button
>
{/each}
{:catch error}
<div>{error.message}</div>
{/await}
{:else if !activeCollection}
{#await nodeRegistry.fetchUser(activeUser)}
<div>Loading...</div>
{:then user}
<h3>Collections</h3>
{#each user.collections as collection}
<button
on:click={() => {
$activeId = collection.id;
}}
>
{collection.id.split(`/`)[1]}
</button>
{/each}
{:catch error}
<div>{error.message}</div>
{/await}
{:else if !activeNode}
<h3>Nodes</h3>
{#await nodeRegistry.fetchCollection(`${activeUser}/${activeCollection}`)}
<div>Loading...</div>
{:then collection}
{#each collection.nodes as node}
<button
on:click={() => {
$activeId = node.id;
}}
>
{node.id.split(`/`)[2]}
</button>
{/each}
{:catch error}
<div>{error.message}</div>
{/await}
{:else}
{#await nodeRegistry.fetchNodeDefinition(`${activeUser}/${activeCollection}/${activeNode}`)}
<div>Loading...</div>
{:then node}
<DraggableNode {node} />
{:catch error}
<div>{error.message}</div>
{/await}
{/if}
</div>
<style>
.wrapper {
padding: 1em;
}
</style>

View File

@ -0,0 +1,31 @@
<script lang="ts">
</script>
<span class="spinner"></span>
<style>
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.spinner::before {
content: "";
position: absolute;
bottom: 10px;
right: 10px;
width: 20px;
height: 20px;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
animation: spin 1s linear infinite;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' class='icon icon-tabler icons-tabler-outline icon-tabler-loader-2'%3E%3Cpath stroke='none' d='M0 0h24v24H0z' fill='none'/%3E%3Cpath d='M12 3a9 9 0 1 0 9 9' /%3E%3C/svg%3E");
background-size: cover;
z-index: 2;
}
</style>

View File

@ -14,15 +14,15 @@
<div class="command-wrapper"> <div class="command-wrapper">
<div class="command"> <div class="command">
{#if key.ctrl} {#if key.ctrl}
<b>Ctrl</b> <span>Ctrl + </span>
{/if} {/if}
{#if key.shift} {#if key.shift}
<b>Shift</b> <span>Shift</span>
{/if} {/if}
{#if key.alt} {#if key.alt}
<b>Alt</b> <span>Alt</span>
{/if} {/if}
<b>{key.key}</b> <span>{key.key}</span>
</div> </div>
</div> </div>
<p>{key.description}</p> <p>{key.description}</p>
@ -51,21 +51,18 @@
.command-wrapper { .command-wrapper {
display: flex; display: flex;
justify-content: left; justify-content: right;
align-items: center; align-items: center;
} }
.command { .command {
background: var(--layer-3); background: var(--outline);
padding: 0.4em; padding: 0.4em;
font-size: 0.8em;
border-radius: 0.3em; border-radius: 0.3em;
white-space: nowrap; white-space: nowrap;
} }
.command > * {
color: var(--layer-0);
}
p { p {
font-size: 0.9em; font-size: 0.9em;
margin: 0; margin: 0;

View File

@ -113,7 +113,7 @@
transform 0.2s, transform 0.2s,
background 0.2s ease; background 0.2s ease;
width: 30%; width: 30%;
min-width: 300px; min-width: 350px;
} }
h1 { h1 {

View File

@ -0,0 +1 @@
export const prerender = true;

View File

@ -5,7 +5,3 @@
</script> </script>
<slot /> <slot />
{#if false}
<span class="absolute i-tabler-settings w-6 h-6 block"></span>
{/if}

View File

@ -2,17 +2,20 @@
import Grid from "$lib/grid"; import Grid from "$lib/grid";
import GraphInterface from "$lib/graph-interface"; import GraphInterface from "$lib/graph-interface";
import { MemoryRuntimeExecutor } from "$lib/runtime-executor"; import { MemoryRuntimeExecutor } from "$lib/runtime-executor";
import { RemoteNodeRegistry } from "$lib/node-registry"; import { RemoteNodeRegistry } from "$lib/node-registry-client";
import * as templates from "$lib/graph-templates"; import * as templates from "$lib/graph-templates";
import type { Graph } from "@nodes/types"; import type { Graph } from "@nodes/types";
import Viewer from "$lib/viewer/Viewer.svelte"; import Viewer from "$lib/result-viewer/Viewer.svelte";
import Settings from "$lib/settings/Settings.svelte"; import Settings from "$lib/settings/Settings.svelte";
import { AppSettings, AppSettingTypes } from "$lib/settings/app-settings"; import { AppSettings, AppSettingTypes } from "$lib/settings/app-settings";
import { get, writable, type Writable } from "svelte/store"; import { get, writable, type Writable } from "svelte/store";
import Keymap from "$lib/settings/Keymap.svelte"; import Keymap from "$lib/settings/Keymap.svelte";
import type { createKeyMap } from "$lib/helpers/createKeyMap"; import type { createKeyMap } from "$lib/helpers/createKeyMap";
import NodeStore from "$lib/node-store/NodeStore.svelte";
import type { GraphManager } from "$lib/graph-interface/graph-manager";
import { setContext } from "svelte";
const nodeRegistry = new RemoteNodeRegistry("http://localhost:3001"); const nodeRegistry = new RemoteNodeRegistry("");
const runtimeExecutor = new MemoryRuntimeExecutor(nodeRegistry); const runtimeExecutor = new MemoryRuntimeExecutor(nodeRegistry);
let res: Int32Array; let res: Int32Array;
@ -21,7 +24,11 @@
? JSON.parse(localStorage.getItem("graph")!) ? JSON.parse(localStorage.getItem("graph")!)
: templates.grid(3, 3); : templates.grid(3, 3);
let manager: GraphManager;
let managerStatus: Writable<"loading" | "error" | "idle">; let managerStatus: Writable<"loading" | "error" | "idle">;
$: if (manager) {
setContext("graphManager", manager);
}
let keymap: ReturnType<typeof createKeyMap>; let keymap: ReturnType<typeof createKeyMap>;
@ -41,6 +48,7 @@
definition: AppSettingTypes, definition: AppSettingTypes,
}, },
shortcuts: {}, shortcuts: {},
nodeStore: {},
graph: {}, graph: {},
}; };
@ -53,7 +61,16 @@
}; };
settings = settings; settings = settings;
console.log({ settings }); }
$: if (manager) {
settings.nodeStore = {
id: "Node Store",
icon: "i-tabler-database",
props: { nodeRegistry, manager },
component: NodeStore,
};
settings = settings;
} }
function handleSettings( function handleSettings(
@ -91,10 +108,10 @@
<Grid.Cell> <Grid.Cell>
{#key graph} {#key graph}
<GraphInterface <GraphInterface
bind:manager
registry={nodeRegistry} registry={nodeRegistry}
{graph} {graph}
bind:keymap bind:keymap
bind:status={managerStatus}
settings={settings?.graph?.settings} settings={settings?.graph?.settings}
on:settings={handleSettings} on:settings={handleSettings}
on:result={handleResult} on:result={handleResult}

View File

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

View File

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

View File

@ -0,0 +1,31 @@
import { json } from "@sveltejs/kit";
import type { EntryGenerator, RequestHandler } from "./$types";
import { getNode } from "$lib/node-registry";
import * as registry from "$lib/node-registry";
export const prerender = true;
export const entries: EntryGenerator = async () => {
const users = await registry.getUsers();
return users.map(user => {
return user.collections.map(collection => {
return collection.nodes.map(node => {
return { user: user.id, collection: collection.id, node: node.id }
});
})
}).flat(2);
}
export const GET: RequestHandler = async function GET({ params }) {
const nodeId = `${params.user}/${params.collection}/${params.node}` as const;
try {
const node = await getNode(nodeId);
return json(node);
} catch (err) {
console.log(err)
return new Response("Not found", { status: 404 });
}
}

View File

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

View File

@ -0,0 +1,14 @@
import { json } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
import * as registry from "$lib/node-registry";
export const prerender = true;
export const GET: RequestHandler = async function GET() {
const users = await registry.getUsers();
return json(users);
}

View File

@ -1,13 +0,0 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View File

@ -1,31 +0,0 @@
/** @type { import("eslint").Linter.Config } */
module.exports = {
root: true,
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2017: true,
node: true
},
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
]
};

View File

@ -1,10 +0,0 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

View File

@ -1 +0,0 @@
engine-strict=true

View File

@ -1,4 +0,0 @@
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View File

@ -1,8 +0,0 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

View File

@ -1,38 +0,0 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.

View File

@ -1,40 +0,0 @@
{
"name": "node-registry",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test": "vitest",
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.2.0",
"@sveltejs/kit": "^2.5.6",
"@sveltejs/vite-plugin-svelte": "^3.1.0",
"@types/eslint": "^8.56.9",
"@typescript-eslint/eslint-plugin": "^7.7.0",
"@typescript-eslint/parser": "^7.7.0",
"eslint": "^9.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.37.0",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.2.3",
"svelte": "^4.2.15",
"svelte-check": "^3.6.9",
"tslib": "^2.6.2",
"typescript": "^5.4.5",
"vite": "^5.2.9",
"vite-plugin-wasm": "^3.3.0",
"vitest": "^1.5.0"
},
"type": "module",
"dependencies": {
"@nodes/utils": "link:../utils",
"utils": "link:../utils"
}
}

View File

@ -1,13 +0,0 @@
// 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 {}
}
}
export {};

View File

@ -1,12 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@ -1 +0,0 @@
// place files you want to import through the `$lib` alias in this folder.

View File

@ -1,32 +0,0 @@
import { createWasmWrapper } from "@nodes/utils"
export async function getNodeWasm(id: `${string}/${string}/${string}`) {
const wasmResponse = await fetch(`/n/${id}/wasm`);
if (!wasmResponse.ok) {
throw new Error(`Failed to load node ${id}`);
}
const wrapper = createWasmWrapper();
const module = new WebAssembly.Module(await wasmResponse.arrayBuffer());
const instance = new WebAssembly.Instance(module, { ["./index_bg.js"]: wrapper });
wrapper.setInstance(instance)
return wrapper;
}
export async function getNode(id: `${string}/${string}/${string}`) {
const wrapper = await getNodeWasm(id);
const { inputs, outputs } = wrapper?.get_definition?.();
try {
return { id, inputs, outputs }
} catch (e) {
console.log("Failed to parse input types for node", { id });
}
}

View File

@ -1,2 +0,0 @@
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>

View File

@ -1,17 +0,0 @@
import { json } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
import { getNode } from "$lib/registry";
export const GET: RequestHandler = async function GET({ fetch, params }) {
globalThis.fetch = fetch;
const nodeId = `${params.user}/${params.collection}/${params.node}` as const;
try {
const node = await getNode(nodeId);
return json(node);
} catch (err) {
console.log(err)
return new Response("Not found", { status: 404 });
}
}

View File

@ -1,27 +0,0 @@
<script lang="ts">
import type { PageData } from './$types';
import { getNode, getNodeWasm } from '$lib/registry';
import { onMount } from 'svelte';
export let data: PageData;
const nodeId = `${data.params.user}/${data.params.collection}/${data.params.node}` as const;
let node;
let wasm;
onMount(async () => {
wasm = await getNodeWasm(nodeId);
window['wasm'] = wasm;
node = await getNode(nodeId);
});
</script>
<h1>{data.params.user}/{data.params.collection}/{data.params.node}</h1>
<h3>Node Definition</h3>
{#if !node}
<p>Loading Node</p>
{:else}
<pre>{JSON.stringify(node, null, 2)}</pre>
{/if}

View File

@ -1,7 +0,0 @@
import type { PageLoad } from "./$types";
export const load: PageLoad = ({ params }) => {
return {
params
}
};

View File

@ -1,20 +0,0 @@
import type { RequestHandler } from "./$types";
import fs from "fs/promises";
import path from "path";
export const GET: RequestHandler = async function GET({ fetch, params }) {
const filePath = path.resolve(`../../nodes/${params.user}/${params.collection}/${params.node}/pkg/index_bg.wasm`);
try {
await fs.access(filePath);
} catch (e) {
return new Response("Not found", { status: 404 });
}
const file = await fs.readFile(filePath);
const bytes = new Uint8Array(file);
return new Response(bytes, { status: 200, headers: { "Content-Type": "application/wasm" } });
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,20 +0,0 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter()
}
};
export default config;

View File

@ -1,19 +0,0 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

View File

@ -1,14 +0,0 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';
import wasm from 'vite-plugin-wasm';
export default defineConfig({
plugins: [sveltekit(), wasm()],
server: {
port: 3001,
},
test: {
include: ['src/**/*.{test,spec}.{js,ts}']
}
});

View File

@ -35,7 +35,7 @@ export type NodeType = {
meta?: { meta?: {
title?: string; title?: string;
}, },
execute?: (args: number[]) => unknown; execute?: (args: Int32Array) => Int32Array;
} }
export type Socket = { export type Socket = {
@ -44,7 +44,6 @@ export type Socket = {
position: [number, number]; position: [number, number];
}; };
export interface NodeRegistry { export interface NodeRegistry {
/** /**
* The status of the node registry * The status of the node registry