feat: migrate some more stuff to svelte-5, mainly app settings
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 4s
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 4s
This commit is contained in:
@ -11,7 +11,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@nodes/registry": "link:../packages/registry",
|
||||
"@nodes/runtime": "link:../packages/runtime",
|
||||
"@nodes/ui": "link:../packages/ui",
|
||||
"@nodes/utils": "link:../packages/utils",
|
||||
"@sveltejs/kit": "^2.7.4",
|
||||
|
@ -29,16 +29,22 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
|
||||
|
||||
private _nodes: Map<number, Node> = new Map();
|
||||
nodes: Writable<Map<number, Node>> = writable(new Map());
|
||||
settingTypes: Record<string, NodeInput> = {};
|
||||
settings: Record<string, unknown> = {};
|
||||
|
||||
private _edges: Edge[] = [];
|
||||
edges: Writable<Edge[]> = writable([]);
|
||||
|
||||
settingTypes: Record<string, NodeInput> = {};
|
||||
settings: Record<string, unknown> = {};
|
||||
|
||||
currentUndoGroup: number | null = null;
|
||||
|
||||
inputSockets: Writable<Set<string>> = writable(new Set());
|
||||
|
||||
history: HistoryManager = new HistoryManager();
|
||||
execute = throttle(() => {
|
||||
if (this.loaded === false) return;
|
||||
this.emit("result", this.serialize());
|
||||
}, 10);
|
||||
|
||||
constructor(public registry: NodeRegistry) {
|
||||
super();
|
||||
@ -53,7 +59,6 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
|
||||
}
|
||||
this.inputSockets.set(s);
|
||||
});
|
||||
this.execute = throttle(() => this._execute(), 10);
|
||||
}
|
||||
|
||||
serialize(): Graph {
|
||||
@ -67,7 +72,6 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
|
||||
const edges = this._edges.map(edge => [edge[0].id, edge[1], edge[2].id, edge[3]]) as Graph["edges"];
|
||||
const serialized = { id: this.graph.id, settings: this.settings, nodes, edges };
|
||||
logger.groupEnd();
|
||||
console.log({ serialized });
|
||||
|
||||
return clone(serialized);
|
||||
}
|
||||
@ -86,11 +90,6 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
|
||||
}
|
||||
|
||||
|
||||
execute() { }
|
||||
_execute() {
|
||||
if (this.loaded === false) return;
|
||||
this.emit("result", this.serialize());
|
||||
}
|
||||
|
||||
getNodeDefinitions() {
|
||||
return this.registry.getAllNodes();
|
||||
@ -111,9 +110,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
|
||||
return [...nodes.values()];
|
||||
}
|
||||
|
||||
|
||||
getEdgesBetweenNodes(nodes: Node[]): [number, number, number, string][] {
|
||||
|
||||
const edges = [];
|
||||
for (const node of nodes) {
|
||||
const children = node.tmp?.children || [];
|
||||
|
@ -21,11 +21,10 @@
|
||||
import HelpView from "../HelpView.svelte";
|
||||
import FileSaver from "file-saver";
|
||||
import { Canvas } from "@threlte/core";
|
||||
import { getGraphManager } from "./context.js";
|
||||
|
||||
const state = getGraphState();
|
||||
|
||||
export let manager: GraphManager;
|
||||
|
||||
export let snapToGrid = true;
|
||||
export let showGrid = true;
|
||||
export let showHelp = false;
|
||||
@ -33,7 +32,8 @@
|
||||
let keymap =
|
||||
getContext<ReturnType<typeof createKeyMap>>("keymap") || createKeyMap([]);
|
||||
|
||||
setContext("graphManager", manager);
|
||||
const manager = getGraphManager();
|
||||
|
||||
const status = manager.status;
|
||||
const nodes = manager.nodes;
|
||||
const edges = manager.edges;
|
||||
@ -965,6 +965,7 @@
|
||||
<style>
|
||||
.graph-wrapper {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
height: 100%;
|
||||
}
|
||||
|
@ -8,6 +8,9 @@
|
||||
import { createKeyMap } from "$lib/helpers/createKeyMap";
|
||||
import { GraphState } from "./state.svelte";
|
||||
|
||||
const state = new GraphState();
|
||||
setContext("graphState", state);
|
||||
|
||||
type Props = {
|
||||
graph: Graph;
|
||||
registry: NodeRegistry;
|
||||
@ -38,10 +41,9 @@
|
||||
}: Props = $props();
|
||||
|
||||
export const keymap = createKeyMap([]);
|
||||
export const manager = new GraphManager(registry);
|
||||
|
||||
const state = new GraphState();
|
||||
setContext("graphState", state);
|
||||
export const manager = new GraphManager(registry);
|
||||
setContext("graphManager", manager);
|
||||
|
||||
$effect(() => {
|
||||
if (state.activeNodeId !== -1) {
|
||||
@ -75,4 +77,4 @@
|
||||
manager.load(graph);
|
||||
</script>
|
||||
|
||||
<GraphEl {manager} bind:showGrid bind:snapToGrid bind:showHelp />
|
||||
<GraphEl bind:showGrid bind:snapToGrid bind:showHelp />
|
||||
|
@ -18,7 +18,6 @@ let lastStyle = "";
|
||||
|
||||
function updateColors() {
|
||||
if (!("getComputedStyle" in globalThis)) return;
|
||||
console.log("updateColors")
|
||||
const style = getComputedStyle(document.body.parentElement!);
|
||||
let hash = "";
|
||||
for (const v of variables) {
|
||||
|
@ -8,12 +8,7 @@ export function getGraphState() {
|
||||
export class GraphState {
|
||||
|
||||
activeNodeId = $state(-1);
|
||||
|
||||
selectedNodes = $state(new Set<number>());
|
||||
clearSelection() {
|
||||
this.selectedNodes = new Set();
|
||||
}
|
||||
|
||||
activeSocket = $state<Socket | null>(null);
|
||||
hoveredSocket = $state<Socket | null>(null);
|
||||
possibleSockets = $state<Socket[]>([]);
|
||||
@ -21,6 +16,10 @@ export class GraphState {
|
||||
this.possibleSockets.map((s) => `${s.node.id}-${s.index}`),
|
||||
));
|
||||
|
||||
clearSelection() {
|
||||
this.selectedNodes = new Set();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { colors } from "./colors";
|
||||
|
@ -3,7 +3,6 @@
|
||||
import NodeHeader from "./NodeHeader.svelte";
|
||||
import NodeParameter from "./NodeParameter.svelte";
|
||||
import { getContext, onMount } from "svelte";
|
||||
import Page from "../../../routes/+page.svelte";
|
||||
export let isActive = false;
|
||||
export let isSelected = false;
|
||||
export let inView = true;
|
||||
|
@ -26,8 +26,8 @@
|
||||
const aspectRatio = 0.25;
|
||||
|
||||
const path = createNodePath({
|
||||
depth: 7,
|
||||
height: 40,
|
||||
depth: 5,
|
||||
height: 29,
|
||||
y: 50,
|
||||
cornerTop,
|
||||
rightBump,
|
||||
|
@ -59,9 +59,9 @@
|
||||
aspectRatio,
|
||||
});
|
||||
const pathDisabled = createNodePath({
|
||||
depth: 0,
|
||||
height: 15,
|
||||
y: 50,
|
||||
depth: 4.5,
|
||||
height: 14,
|
||||
y: 50.5,
|
||||
cornerBottom,
|
||||
leftBump,
|
||||
aspectRatio,
|
||||
@ -80,7 +80,7 @@
|
||||
class="wrapper"
|
||||
data-node-type={node.type}
|
||||
data-node-input={id}
|
||||
class:disabled={!graphState.possibleSocketIds.has(socketId)}
|
||||
class:disabled={!graphState?.possibleSocketIds.has(socketId)}
|
||||
>
|
||||
{#key id && graphId}
|
||||
<div class="content" class:disabled={$inputSockets?.has(socketId)}>
|
||||
|
24
app/src/lib/helpers/deepMerge.ts
Normal file
24
app/src/lib/helpers/deepMerge.ts
Normal file
@ -0,0 +1,24 @@
|
||||
export function isObject(item: Record<string, unknown> | unknown): item is Record<string, unknown> {
|
||||
return (typeof item === 'object' && !Array.isArray(item));
|
||||
}
|
||||
|
||||
type Object = Record<string, unknown>;
|
||||
|
||||
export function mergeDeep<T extends Object>(target: T, ...sources: Object[]): T {
|
||||
if (!sources.length) return target;
|
||||
const source = sources.shift();
|
||||
|
||||
if (source === undefined) return target;
|
||||
if (isObject(target) && isObject(source)) {
|
||||
for (const key in source) {
|
||||
if (isObject(source[key])) {
|
||||
if (!target[key]) Object.assign(target, { [key]: {} });
|
||||
if (isObject(target[key])) mergeDeep(target[key], source[key]);
|
||||
} else {
|
||||
Object.assign(target, { [key]: source[key] });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mergeDeep(target, ...sources);
|
||||
}
|
11
app/src/lib/helpers/localState.svelte.ts
Normal file
11
app/src/lib/helpers/localState.svelte.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export function localState<T>(key: string, defaultValue: T): T {
|
||||
const stored = localStorage.getItem(key)
|
||||
const state = $state(stored ? JSON.parse(stored) : defaultValue)
|
||||
$effect.root(() => {
|
||||
$effect(() => {
|
||||
const value = $state.snapshot(state);
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
});
|
||||
});
|
||||
return state;
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
import type { NodeDefinition } from "@nodes/types";
|
||||
|
||||
export let node: NodeDefinition;
|
||||
console.log(node);
|
||||
|
||||
let dragging = false;
|
||||
|
||||
|
@ -1,9 +1,8 @@
|
||||
<script lang="ts">
|
||||
import localStore from "$lib/helpers/localStore";
|
||||
import type { RemoteNodeRegistry } from "$lib/node-registry-client";
|
||||
import { writable } from "svelte/store";
|
||||
import BreadCrumbs from "./BreadCrumbs.svelte";
|
||||
import DraggableNode from "./DraggableNode.svelte";
|
||||
import type { RemoteNodeRegistry } from "@nodes/registry";
|
||||
|
||||
export let registry: RemoteNodeRegistry;
|
||||
|
||||
@ -58,7 +57,7 @@
|
||||
{:then collection}
|
||||
{#each collection.nodes as node}
|
||||
{#await registry.fetchNodeDefinition(node.id)}
|
||||
<div>Loading...</div>
|
||||
<div>Loading... {node.id}</div>
|
||||
{:then node}
|
||||
{#if node}
|
||||
<DraggableNode {node} />
|
||||
|
@ -25,44 +25,42 @@
|
||||
<div class="wrapper">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr on:click={() => ($open.runtime = !$open.runtime)}>
|
||||
<td>{$open.runtime ? "-" : "+"} runtime </td>
|
||||
<td>{humanizeDuration(runtime || 1000)}</td>
|
||||
</tr>
|
||||
{#if $open.runtime}
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<SmallGraph points={getPoints($store, "runtime")} />
|
||||
<tr on:click={() => ($open.runtime = !$open.runtime)}>
|
||||
<td>{$open.runtime ? "-" : "+"} runtime </td>
|
||||
<td>{humanizeDuration(runtime || 1000)}</td>
|
||||
</tr>
|
||||
{#if $open.runtime}
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<SmallGraph points={getPoints($store, "runtime")} />
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
|
||||
<tr on:click={() => ($open.fps = !$open.fps)}>
|
||||
<td>{$open.fps ? "-" : "+"} fps </td>
|
||||
<td>
|
||||
{Math.floor(fps[fps.length - 1])}fps
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{#if $open.fps}
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<SmallGraph points={fps} />
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
|
||||
<tr on:click={() => ($open.fps = !$open.fps)}>
|
||||
<td>{$open.fps ? "-" : "+"} fps </td>
|
||||
<td>
|
||||
{#if fps[fps.length - 1] > 5}
|
||||
{Math.floor(1000 / fps[fps.length - 1])}fps
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{#if $open.fps}
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<SmallGraph points={fps} />
|
||||
</td>
|
||||
<td>vertices </td>
|
||||
<td>{humanizeNumber(vertices || 0)}</td>
|
||||
</tr>
|
||||
{/if}
|
||||
|
||||
<tr>
|
||||
<td>vertices </td>
|
||||
<td>{humanizeNumber(vertices || 0)}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>faces </td>
|
||||
<td>{humanizeNumber(faces || 0)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tr>
|
||||
<td>faces </td>
|
||||
<td>{humanizeNumber(faces || 0)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@ -71,6 +69,7 @@
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 2;
|
||||
background: var(--layer-0);
|
||||
border: solid thin var(--outline);
|
||||
border-collapse: collapse;
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { T, useThrelte } from "@threlte/core";
|
||||
import { T, useTask, useThrelte } from "@threlte/core";
|
||||
import { MeshLineGeometry, MeshLineMaterial, Text } from "@threlte/extras";
|
||||
import {
|
||||
type Group,
|
||||
@ -8,23 +8,29 @@
|
||||
type Vector3Tuple,
|
||||
Box3,
|
||||
Mesh,
|
||||
MeshMatcapMaterial,
|
||||
MeshBasicMaterial,
|
||||
} from "three";
|
||||
import { AppSettings } from "../settings/app-settings";
|
||||
import Camera from "./Camera.svelte";
|
||||
|
||||
const threlte = useThrelte();
|
||||
const { renderStage, invalidate: _invalidate } = useThrelte();
|
||||
|
||||
export let fps: number[] = [];
|
||||
let renderer = threlte.renderer;
|
||||
let rendererRender = renderer.render;
|
||||
renderer.render = function (scene, camera) {
|
||||
const a = performance.now();
|
||||
rendererRender.call(renderer, scene, camera);
|
||||
fps.push(performance.now() - a);
|
||||
fps = fps.slice(-100);
|
||||
};
|
||||
// let renderer = threlte.renderer;
|
||||
// let rendererRender = renderer.render;
|
||||
// renderer.render = function (scene, camera) {
|
||||
// const a = performance.now();
|
||||
// rendererRender.call(renderer, scene, camera);
|
||||
// fps.push(performance.now() - a);
|
||||
// fps = fps.slice(-100);
|
||||
// };
|
||||
useTask(
|
||||
(delta) => {
|
||||
fps.push(1 / delta);
|
||||
fps = fps.slice(-100);
|
||||
},
|
||||
{ stage: renderStage, autoInvalidate: false },
|
||||
);
|
||||
|
||||
export const invalidate = function () {
|
||||
if (scene) {
|
||||
@ -44,7 +50,7 @@
|
||||
.max(new Vector3(-4, -4, -4))
|
||||
.min(new Vector3(4, 4, 4));
|
||||
}
|
||||
threlte.invalidate();
|
||||
_invalidate();
|
||||
};
|
||||
|
||||
let geometries: BufferGeometry[] = [];
|
||||
@ -68,7 +74,7 @@
|
||||
child.material.wireframe = $AppSettings.wireframe;
|
||||
}
|
||||
});
|
||||
threlte.invalidate();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
function getPosition(geo: BufferGeometry, i: number) {
|
||||
|
4
app/src/lib/runtime/index.ts
Normal file
4
app/src/lib/runtime/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./runtime-executor"
|
||||
export * from "./runtime-executor-cache"
|
||||
export * from "./worker-runtime-executor"
|
||||
|
18
app/src/lib/runtime/remote-runtime-executor.ts
Normal file
18
app/src/lib/runtime/remote-runtime-executor.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import type { Graph, RuntimeExecutor } from "@nodes/types";
|
||||
|
||||
export class RemoteRuntimeExecutor implements RuntimeExecutor {
|
||||
|
||||
constructor(private url: string) { }
|
||||
|
||||
async execute(graph: Graph, settings: Record<string, any>): Promise<Int32Array> {
|
||||
|
||||
const res = await fetch(this.url, { method: "POST", body: JSON.stringify({ graph, settings }) });
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to execute graph`);
|
||||
}
|
||||
|
||||
return new Int32Array(await res.arrayBuffer());
|
||||
|
||||
}
|
||||
}
|
19
app/src/lib/runtime/runtime-executor-cache.ts
Normal file
19
app/src/lib/runtime/runtime-executor-cache.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { type SyncCache } from "@nodes/types";
|
||||
|
||||
export class MemoryRuntimeCache implements SyncCache {
|
||||
|
||||
private cache: [string, unknown][] = [];
|
||||
size = 50;
|
||||
|
||||
get<T>(key: string): T | undefined {
|
||||
return this.cache.find(([k]) => k === key)?.[1] as T;
|
||||
}
|
||||
set<T>(key: string, value: T): void {
|
||||
this.cache.push([key, value]);
|
||||
this.cache = this.cache.slice(-this.size);
|
||||
}
|
||||
clear(): void {
|
||||
this.cache = [];
|
||||
}
|
||||
|
||||
}
|
263
app/src/lib/runtime/runtime-executor.ts
Normal file
263
app/src/lib/runtime/runtime-executor.ts
Normal file
@ -0,0 +1,263 @@
|
||||
import type { Graph, NodeRegistry, NodeDefinition, RuntimeExecutor, NodeInput } from "@nodes/types";
|
||||
import { concatEncodedArrays, encodeFloat, fastHashArrayBuffer, createLogger, type PerformanceStore } from "@nodes/utils"
|
||||
import type { SyncCache } from "@nodes/types";
|
||||
|
||||
const log = createLogger("runtime-executor");
|
||||
log.mute()
|
||||
|
||||
function getValue(input: NodeInput, value?: unknown) {
|
||||
if (value === undefined && "value" in input) {
|
||||
value = input.value
|
||||
}
|
||||
if (input.type === "float") {
|
||||
return encodeFloat(value as number);
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (input.type === "vec3") {
|
||||
return [0, value.length + 1, ...value.map(v => encodeFloat(v)), 1, 1] as number[];
|
||||
}
|
||||
return [0, value.length + 1, ...value, 1, 1] as number[];
|
||||
}
|
||||
|
||||
if (typeof value === "boolean") {
|
||||
return value ? 1 : 0;
|
||||
}
|
||||
|
||||
if (typeof value === "number") {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value instanceof Int32Array) {
|
||||
return value;
|
||||
}
|
||||
|
||||
throw new Error(`Unknown input type ${input.type}`);
|
||||
}
|
||||
|
||||
export class MemoryRuntimeExecutor implements RuntimeExecutor {
|
||||
|
||||
private definitionMap: Map<string, NodeDefinition> = new Map();
|
||||
|
||||
private randomSeed = Math.floor(Math.random() * 100000000);
|
||||
|
||||
perf?: PerformanceStore;
|
||||
|
||||
constructor(private registry: NodeRegistry, private cache?: SyncCache<Int32Array>) { }
|
||||
|
||||
private async getNodeDefinitions(graph: Graph) {
|
||||
|
||||
if (this.registry.status !== "ready") {
|
||||
throw new Error("Node registry is not ready");
|
||||
}
|
||||
|
||||
await this.registry.load(graph.nodes.map(node => node.type));
|
||||
|
||||
const typeMap = new Map<string, NodeDefinition>();
|
||||
for (const node of graph.nodes) {
|
||||
if (!typeMap.has(node.type)) {
|
||||
const type = this.registry.getNode(node.type);
|
||||
if (type) {
|
||||
typeMap.set(node.type, type);
|
||||
}
|
||||
}
|
||||
}
|
||||
return typeMap;
|
||||
}
|
||||
|
||||
private async addMetaData(graph: Graph) {
|
||||
|
||||
// First, lets check if all nodes have a definition
|
||||
this.definitionMap = await this.getNodeDefinitions(graph);
|
||||
|
||||
const outputNode = graph.nodes.find(node => node.type.endsWith("/output"));
|
||||
if (!outputNode) {
|
||||
throw new Error("No output node found");
|
||||
}
|
||||
outputNode.tmp = outputNode.tmp || {};
|
||||
outputNode.tmp.depth = 0;
|
||||
|
||||
const nodeMap = new Map(graph.nodes.map(node => [node.id, node]));
|
||||
|
||||
// loop through all edges and assign the parent and child nodes to each node
|
||||
for (const edge of graph.edges) {
|
||||
const [parentId, _parentOutput, childId, childInput] = edge;
|
||||
const parent = nodeMap.get(parentId);
|
||||
const child = nodeMap.get(childId);
|
||||
if (parent && child) {
|
||||
parent.tmp = parent.tmp || {};
|
||||
parent.tmp.children = parent.tmp.children || [];
|
||||
parent.tmp.children.push(child);
|
||||
child.tmp = child.tmp || {};
|
||||
child.tmp.parents = child.tmp.parents || [];
|
||||
child.tmp.parents.push(parent);
|
||||
child.tmp.inputNodes = child.tmp.inputNodes || {};
|
||||
child.tmp.inputNodes[childInput] = parent;
|
||||
}
|
||||
}
|
||||
|
||||
const nodes = []
|
||||
|
||||
// loop through all the nodes and assign each nodes its depth
|
||||
const stack = [outputNode];
|
||||
while (stack.length) {
|
||||
const node = stack.pop();
|
||||
if (!node) continue;
|
||||
node.tmp = node.tmp || {};
|
||||
if (node?.tmp?.depth === undefined) {
|
||||
node.tmp.depth = 0;
|
||||
}
|
||||
if (node?.tmp?.parents !== undefined) {
|
||||
for (const parent of node.tmp.parents) {
|
||||
parent.tmp = parent.tmp || {};
|
||||
if (parent.tmp?.depth === undefined) {
|
||||
parent.tmp.depth = node.tmp.depth + 1;
|
||||
stack.push(parent);
|
||||
} else {
|
||||
parent.tmp.depth = Math.max(parent.tmp.depth, node.tmp.depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
nodes.push(node);
|
||||
}
|
||||
|
||||
return [outputNode, nodes] as const;
|
||||
}
|
||||
|
||||
async execute(graph: Graph, settings: Record<string, unknown>) {
|
||||
|
||||
this.perf?.addPoint("runtime");
|
||||
|
||||
let a = performance.now();
|
||||
|
||||
// Then we add some metadata to the graph
|
||||
const [outputNode, nodes] = await this.addMetaData(graph);
|
||||
let b = performance.now();
|
||||
|
||||
this.perf?.addPoint("collect-metadata", b - a);
|
||||
|
||||
/*
|
||||
* Here we sort the nodes into buckets, which we then execute one by one
|
||||
* +-b2-+-b1-+---b0---+
|
||||
* | | | |
|
||||
* | n3 | n2 | Output |
|
||||
* | n6 | n4 | Level |
|
||||
* | | n5 | |
|
||||
* | | | |
|
||||
* +----+----+--------+
|
||||
*/
|
||||
|
||||
// we execute the nodes from the bottom up
|
||||
const sortedNodes = nodes.sort((a, b) => (b.tmp?.depth || 0) - (a.tmp?.depth || 0));
|
||||
|
||||
// here we store the intermediate results of the nodes
|
||||
const results: Record<string, Int32Array> = {};
|
||||
|
||||
for (const node of sortedNodes) {
|
||||
|
||||
const node_type = this.definitionMap.get(node.type)!;
|
||||
|
||||
if (!node_type || !node.tmp || !node_type.execute) {
|
||||
log.warn(`Node ${node.id} has no definition`);
|
||||
continue;
|
||||
};
|
||||
|
||||
a = performance.now();
|
||||
|
||||
// Collect the inputs for the node
|
||||
const inputs = Object.entries(node_type.inputs || {}).map(([key, input]) => {
|
||||
|
||||
if (input.type === "seed") {
|
||||
if (settings["randomSeed"] === true) {
|
||||
return Math.floor(Math.random() * 100000000)
|
||||
} else {
|
||||
return this.randomSeed
|
||||
}
|
||||
}
|
||||
|
||||
// If the input is linked to a setting, we use that value
|
||||
if (input.setting) {
|
||||
return getValue(input, settings[input.setting]);
|
||||
}
|
||||
|
||||
// check if the input is connected to another node
|
||||
const inputNode = node.tmp?.inputNodes?.[key];
|
||||
if (inputNode) {
|
||||
if (results[inputNode.id] === undefined) {
|
||||
throw new Error(`Node ${node.type} is missing input from node ${inputNode.type}`);
|
||||
}
|
||||
return results[inputNode.id];
|
||||
}
|
||||
|
||||
// If the value is stored in the node itself, we use that value
|
||||
if (node.props?.[key] !== undefined) {
|
||||
return getValue(input, node.props[key]);
|
||||
}
|
||||
|
||||
return getValue(input);
|
||||
});
|
||||
b = performance.now();
|
||||
|
||||
this.perf?.addPoint("collected-inputs", b - a);
|
||||
|
||||
try {
|
||||
|
||||
a = performance.now();
|
||||
const encoded_inputs = concatEncodedArrays(inputs);
|
||||
b = performance.now();
|
||||
this.perf?.addPoint("encoded-inputs", b - a);
|
||||
|
||||
a = performance.now();
|
||||
let inputHash = `node-${node.id}-${fastHashArrayBuffer(encoded_inputs)}`;
|
||||
b = performance.now();
|
||||
this.perf?.addPoint("hash-inputs", b - a);
|
||||
|
||||
let cachedValue = this.cache?.get(inputHash);
|
||||
if (cachedValue !== undefined) {
|
||||
log.log(`Using cached value for ${node_type.id || node.id}`);
|
||||
this.perf?.addPoint("cache-hit", 1);
|
||||
results[node.id] = cachedValue as Int32Array;
|
||||
continue;
|
||||
}
|
||||
this.perf?.addPoint("cache-hit", 0);
|
||||
|
||||
log.group(`executing ${node_type.id || node.id}`);
|
||||
log.log(`Inputs:`, inputs);
|
||||
a = performance.now();
|
||||
results[node.id] = node_type.execute(encoded_inputs);
|
||||
b = performance.now();
|
||||
|
||||
if (this.cache) {
|
||||
this.cache.set(inputHash, results[node.id]);
|
||||
}
|
||||
|
||||
this.perf?.addPoint("node/" + node_type.id, b - a);
|
||||
log.log("Result:", results[node.id]);
|
||||
log.groupEnd();
|
||||
|
||||
} catch (e) {
|
||||
log.groupEnd();
|
||||
log.error(`Error executing node ${node_type.id || node.id}`, e);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// return the result of the parent of the output node
|
||||
const res = results[outputNode.id];
|
||||
|
||||
if (this.cache) {
|
||||
this.cache.size = sortedNodes.length * 2;
|
||||
}
|
||||
|
||||
this.perf?.endPoint("runtime");
|
||||
|
||||
return res as unknown as Int32Array;
|
||||
|
||||
}
|
||||
|
||||
getPerformanceData() {
|
||||
return this.perf?.get();
|
||||
}
|
||||
|
||||
}
|
26
app/src/lib/runtime/worker-runtime-executor-backend.ts
Normal file
26
app/src/lib/runtime/worker-runtime-executor-backend.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { MemoryRuntimeExecutor } from "./runtime-executor";
|
||||
import { RemoteNodeRegistry, IndexDBCache } from "@nodes/registry";
|
||||
import type { Graph } from "@nodes/types";
|
||||
import { createPerformanceStore } from "@nodes/utils";
|
||||
import { MemoryRuntimeCache } from "./runtime-executor-cache";
|
||||
|
||||
const cache = new MemoryRuntimeCache();
|
||||
const indexDbCache = new IndexDBCache("node-registry");
|
||||
const nodeRegistry = new RemoteNodeRegistry("");
|
||||
nodeRegistry.cache = indexDbCache;
|
||||
const executor = new MemoryRuntimeExecutor(nodeRegistry, cache);
|
||||
|
||||
const performanceStore = createPerformanceStore();
|
||||
executor.perf = performanceStore;
|
||||
|
||||
export async function executeGraph(graph: Graph, settings: Record<string, unknown>): Promise<Int32Array> {
|
||||
await nodeRegistry.load(graph.nodes.map((n) => n.type));
|
||||
performanceStore.startRun();
|
||||
let res = await executor.execute(graph, settings);
|
||||
performanceStore.stopRun();
|
||||
return res;
|
||||
}
|
||||
|
||||
export function getPerformanceData() {
|
||||
return performanceStore.get();
|
||||
}
|
18
app/src/lib/runtime/worker-runtime-executor.ts
Normal file
18
app/src/lib/runtime/worker-runtime-executor.ts
Normal file
@ -0,0 +1,18 @@
|
||||
/// <reference types="vite-plugin-comlink/client" />
|
||||
import type { Graph, RuntimeExecutor } from "@nodes/types";
|
||||
|
||||
|
||||
export class WorkerRuntimeExecutor implements RuntimeExecutor {
|
||||
private worker = new ComlinkWorker<typeof import('./worker-runtime-executor-backend.ts')>(new URL(`./worker-runtime-executor-backend.ts`, import.meta.url));
|
||||
|
||||
constructor() {
|
||||
console.log(import.meta.url)
|
||||
}
|
||||
async execute(graph: Graph, settings: Record<string, unknown>) {
|
||||
return this.worker.executeGraph(graph, settings);
|
||||
}
|
||||
async getPerformanceData() {
|
||||
return this.worker.getPerformanceData();
|
||||
}
|
||||
}
|
||||
|
@ -94,6 +94,7 @@
|
||||
|
||||
.content {
|
||||
background: var(--layer-1);
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
max-height: 100vh;
|
||||
overflow-y: auto;
|
||||
|
153
app/src/lib/settings/app-settings.svelte.ts
Normal file
153
app/src/lib/settings/app-settings.svelte.ts
Normal file
@ -0,0 +1,153 @@
|
||||
import { localState } from "$lib/helpers/localState.svelte";
|
||||
import type { NodeInput } from "@nodes/types";
|
||||
|
||||
const themes = ["dark", "light", "catppuccin", "solarized", "high-contrast", "nord", "dracula"];
|
||||
|
||||
export const AppSettingTypes = {
|
||||
theme: {
|
||||
type: "select",
|
||||
options: themes,
|
||||
label: "Theme",
|
||||
value: themes[0],
|
||||
},
|
||||
showGrid: {
|
||||
type: "boolean",
|
||||
label: "Show Grid",
|
||||
value: true,
|
||||
},
|
||||
centerCamera: {
|
||||
type: "boolean",
|
||||
label: "Center Camera",
|
||||
value: true
|
||||
},
|
||||
nodeInterface: {
|
||||
title: "Node Interface",
|
||||
showNodeGrid: {
|
||||
type: "boolean",
|
||||
label: "Show Grid",
|
||||
value: true
|
||||
},
|
||||
snapToGrid: {
|
||||
type: "boolean",
|
||||
label: "Snap to Grid",
|
||||
value: true
|
||||
},
|
||||
showHelp: {
|
||||
type: "boolean",
|
||||
label: "Show Help",
|
||||
value: false
|
||||
}
|
||||
},
|
||||
debug: {
|
||||
wireframe: {
|
||||
type: "boolean",
|
||||
label: "Wireframe",
|
||||
value: false,
|
||||
},
|
||||
useWorker: {
|
||||
type: "boolean",
|
||||
label: "Execute runtime in worker",
|
||||
value: true,
|
||||
},
|
||||
showIndices: {
|
||||
type: "boolean",
|
||||
label: "Show Indices",
|
||||
value: false,
|
||||
},
|
||||
showPerformancePanel: {
|
||||
type: "boolean",
|
||||
label: "Show Performance Panel",
|
||||
value: false,
|
||||
},
|
||||
showBenchmarkPanel: {
|
||||
type: "boolean",
|
||||
label: "Show Benchmark Panel",
|
||||
value: false,
|
||||
},
|
||||
showVertices: {
|
||||
type: "boolean",
|
||||
label: "Show Vertices",
|
||||
value: false,
|
||||
},
|
||||
showStemLines: {
|
||||
type: "boolean",
|
||||
label: "Show Stem Lines",
|
||||
value: false,
|
||||
},
|
||||
stressTest: {
|
||||
title: "Stress Test",
|
||||
amount: {
|
||||
type: "integer",
|
||||
min: 2,
|
||||
max: 15
|
||||
},
|
||||
loadGrid: {
|
||||
type: "button",
|
||||
label: "Load Grid"
|
||||
},
|
||||
loadTree: {
|
||||
type: "button",
|
||||
label: "Load Tree"
|
||||
},
|
||||
lottaFaces: {
|
||||
type: "button",
|
||||
label: "Load 'lots of faces'"
|
||||
},
|
||||
lottaNodes: {
|
||||
type: "button",
|
||||
label: "Load 'lots of nodes'"
|
||||
},
|
||||
lottaNodesAndFaces: {
|
||||
type: "button",
|
||||
label: "Load 'lots of nodes and faces'"
|
||||
}
|
||||
},
|
||||
}
|
||||
} as const
|
||||
|
||||
type IsInputDefinition<T> = T extends NodeInput ? T : never;
|
||||
type HasTitle = { title: string };
|
||||
type ExtractSettingsValues<T> = {
|
||||
[K in keyof T]: T[K] extends HasTitle
|
||||
? ExtractSettingsValues<Omit<T[K], 'title'>>
|
||||
: T[K] extends IsInputDefinition<T[K]>
|
||||
? T[K] extends { value: any }
|
||||
? T[K]['value']
|
||||
: never
|
||||
: T[K] extends Record<string, any>
|
||||
? ExtractSettingsValues<T[K]>
|
||||
: never;
|
||||
};
|
||||
|
||||
function settingsToStore<T>(settings: T): ExtractSettingsValues<T> {
|
||||
const result = {} as any;
|
||||
for (const key in settings) {
|
||||
const value = settings[key];
|
||||
if (value && typeof value === 'object') {
|
||||
if ('value' in value) {
|
||||
result[key] = value.value;
|
||||
} else {
|
||||
result[key] = settingsToStore(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export const appSettings = localState("app-settings", settingsToStore(AppSettingTypes));
|
||||
|
||||
$effect.root(() => {
|
||||
$effect(() => {
|
||||
const { theme } = $state.snapshot(appSettings);
|
||||
const classes = document.body.parentElement?.classList;
|
||||
const newClassName = `theme-${theme}`;
|
||||
if (classes) {
|
||||
for (const className of classes) {
|
||||
if (className.startsWith("theme-") && className !== newClassName) {
|
||||
classes.remove(className);
|
||||
}
|
||||
}
|
||||
}
|
||||
document.body?.parentElement?.classList.add(newClassName);
|
||||
});
|
||||
});
|
@ -11,7 +11,7 @@
|
||||
function filterInputs(inputs: Record<string, NodeInput>) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(inputs)
|
||||
.filter(([key, value]) => {
|
||||
.filter(([_key, value]) => {
|
||||
return value.hidden === true;
|
||||
})
|
||||
.map(([key, value]) => {
|
||||
|
@ -3,6 +3,7 @@
|
||||
import type { OBJExporter } from "three/addons/exporters/OBJExporter.js";
|
||||
import type { GLTFExporter } from "three/addons/exporters/GLTFExporter.js";
|
||||
import FileSaver from "file-saver";
|
||||
import { appSettings } from "../app-settings.svelte";
|
||||
|
||||
// Download
|
||||
const download = (
|
||||
@ -51,6 +52,8 @@
|
||||
// download .obj file
|
||||
download(result, "plant", "text/plain", "obj");
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<div class="p-2">
|
||||
|
@ -1,70 +1,100 @@
|
||||
<script lang="ts">
|
||||
import localStore from "$lib/helpers/localStore";
|
||||
import NestedSettings from "./NestedSettings.svelte";
|
||||
import {localState} from "$lib/helpers/localState.svelte";
|
||||
import type { NodeInput } from "@nodes/types";
|
||||
import Input from "@nodes/ui";
|
||||
import type { Writable } from "svelte/store";
|
||||
|
||||
type Button = { type: "button"; label?: string; callback: () => void };
|
||||
type Button = { type: "button"; label?: string };
|
||||
|
||||
type InputType = NodeInput | Button;
|
||||
|
||||
interface Nested {
|
||||
[key: string]: (Nested & { __title?: string }) | InputType;
|
||||
[key: string]: (Nested & { title?: string }) | InputType;
|
||||
}
|
||||
|
||||
export let id: string;
|
||||
type Props = {
|
||||
id: string;
|
||||
key?: string;
|
||||
value: Record<string, unknown> | string | number | boolean;
|
||||
type: Nested;
|
||||
depth?: number;
|
||||
};
|
||||
|
||||
$: expandedDetails = localStore<Record<string, boolean>>(
|
||||
`nodes.settings.expanded.${id}`,
|
||||
{},
|
||||
);
|
||||
let { id, key = "", value = $bindable(), type, depth = 0 }: Props = $props();
|
||||
|
||||
export let settings: Nested;
|
||||
export let store: Writable<Record<string, any>>;
|
||||
export let depth = 0;
|
||||
|
||||
const keys = Object.keys(settings).filter((key) => key !== "__title");
|
||||
function isNodeInput(v: InputType | Nested): v is InputType {
|
||||
return v && "type" in v;
|
||||
}
|
||||
|
||||
let internalValue = $state(Array.isArray(type?.[key]?.options) ? type[key]?.options?.indexOf(value?.[key]) : value?.[key]);
|
||||
|
||||
let openSections = localState("open-details", {});
|
||||
let open = $state(openSections[id]);
|
||||
if(depth > 0 && !isNodeInput(type[key])){
|
||||
$effect(() => {
|
||||
if(open !== undefined){}
|
||||
openSections[id] = open;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
$effect(() => {
|
||||
if(key === "" || internalValue === undefined) return;
|
||||
if(isNodeInput(type[key]) && Array.isArray(type[key]?.options) && typeof internalValue === "number"){
|
||||
value[key] = type[key].options?.[internalValue];
|
||||
}else{
|
||||
value[key] = internalValue;
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if $store}
|
||||
{#each keys as key}
|
||||
{@const value = settings[key]}
|
||||
<div class="wrapper" class:first-level={depth === 0}>
|
||||
{#if value !== undefined && isNodeInput(value)}
|
||||
<div
|
||||
class="input input-{settings[key].type}"
|
||||
data-node-type={value?.__node_type || null}
|
||||
data-node-input={value?.__node_input || null}
|
||||
>
|
||||
{#if value.type === "button"}
|
||||
<button on:click={() => value?.callback?.()}
|
||||
>{value.label || key}</button
|
||||
>
|
||||
{:else if "setting" in value && value.setting !== undefined}
|
||||
<label for={key}>{settings[key].label || key}</label>
|
||||
<Input id={key} input={value} bind:value={$store[value?.setting]} />
|
||||
{:else}
|
||||
<label for={key}>{settings[key].label || key}</label>
|
||||
<Input id={key} input={value} bind:value={$store[key]} />
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
{#if depth > 0}
|
||||
<hr />
|
||||
{/if}
|
||||
{#if key && isNodeInput(type?.[key]) }
|
||||
<div class="input input-{type[key].type}">
|
||||
{#if type[key].type === "button"}
|
||||
<button onclick={() => console.log(type[key])}>
|
||||
{type[key].label || key}
|
||||
</button>
|
||||
{:else}
|
||||
<label for={id}>{type[key].label || key}</label>
|
||||
<Input id={id} input={type[key]} bind:value={internalValue} />
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
{#if depth === 0}
|
||||
{#each Object.keys(type).filter((key) => key !== "title") as childKey}
|
||||
<NestedSettings
|
||||
id={`${id}.${childKey}`}
|
||||
key={childKey}
|
||||
value={value as Record<string, unknown>}
|
||||
type={type as Nested}
|
||||
depth={depth + 1}
|
||||
/>
|
||||
{/each}
|
||||
{#if depth > 0}
|
||||
<hr />
|
||||
{/if}
|
||||
{:else if key && type?.[key]}
|
||||
{#if depth > 0}
|
||||
<hr />
|
||||
{/if}
|
||||
<details bind:open>
|
||||
<summary>{type[key]?.title||key}</summary>
|
||||
<div class="content">
|
||||
{#each Object.keys(type[key]).filter((key) => key !== "title") as childKey}
|
||||
<NestedSettings
|
||||
id={`${id}.${childKey}`}
|
||||
key={childKey}
|
||||
value={value[key] as Record<string, unknown>}
|
||||
type={type[key] as Nested}
|
||||
depth={depth + 1}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
|
||||
{/if}
|
||||
|
||||
<details bind:open={$expandedDetails[key]}>
|
||||
<summary>{settings[key]?.__title || key}</summary>
|
||||
<div class="content">
|
||||
<svelte:self settings={settings[key]} {store} depth={depth + 1} />
|
||||
</div>
|
||||
</details>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
|
@ -6,11 +6,11 @@
|
||||
import Viewer from "$lib/result-viewer/Viewer.svelte";
|
||||
import Settings from "$lib/settings/Settings.svelte";
|
||||
import { AppSettingTypes, AppSettings } from "$lib/settings/app-settings";
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
import { appSettings as _appSettings, AppSettingTypes as _AppSettingTypes} from "$lib/settings/app-settings.svelte";
|
||||
import { writable } from "svelte/store";
|
||||
import Keymap from "$lib/settings/panels/Keymap.svelte";
|
||||
import { createKeyMap } from "$lib/helpers/createKeyMap";
|
||||
import NodeStore from "$lib/node-store/NodeStore.svelte";
|
||||
import type { GraphManager } from "$lib/graph-interface/graph-manager";
|
||||
import ActiveNodeSettings from "$lib/settings/panels/ActiveNodeSettings.svelte";
|
||||
import PerformanceViewer from "$lib/performance/PerformanceViewer.svelte";
|
||||
import Panel from "$lib/settings/Panel.svelte";
|
||||
@ -22,12 +22,11 @@
|
||||
MemoryRuntimeCache,
|
||||
WorkerRuntimeExecutor,
|
||||
MemoryRuntimeExecutor,
|
||||
} from "@nodes/runtime";
|
||||
} from "$lib/runtime";
|
||||
import { IndexDBCache, RemoteNodeRegistry } from "@nodes/registry";
|
||||
import { createPerformanceStore } from "@nodes/utils";
|
||||
import BenchmarkPanel from "$lib/settings/panels/BenchmarkPanel.svelte";
|
||||
import { debounceAsyncFunction } from "$lib/helpers";
|
||||
import type { Component } from "svelte";
|
||||
|
||||
let performanceStore = createPerformanceStore();
|
||||
|
||||
@ -160,8 +159,8 @@
|
||||
<Panel id="general" title="General" icon="i-tabler-settings">
|
||||
<NestedSettings
|
||||
id="general"
|
||||
store={AppSettings}
|
||||
settings={AppSettingTypes}
|
||||
value={_appSettings}
|
||||
type={_AppSettingTypes}
|
||||
/>
|
||||
</Panel>
|
||||
<Panel
|
||||
|
Reference in New Issue
Block a user