feat: refactor how frontend was structured
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 1m16s

This commit is contained in:
max_richter 2024-04-25 01:53:20 +02:00
parent f51f61df17
commit c28ef550a9
18 changed files with 264 additions and 225 deletions

View File

@ -27,6 +27,8 @@
export let showGrid = false;
export let snapToGrid = false;
export let settingTypes = {};
const updateSettings = debounce((s) => {
manager.setSettings(s);
}, 200);
@ -36,7 +38,8 @@
}
manager.on("settings", (settings) => {
dispatch("settings", settings);
settingTypes = settings.types;
$settings = settings.values;
});
manager.on("result", (result) => {

View File

@ -3,6 +3,8 @@ import { createWasmWrapper } from "@nodes/utils";
import { createLogger } from "./helpers";
const log = createLogger("node-registry");
log.mute();
export class RemoteNodeRegistry implements NodeRegistry {
status: "loading" | "ready" | "error" = "loading";
@ -10,9 +12,6 @@ export class RemoteNodeRegistry implements NodeRegistry {
constructor(private url: string) { }
async loadNode(id: `${string}/${string}/${string}`) {
}
async fetchUsers() {
const response = await fetch(`${this.url}/nodes/users.json`);
if (!response.ok) {
@ -58,6 +57,10 @@ export class RemoteNodeRegistry implements NodeRegistry {
const nodes = await Promise.all(nodeIds.map(async id => {
if (this.nodes.has(id)) {
return this.nodes.get(id);
}
const wasmResponse = await this.fetchNode(id);
const wrapper = createWasmWrapper(wasmResponse);

View File

@ -4,7 +4,7 @@
import BreadCrumbs from "./BreadCrumbs.svelte";
import DraggableNode from "./DraggableNode.svelte";
export let nodeRegistry: RemoteNodeRegistry;
export let registry: RemoteNodeRegistry;
const activeId = localStore<
`${string}` | `${string}/${string}` | `${string}/${string}/${string}`
@ -20,7 +20,7 @@
<div class="header">
<h3>Users</h3>
</div>
{#await nodeRegistry.fetchUsers()}
{#await registry.fetchUsers()}
<div>Loading...</div>
{:then users}
{#each users as user}
@ -34,7 +34,7 @@
<div>{error.message}</div>
{/await}
{:else if !activeCollection}
{#await nodeRegistry.fetchUser(activeUser)}
{#await registry.fetchUser(activeUser)}
<div>Loading...</div>
{:then user}
<div class="header">
@ -68,11 +68,11 @@
></button>
<h3>Nodes</h3>
</div>
{#await nodeRegistry.fetchCollection(`${activeUser}/${activeCollection}`)}
{#await registry.fetchCollection(`${activeUser}/${activeCollection}`)}
<div>Loading...</div>
{:then collection}
{#each collection.nodes as node}
{#await nodeRegistry.fetchNodeDefinition(node.id)}
{#await registry.fetchNodeDefinition(node.id)}
<div>Loading...</div>
{:then node}
<DraggableNode {node} />

View File

@ -1,14 +1,14 @@
<script lang="ts">
import type { PerformanceStore } from ".";
import type { PerformanceData } from ".";
export let store: PerformanceStore;
export let data: PerformanceData;
function getPerformanceData() {
return Object.entries($store.total).sort((a, b) => b[1] - a[1]);
return Object.entries(data.total).sort((a, b) => b[1] - a[1]);
}
</script>
{#if $store.runs.length !== 0}
{#if data.runs.length !== 0}
{#each getPerformanceData() as [key, value]}
<p>{key}: {Math.floor(value * 100) / 100}ms</p>
{/each}

View File

@ -1,6 +1,6 @@
import { readable, type Readable } from "svelte/store";
type PerformanceData = {
export type PerformanceData = {
total: Record<string, number>;
runs: Record<string, number[]>[];
}
@ -8,6 +8,7 @@ export interface PerformanceStore extends Readable<PerformanceData> {
startRun(): void;
stopRun(): void;
addPoint(name: string, value?: number): void;
get: () => PerformanceData;
}
export function createPerformanceStore(): PerformanceStore {
@ -41,7 +42,7 @@ export function createPerformanceStore(): PerformanceStore {
data.runs.push(currentRun);
currentRun = undefined;
set(data);
if (set) set(data);
}
}
@ -51,11 +52,16 @@ export function createPerformanceStore(): PerformanceStore {
currentRun[name].push(value);
}
function get() {
return data;
}
return {
subscribe,
startRun,
stopRun,
addPoint,
get
}
}

View File

@ -1,7 +1,6 @@
<script lang="ts">
import { Canvas } from "@threlte/core";
import Scene from "./Scene.svelte";
import { Inspector } from "three-inspect";
import {
BufferGeometry,
Float32BufferAttribute,
@ -13,9 +12,10 @@
export let result: Int32Array;
export let camera: PerspectiveCamera;
export let controls: OrbitControls;
export let center: Vector3;
let camera: PerspectiveCamera;
let controls: OrbitControls;
let center: Vector3;
export let centerCamera: boolean = true;
let geometries: BufferGeometry[] = [];
let lines: Vector3[][] = [];

View File

@ -0,0 +1,39 @@
<script lang="ts">
import { getContext } from "svelte";
import type { Readable } from "svelte/store";
export let id: string;
export let icon: string = "";
export let title = "";
const registerPanel =
getContext<(id: string, icon: string) => Readable<boolean>>(
"registerPanel",
);
let visible = registerPanel(id, icon);
</script>
{#if $visible}
<div class="wrapper">
{#if title}
<header>
<h3>{title}</h3>
</header>
{/if}
<slot />
</div>
{/if}
<style>
header {
border-bottom: solid thin var(--outline);
height: 69px;
display: flex;
align-items: center;
padding-left: 1em;
}
h3 {
margin: 0px;
}
</style>

View File

@ -1,33 +1,34 @@
<script lang="ts">
import type { NodeInput } from "@nodes/types";
import type { Writable } from "svelte/store";
import localStore from "$lib/helpers/localStore";
import type { SvelteComponent } from "svelte";
import NestedSettings from "./NestedSettings.svelte";
import { setContext } from "svelte";
import { derived } from "svelte/store";
export let panels: Record<
let panels: Record<
string,
{
icon: string;
id: string;
hidden?: boolean;
props?: Record<string, unknown>;
component?: typeof SvelteComponent<{}, {}, {}>;
definition: Record<string, NodeInput>;
settings: Writable<Record<string, unknown>>;
}
>;
> = {};
const activePanel = localStore<keyof typeof panels | false>(
"nodes.settings.activePanel",
false,
);
$: keys = panels
? (Object.keys(panels) as unknown as (keyof typeof panels)[]).filter(
(key) => !!panels[key]?.id && panels[key]?.hidden !== true,
(key) => !!panels[key]?.id,
)
: [];
setContext("registerPanel", (id: string, icon: string) => {
panels[id] = { id, icon };
return derived(activePanel, ($activePanel) => {
return $activePanel === id;
});
});
function setActivePanel(panel: keyof typeof panels | false) {
if (panel === $activePanel) {
$activePanel = false;
@ -37,28 +38,6 @@
$activePanel = false;
}
}
interface Nested {
[key: string]: NodeInput | Nested;
}
function constructNested(panel: (typeof panels)[keyof typeof panels]) {
const nested: Nested = {};
for (const key in panel.definition) {
const parts = key.split(".");
let current = nested;
for (let i = 0; i < parts.length; i++) {
if (i === parts.length - 1) {
current[parts[i]] = panel.definition[key];
} else {
current[parts[i]] = current[parts[i]] || {};
current = current[parts[i]] as Nested;
}
}
}
return nested;
}
</script>
<div class="wrapper" class:visible={$activePanel}>
@ -81,25 +60,7 @@
{/each}
</div>
<div class="content">
{#if $activePanel && panels[$activePanel] && panels[$activePanel].hidden !== true}
<h1 class="m-0 p-4">{panels[$activePanel].id}</h1>
{#key $activePanel}
{#if panels[$activePanel]?.component}
<svelte:component
this={panels[$activePanel].component}
{...panels[$activePanel]?.props}
/>
{:else}
<div class="flex flex-col">
<NestedSettings
id={$activePanel}
settings={constructNested(panels[$activePanel])}
store={panels[$activePanel].settings}
/>
</div>
{/if}
{/key}
{/if}
<slot />
</div>
</div>

View File

@ -10,6 +10,7 @@ export const AppSettings = localStore("node-settings", {
showVertices: false,
centerCamera: true,
showStemLines: false,
amount: 5
});
const themes = ["dark", "light", "catppuccin", "solarized", "high-contrast", "nord", "dracula"];

View File

@ -1,6 +1,6 @@
<script lang="ts">
import type { Node, NodeInput } from "@nodes/types";
import NestedSettings from "../NestedSettings.svelte";
import NestedSettings from "./NestedSettings.svelte";
import { writable } from "svelte/store";
import type { GraphManager } from "$lib/graph-interface/graph-manager";
@ -28,7 +28,7 @@
export let manager: GraphManager;
export let node: Node;
export let node: Node | undefined;
let nodeDefinition: Record<string, NodeInput> | undefined;
$: nodeDefinition = node?.tmp?.type
? filterInputs(node.tmp.type.inputs)

View File

@ -0,0 +1,40 @@
<script lang="ts">
import type { NodeInput } from "@nodes/types";
import NestedSettings from "./NestedSettings.svelte";
import type { Writable } from "svelte/store";
interface Nested {
[key: string]: NodeInput | Nested;
}
export let type: Record<string, NodeInput>;
export let store: Writable<Record<string, any>>;
function constructNested(type: Record<string, NodeInput>) {
const nested: Nested = {};
for (const key in type) {
const parts = key.split(".");
let current = nested;
for (let i = 0; i < parts.length; i++) {
if (i === parts.length - 1) {
current[parts[i]] = type[key];
} else {
current[parts[i]] = current[parts[i]] || {};
current = current[parts[i]] as Nested;
}
}
}
return nested;
}
$: settings = constructNested({
randomSeed: { type: "boolean", value: false },
...type,
});
</script>
{#key settings}
<NestedSettings id="graph-settings" {settings} {store} />
{/key}

View File

@ -3,7 +3,7 @@
import { ShortCut } from "@nodes/ui";
export let keymap: ReturnType<typeof createKeyMap>;
const keys = keymap.keys;
const keys = keymap?.keys;
</script>
<div class="wrapper">
@ -13,7 +13,12 @@
{#each $keys as key}
{#if key.description}
<div class="command-wrapper">
<ShortCut {...key} />
<ShortCut
alt={key.alt}
ctrl={key.ctrl}
shift={key.shift}
key={key.key}
/>
</div>
<p>{key.description}</p>
{/if}

View File

@ -4,8 +4,12 @@
import Input from "@nodes/ui";
import type { Writable } from "svelte/store";
type Button = { type: "button"; label?: string; callback: () => void };
type Input = NodeInput | Button;
interface Nested {
[key: string]: Nested | NodeInput;
[key: string]: (Nested & { __title?: string }) | Input;
}
export let id: string;
@ -20,22 +24,23 @@
export let depth = 0;
const keys = Object.keys(settings).filter((key) => key !== "__title");
function isNodeInput(v: NodeInput | Nested): v is NodeInput {
function isNodeInput(v: Input | Nested): v is Input {
return v && "type" in v;
}
console.log({ settings, store });
</script>
{#if store}
{#if $store}
{#each keys as key}
{@const value = settings[key]}
<div class="wrapper" class:first-level={depth === 0}>
{#if isNodeInput(value)}
{#if value !== undefined && isNodeInput(value)}
<div class="input input-{settings[key].type}">
{#if settings[key].type === "button"}
<button on:click={() => settings[key]?.callback?.()}
>{settings[key].label || key}</button
{#if value.type === "button"}
<button on:click={() => value?.callback?.()}
>{value.label || key}</button
>
{:else if "setting" in value}
{: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}

View File

@ -0,0 +1,19 @@
import { MemoryRuntimeExecutor } from "./runtime-executor";
import { RemoteNodeRegistry } from "./node-registry-client";
import type { Graph } from "@nodes/types";
import { createPerformanceStore } from "./performance";
const nodeRegistry = new RemoteNodeRegistry("");
const executor = new MemoryRuntimeExecutor(nodeRegistry);
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));
return executor.execute(graph, settings);
}
export function getPerformanceData() {
return performanceStore.get();
}

View File

@ -0,0 +1,16 @@
/// <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() {
}
async execute(graph: Graph, settings: Record<string, unknown>) {
return this.worker.executeGraph(graph, settings);
}
async getPerformanceData() {
return this.worker.getPerformanceData();
}
}

View File

@ -1,17 +1,14 @@
<script lang="ts">
import Grid from "$lib/grid";
import GraphInterface from "$lib/graph-interface";
import {
MemoryRuntimeExecutor,
MemoryRuntimeCache,
} from "$lib/runtime-executor";
import { WorkerRuntimeExecutor } from "$lib/worker-runtime-executor";
import { RemoteNodeRegistry } from "$lib/node-registry-client";
import * as templates from "$lib/graph-templates";
import type { Graph, Node } from "@nodes/types";
import Viewer from "$lib/result-viewer/Viewer.svelte";
import Settings from "$lib/settings/Settings.svelte";
import { AppSettings, AppSettingTypes } from "$lib/settings/app-settings";
import { get, writable, type Writable } from "svelte/store";
import { AppSettingTypes, AppSettings } from "$lib/settings/app-settings";
import { writable, type Writable } from "svelte/store";
import Keymap from "$lib/settings/panels/Keymap.svelte";
import type { createKeyMap } from "$lib/helpers/createKeyMap";
import NodeStore from "$lib/node-store/NodeStore.svelte";
@ -22,27 +19,22 @@
decodeNestedArray,
encodeNestedArray,
} from "@nodes/utils";
import type { PerspectiveCamera, Vector3 } from "three";
import type { OrbitControls } from "three/examples/jsm/Addons.js";
import ActiveNode from "$lib/settings/panels/ActiveNode.svelte";
import { createPerformanceStore } from "$lib/performance";
import ActiveNodeSettings from "$lib/settings/panels/ActiveNodeSettings.svelte";
import PerformanceViewer from "$lib/performance/PerformanceViewer.svelte";
import Panel from "$lib/settings/Panel.svelte";
import GraphSettings from "$lib/settings/panels/GraphSettings.svelte";
import NestedSettings from "$lib/settings/panels/NestedSettings.svelte";
const nodePerformance = createPerformanceStore();
const runtimeCache = new MemoryRuntimeCache();
const nodeRegistry = new RemoteNodeRegistry("");
const runtimeExecutor = new MemoryRuntimeExecutor(nodeRegistry, runtimeCache);
runtimeExecutor.perf = nodePerformance;
const workerRuntime = new WorkerRuntimeExecutor();
let performanceData: PerformanceData;
globalThis.decode = decodeNestedArray;
globalThis.encode = encodeNestedArray;
globalThis.decodeFloat = decodeFloat;
let res: Int32Array;
let viewerCamera: PerspectiveCamera;
let viewerControls: OrbitControls;
let viewerCenter: Vector3;
let activeNode: Node | undefined;
let graph = localStorage.getItem("graph")
@ -56,154 +48,103 @@
}
let keymap: ReturnType<typeof createKeyMap>;
let graphSettings = writable<Record<string, any>>({});
let graphSettingTypes = {};
function handleResult(event: CustomEvent<Graph>) {
async function handleResult(event: CustomEvent<Graph>) {
const settings = $graphSettings;
if (!settings) return;
try {
res = runtimeExecutor.execute(
event.detail,
get(settingPanels?.graph?.settings),
);
res = await workerRuntime.execute(event.detail, settings);
performanceData = await workerRuntime.getPerformanceData();
} catch (error) {
console.log("errors", error);
}
}
if ($AppSettings.centerCamera && viewerCamera && viewerCenter) {
if (
Number.isNaN(viewerCenter.x) ||
Number.isNaN(viewerCenter.y) ||
Number.isNaN(viewerCenter.z)
) {
// viewerCenter.set(0, 0, 0);
} else {
viewerControls.target.copy(viewerCenter);
}
viewerControls.update();
}
$: if (AppSettings) {
//@ts-ignore
AppSettingTypes.debug.stressTest.loadGrid.callback = () => {
graph = templates.grid($AppSettings.amount, $AppSettings.amount);
};
//@ts-ignore
AppSettingTypes.debug.stressTest.loadTree.callback = () => {
graph = templates.tree($AppSettings.amount, $AppSettings.amount);
};
}
function handleSave(event: CustomEvent<Graph>) {
localStorage.setItem("graph", JSON.stringify(event.detail));
}
let settingPanels: Record<string, any> = {
general: {
id: "general",
icon: "i-tabler-settings",
settings: AppSettings,
definition: AppSettingTypes,
},
shortcuts: {},
nodeStore: {},
graph: {
id: "graph",
icon: "i-tabler-git-fork",
definition: {
randomSeed: {
type: "boolean",
label: "Random Seed",
value: true,
},
},
},
performance: {
id: "performance",
icon: "i-tabler-brand-speedtest",
props: { store: nodePerformance, title: "Runtime Performance" },
component: PerformanceViewer,
},
activeNode: {
id: "Active Node",
icon: "i-tabler-adjustments",
props: { node: undefined, manager: undefined },
component: ActiveNode,
},
};
$: if (keymap) {
settingPanels.shortcuts = {
id: "shortcuts",
icon: "i-tabler-keyboard",
props: { keymap },
component: Keymap,
};
settingPanels = settingPanels;
}
$: if (manager) {
settingPanels.activeNode.props.manager = manager;
settingPanels.nodeStore = {
id: "Node Store",
icon: "i-tabler-database",
props: { nodeRegistry, manager },
component: NodeStore,
};
settingPanels = settingPanels;
}
$: if (activeNode) {
settingPanels.activeNode.props.node = activeNode;
settingPanels = settingPanels;
} else {
settingPanels.activeNode.props.node = undefined;
settingPanels = settingPanels;
}
function handleSettings(
ev: CustomEvent<{
values: Record<string, unknown>;
types: Record<string, unknown>;
}>,
) {
settingPanels.general.definition.debug.stressTest.loadGrid.callback =
function () {
const store = get(settingPanels.general.settings);
graph = templates.grid(store.amount, store.amount);
};
settingPanels.general.definition.debug.stressTest.loadTree.callback =
function () {
const store = get(settingPanels.general.settings);
graph = templates.tree(store.amount);
};
settingPanels.graph.settings = writable(ev.detail.values);
settingPanels.graph.definition = {
...settingPanels.graph.definition,
...ev.detail.types,
};
settingPanels = settingPanels;
}
</script>
<div class="wrapper manager-{$managerStatus}">
<header></header>
<Grid.Row>
<Grid.Cell>
<Viewer
bind:controls={viewerControls}
bind:center={viewerCenter}
bind:camera={viewerCamera}
result={res}
/>
<Viewer centerCamera={$AppSettings.centerCamera} result={res} />
</Grid.Cell>
<Grid.Cell>
{#key graph}
<GraphInterface
{graph}
registry={nodeRegistry}
bind:manager
bind:activeNode
registry={nodeRegistry}
{graph}
bind:keymap
showGrid={$AppSettings?.showNodeGrid}
snapToGrid={$AppSettings?.snapToGrid}
settings={settingPanels?.graph?.settings}
on:settings={handleSettings}
bind:settings={graphSettings}
bind:settingTypes={graphSettingTypes}
on:result={handleResult}
on:save={handleSave}
/>
<Settings panels={settingPanels}></Settings>
<Settings>
<Panel id="general" title="General" icon="i-tabler-settings">
<NestedSettings
id="general"
store={AppSettings}
settings={AppSettingTypes}
/>
</Panel>
<Panel id="node-store" title="Node Store" icon="i-tabler-database">
<NodeStore registry={nodeRegistry} />
</Panel>
<Panel
id="performance"
title="Performance"
icon="i-tabler-brand-speedtest"
>
{#if performanceData}
<PerformanceViewer data={performanceData} />
{/if}
</Panel>
<Panel
id="shortcuts"
title="Keyboard Shortcuts"
icon="i-tabler-keyboard"
>
{#if keymap}
<Keymap {keymap} />
{/if}
</Panel>
<Panel
id="graph-settings"
title="Graph Settings"
icon="i-tabler-brand-git"
>
{#if Object.keys(graphSettingTypes).length > 0}
<GraphSettings type={graphSettingTypes} store={graphSettings} />
{/if}
</Panel>
<Panel
id="active-node"
title="Node Settings"
icon="i-tabler-adjustments"
>
<ActiveNodeSettings {manager} node={activeNode} />
</Panel>
</Settings>
{/key}
</Grid.Cell>
</Grid.Row>

View File

@ -10,18 +10,18 @@
"scale": {
"type": "float",
"min": 0.1,
"max": 100
"max": 10
},
"strength": {
"type": "float",
"min": 0.1,
"max": 100
"max": 10
},
"fixBottom": {
"type": "float",
"label": "Fixate bottom of plant",
"hidden": true,
"value": 0,
"value": 1,
"min": 0,
"max": 1
},

View File

@ -33,7 +33,7 @@ export interface RuntimeExecutor {
* @param graph - The graph to execute
* @returns The result of the execution
*/
execute: (graph: Graph, settings: Record<string, unknown>) => unknown;
execute: (graph: Graph, settings: Record<string, unknown>) => Int32Array;
}
export interface RuntimeCache {