feat: add benchmark settings panel
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m59s
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m59s
This commit is contained in:
@ -18,6 +18,7 @@
|
||||
"@types/three": "^0.164.0",
|
||||
"@unocss/reset": "^0.59.4",
|
||||
"comlink": "^4.4.1",
|
||||
"file-saver": "^2.0.5",
|
||||
"jsondiffpatch": "^0.6.0",
|
||||
"three": "^0.164.1"
|
||||
},
|
||||
@ -27,6 +28,7 @@
|
||||
"@sveltejs/adapter-static": "^3.0.1",
|
||||
"@sveltejs/vite-plugin-svelte": "next",
|
||||
"@tsconfig/svelte": "^5.0.4",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@unocss/preset-icons": "^0.59.4",
|
||||
"svelte": "5.0.0-next.118",
|
||||
"svelte-check": "^3.7.0",
|
||||
|
@ -12,7 +12,7 @@
|
||||
import Camera from "../Camera.svelte";
|
||||
import GraphView from "./GraphView.svelte";
|
||||
import type { Node, NodeId, Node as NodeType, Socket } from "@nodes/types";
|
||||
import { NodeDefinitionSchema } from "@nodes/types";
|
||||
import { GraphSchema, NodeDefinitionSchema } from "@nodes/types";
|
||||
import FloatingEdge from "../edges/FloatingEdge.svelte";
|
||||
import {
|
||||
activeNodeId,
|
||||
@ -28,6 +28,7 @@
|
||||
import { createWasmWrapper } from "@nodes/utils";
|
||||
|
||||
import HelpView from "../HelpView.svelte";
|
||||
import FileSaver from "file-saver";
|
||||
|
||||
export let manager: GraphManager;
|
||||
|
||||
@ -612,6 +613,7 @@
|
||||
keymap.addShortcut({
|
||||
key: "a",
|
||||
ctrl: true,
|
||||
preventDefault: true,
|
||||
description: "Select all nodes",
|
||||
callback: () => {
|
||||
if (!isBodyFocused()) return;
|
||||
@ -637,7 +639,6 @@
|
||||
ctrl: true,
|
||||
description: "Redo",
|
||||
callback: () => {
|
||||
if (!isBodyFocused()) return;
|
||||
manager.redo();
|
||||
for (const node of $nodes.values()) {
|
||||
updateNodePosition(node);
|
||||
@ -645,6 +646,20 @@
|
||||
},
|
||||
});
|
||||
|
||||
keymap.addShortcut({
|
||||
key: "s",
|
||||
ctrl: true,
|
||||
description: "Save",
|
||||
preventDefault: true,
|
||||
callback: () => {
|
||||
const state = manager.serialize();
|
||||
const blob = new Blob([JSON.stringify(state)], {
|
||||
type: "application/json;charset=utf-8",
|
||||
});
|
||||
FileSaver.saveAs(blob, "nodarium-graph.json");
|
||||
},
|
||||
});
|
||||
|
||||
keymap.addShortcut({
|
||||
key: ["Delete", "Backspace", "x"],
|
||||
description: "Delete selected nodes",
|
||||
@ -833,17 +848,31 @@
|
||||
});
|
||||
});
|
||||
} else if (event.dataTransfer.files.length) {
|
||||
const files = event.dataTransfer.files;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const buffer = e.target?.result as Buffer;
|
||||
if (buffer) {
|
||||
const wrapper = createWasmWrapper(buffer);
|
||||
const definition = wrapper.get_definition();
|
||||
const res = NodeDefinitionSchema.parse(definition);
|
||||
}
|
||||
};
|
||||
reader.readAsArrayBuffer(files[0]);
|
||||
const file = event.dataTransfer.files[0];
|
||||
|
||||
if (file.type === "application/wasm") {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const buffer = e.target?.result as Buffer;
|
||||
if (buffer) {
|
||||
const wrapper = createWasmWrapper(buffer);
|
||||
const definition = wrapper.get_definition();
|
||||
const res = NodeDefinitionSchema.parse(definition);
|
||||
console.log(res);
|
||||
}
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
} else if (file.type === "application/json") {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const buffer = e.target?.result as Buffer;
|
||||
if (buffer) {
|
||||
const state = GraphSchema.parse(JSON.parse(buffer.toString()));
|
||||
manager.load(state);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -893,13 +922,13 @@
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
accept="application/wasm"
|
||||
accept="application/wasm,application/json"
|
||||
id="drop-zone"
|
||||
disabled={!isDragging}
|
||||
on:dragend={handleDragEnd}
|
||||
on:dragleave={handleDragEnd}
|
||||
/>
|
||||
<label for="drop-zone" />
|
||||
<label for="drop-zone"></label>
|
||||
|
||||
{#if showHelp}
|
||||
<HelpView registry={manager.registry} />
|
||||
|
@ -20,7 +20,7 @@ export function grid(width: number, height: number) {
|
||||
visible: false,
|
||||
},
|
||||
position: [x * 30, y * 40],
|
||||
props: i == 0 ? { value: 0 } : { op_type: 2, a: 2, b: 2 },
|
||||
props: i == 0 ? { value: 0 } : { op_type: 0, a: 1, b: 0.05 },
|
||||
type: i == 0 ? "max/plantarium/float" : "max/plantarium/math",
|
||||
});
|
||||
|
||||
|
@ -1,3 +1,7 @@
|
||||
export { grid } from "./grid";
|
||||
export { tree } from "./tree";
|
||||
export { plant } from "./plant";
|
||||
export { default as lottaFaces } from "./lotta-faces.json";
|
||||
export { default as lottaNodes } from "./lotta-nodes.json";
|
||||
export { default as lottaNodesAndFaces } from "./lotta-nodes-and-faces.json";
|
||||
|
||||
|
1
app/src/lib/graph-templates/lotta-faces.json
Normal file
1
app/src/lib/graph-templates/lotta-faces.json
Normal file
@ -0,0 +1 @@
|
||||
{"settings":{"resolution.circle":64,"resolution.curve":64,"randomSeed":false},"nodes":[{"id":9,"position":[260,0],"type":"max/plantarium/output","props":{}},{"id":18,"position":[185,0],"type":"max/plantarium/stem","props":{"amount":64,"length":12,"thickness":0.15}},{"id":19,"position":[210,0],"type":"max/plantarium/noise","props":{"scale":1.3,"strength":5.4}},{"id":20,"position":[235,0],"type":"max/plantarium/branch","props":{"length":0.8,"thickness":0.8,"amount":3}},{"id":21,"position":[160,0],"type":"max/plantarium/vec3","props":{"0":0.39,"1":0,"2":0.41}},{"id":22,"position":[130,0],"type":"max/plantarium/random","props":{"min":-2,"max":2}}],"edges":[[18,0,19,"plant"],[19,0,20,"plant"],[20,0,9,"input"],[21,0,18,"origin"],[22,0,21,"0"],[22,0,21,"2"]]}
|
1
app/src/lib/graph-templates/lotta-nodes-and-faces.json
Normal file
1
app/src/lib/graph-templates/lotta-nodes-and-faces.json
Normal file
File diff suppressed because one or more lines are too long
1
app/src/lib/graph-templates/lotta-nodes.json
Normal file
1
app/src/lib/graph-templates/lotta-nodes.json
Normal file
File diff suppressed because one or more lines are too long
@ -5,6 +5,7 @@ type Shortcut = {
|
||||
shift?: boolean,
|
||||
ctrl?: boolean,
|
||||
alt?: boolean,
|
||||
preventDefault?: boolean,
|
||||
description?: string,
|
||||
callback: (event: KeyboardEvent) => void
|
||||
}
|
||||
@ -17,8 +18,11 @@ export function createKeyMap(keys: Shortcut[]) {
|
||||
|
||||
const store = writable(new Map(keys.map(k => [getShortcutId(k), k])));
|
||||
|
||||
|
||||
return {
|
||||
handleKeyboardEvent: (event: KeyboardEvent) => {
|
||||
const activeElement = document.activeElement as HTMLElement;
|
||||
if (activeElement?.tagName === "INPUT" || activeElement?.tagName === "TEXTAREA") return;
|
||||
const key = [...get(store).values()].find(k => {
|
||||
if (Array.isArray(k.key) ? !k.key.includes(event.key) : k.key !== event.key) return false;
|
||||
if ("shift" in k && k.shift !== event.shiftKey) return false;
|
||||
@ -26,6 +30,7 @@ export function createKeyMap(keys: Shortcut[]) {
|
||||
if ("alt" in k && k.alt !== event.altKey) return false;
|
||||
return true;
|
||||
});
|
||||
if (key && key.preventDefault) event.preventDefault();
|
||||
key?.callback(event);
|
||||
},
|
||||
addShortcut: (shortcut: Shortcut) => {
|
||||
|
48
app/src/lib/performance/BarSplit.svelte
Normal file
48
app/src/lib/performance/BarSplit.svelte
Normal file
@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
export let labels: string[] = [];
|
||||
export let values: number[] = [];
|
||||
|
||||
$: total = values.reduce((acc, v) => acc + v, 0);
|
||||
|
||||
let colors = ["red", "green", "blue"];
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<div class="bars">
|
||||
{#each values as value, i}
|
||||
<div class="bar bg-{colors[i]}" style="width: {(value / total) * 100}%;">
|
||||
{Math.round(value)}ms
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="labels mt-2">
|
||||
{#each values as _label, i}
|
||||
<div class="text-{colors[i]}">{labels[i]}</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<span
|
||||
class="bg-red bg-green bg-yellow bg-blue text-red text-green text-yellow text-blue"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
margin-block: 1em;
|
||||
}
|
||||
|
||||
.bars {
|
||||
height: 20px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.bar {
|
||||
height: 100%;
|
||||
color: black;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.8em;
|
||||
padding-left: 0.4em;
|
||||
}
|
||||
</style>
|
@ -4,6 +4,7 @@
|
||||
import { Checkbox } from "@nodes/ui";
|
||||
import localStore from "$lib/helpers/localStore";
|
||||
import { type PerformanceData } from "./store";
|
||||
import BarSplit from "./BarSplit.svelte";
|
||||
|
||||
export let data: PerformanceData;
|
||||
|
||||
@ -42,7 +43,7 @@
|
||||
}
|
||||
|
||||
function getLast(key: string) {
|
||||
return data.at(-1)?.[key][0] || 0;
|
||||
return data.at(-1)?.[key]?.[0] || 0;
|
||||
}
|
||||
|
||||
function getLasts() {
|
||||
@ -53,13 +54,13 @@
|
||||
if (onlyLast) {
|
||||
return (
|
||||
getLast("runtime") +
|
||||
getLast("create-geometries") +
|
||||
getLast("update-geometries") +
|
||||
getLast("worker-transfer")
|
||||
);
|
||||
}
|
||||
return (
|
||||
getAverage("runtime") +
|
||||
getAverage("create-geometries") +
|
||||
getAverage("update-geometries") +
|
||||
getAverage("worker-transfer")
|
||||
);
|
||||
}
|
||||
@ -73,7 +74,7 @@
|
||||
const viewerKeys = [
|
||||
"total-vertices",
|
||||
"total-faces",
|
||||
"create-geometries",
|
||||
"update-geometries",
|
||||
"split-result",
|
||||
];
|
||||
|
||||
@ -116,8 +117,8 @@
|
||||
return data.map((run) => {
|
||||
return (
|
||||
run["runtime"].reduce((acc, v) => acc + v, 0) +
|
||||
run["create-geometries"].reduce((acc, v) => acc + v, 0) +
|
||||
run["worker-transfer"].reduce((acc, v) => acc + v, 0)
|
||||
run["update-geometries"].reduce((acc, v) => acc + v, 0) +
|
||||
(run["worker-transfer"]?.reduce((acc, v) => acc + v, 0) || 0)
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -125,8 +126,8 @@
|
||||
return data.map((run) => {
|
||||
return (
|
||||
run["runtime"][0] +
|
||||
run["create-geometries"][0] +
|
||||
run["worker-transfer"][0]
|
||||
run["update-geometries"][0] +
|
||||
(run["worker-transfer"]?.[0] || 0)
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -147,6 +148,22 @@
|
||||
});
|
||||
}
|
||||
|
||||
function getSplitValues(): number[] {
|
||||
if (showAverage) {
|
||||
return [
|
||||
getAverage("worker-transfer"),
|
||||
getAverage("runtime"),
|
||||
getAverage("update-geometries"),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
getLast("worker-transfer"),
|
||||
getLast("runtime"),
|
||||
getLast("update-geometries"),
|
||||
];
|
||||
}
|
||||
|
||||
function getTitle(t: string) {
|
||||
if (t.includes("/")) {
|
||||
return `Node ${t.split("/").slice(-1).join("/")}`;
|
||||
@ -159,7 +176,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#key $activeType && data}
|
||||
{#key $activeType}
|
||||
{#if $activeType === "cache-hit"}
|
||||
<Monitor
|
||||
title="Cache Hits"
|
||||
@ -174,117 +191,122 @@
|
||||
points={constructPoints($activeType)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="p-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox id="show-total" bind:value={showAverage} />
|
||||
<label for="show-total">Show Average</label>
|
||||
</div>
|
||||
{#if data.length !== 0}
|
||||
<h3>General</h3>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
{round(getTotalPerformance(!showAverage))}<span>ms</span>
|
||||
</td>
|
||||
<td
|
||||
class:active={$activeType === "total"}
|
||||
on:click={() => ($activeType = "total")}
|
||||
>
|
||||
total<span
|
||||
>({Math.floor(
|
||||
1000 / getTotalPerformance(showAverage),
|
||||
)}fps)</span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
{#each getPerformanceData(!showAverage) as [key, value]}
|
||||
<tr>
|
||||
<td>
|
||||
{round(value)}<span>ms</span>
|
||||
</td>
|
||||
<td
|
||||
class:active={$activeType === key}
|
||||
on:click={() => ($activeType = key)}
|
||||
>
|
||||
{key}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
|
||||
<tr>
|
||||
<td>{data.length}</td>
|
||||
<td>Samples</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<h3>Nodes</h3>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td> {getCacheRatio(!showAverage)}<span>%</span> </td>
|
||||
<td
|
||||
class:active={$activeType === "cache-hit"}
|
||||
on:click={() => ($activeType = "cache-hit")}>cache hits</td
|
||||
>
|
||||
</tr>
|
||||
{#each getNodePerformanceData(!showAverage) as [key, value]}
|
||||
<tr>
|
||||
<td>
|
||||
{round(value)}<span>ms</span>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class:active={$activeType === key}
|
||||
on:click={() => ($activeType = key)}
|
||||
>
|
||||
{key.split("/").slice(-1).join("/")}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<h3>Viewer</h3>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{humanizeNumber(getLast("total-vertices"))}</td>
|
||||
<td>Vertices</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{humanizeNumber(getLast("total-faces"))}</td>
|
||||
<td>Faces</td>
|
||||
</tr>
|
||||
{#each getViewerPerformanceData(!showAverage) as [key, value]}
|
||||
<tr>
|
||||
<td>
|
||||
{round(value)}<span>ms</span>
|
||||
</td>
|
||||
<td
|
||||
class:active={$activeType === key}
|
||||
on:click={() => ($activeType = key)}
|
||||
>
|
||||
{key.split("/").slice(-1).join("/")}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{:else}
|
||||
<p>No runs available</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/key}
|
||||
|
||||
<div class="p-4 performance-tabler">
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox id="show-total" bind:value={showAverage} />
|
||||
<label for="show-total">Show Average</label>
|
||||
</div>
|
||||
|
||||
{#if data.length !== 0}
|
||||
<BarSplit
|
||||
labels={["worker-transfer", "runtime", "update-geometries"]}
|
||||
values={getSplitValues()}
|
||||
/>
|
||||
|
||||
<h3>General</h3>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
{round(getTotalPerformance(!showAverage))}<span>ms</span>
|
||||
</td>
|
||||
<td
|
||||
class:active={$activeType === "total"}
|
||||
on:click={() => ($activeType = "total")}
|
||||
>
|
||||
total<span
|
||||
>({Math.floor(1000 / getTotalPerformance(showAverage))}fps)</span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
{#each getPerformanceData(!showAverage) as [key, value]}
|
||||
<tr>
|
||||
<td>
|
||||
{round(value)}<span>ms</span>
|
||||
</td>
|
||||
<td
|
||||
class:active={$activeType === key}
|
||||
on:click={() => ($activeType = key)}
|
||||
>
|
||||
{key}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
|
||||
<tr>
|
||||
<td>{data.length}</td>
|
||||
<td>Samples</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<h3>Nodes</h3>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td> {getCacheRatio(!showAverage)}<span>%</span> </td>
|
||||
<td
|
||||
class:active={$activeType === "cache-hit"}
|
||||
on:click={() => ($activeType = "cache-hit")}>cache hits</td
|
||||
>
|
||||
</tr>
|
||||
{#each getNodePerformanceData(!showAverage) as [key, value]}
|
||||
<tr>
|
||||
<td>
|
||||
{round(value)}<span>ms</span>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class:active={$activeType === key}
|
||||
on:click={() => ($activeType = key)}
|
||||
>
|
||||
{key.split("/").slice(-1).join("/")}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<h3>Viewer</h3>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{humanizeNumber(getLast("total-vertices"))}</td>
|
||||
<td>Vertices</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{humanizeNumber(getLast("total-faces"))}</td>
|
||||
<td>Faces</td>
|
||||
</tr>
|
||||
{#each getViewerPerformanceData(!showAverage) as [key, value]}
|
||||
<tr>
|
||||
<td>
|
||||
{round(value)}<span>ms</span>
|
||||
</td>
|
||||
<td
|
||||
class:active={$activeType === key}
|
||||
on:click={() => ($activeType = key)}
|
||||
>
|
||||
{key.split("/").slice(-1).join("/")}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{:else}
|
||||
<p>No runs available</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
h3 {
|
||||
margin: 0;
|
||||
@ -296,6 +318,9 @@
|
||||
opacity: 0.3;
|
||||
margin-left: 4px;
|
||||
}
|
||||
table {
|
||||
margin-bottom: 70px;
|
||||
}
|
||||
td {
|
||||
padding-right: 10px;
|
||||
padding-block: 5px;
|
||||
|
@ -6,15 +6,18 @@ export interface PerformanceStore extends Readable<PerformanceData> {
|
||||
startRun(): void;
|
||||
stopRun(): void;
|
||||
addPoint(name: string, value?: number): void;
|
||||
endPoint(name?: string): void;
|
||||
mergeData(data: PerformanceData[number]): void;
|
||||
get: () => PerformanceData;
|
||||
}
|
||||
|
||||
export function createPerformanceStore(): PerformanceStore {
|
||||
export function createPerformanceStore(id?: string): PerformanceStore {
|
||||
|
||||
let data: PerformanceData = [];
|
||||
|
||||
let currentRun: Record<string, number[]> | undefined;
|
||||
let temp: Record<string, number> | undefined;
|
||||
let lastPoint: string | undefined;
|
||||
|
||||
let set: (v: PerformanceData) => void;
|
||||
|
||||
@ -23,22 +26,36 @@ export function createPerformanceStore(): PerformanceStore {
|
||||
});
|
||||
|
||||
function startRun() {
|
||||
if (currentRun) return;
|
||||
currentRun = {};
|
||||
lastPoint = undefined;
|
||||
temp = {
|
||||
start: performance.now()
|
||||
}
|
||||
}
|
||||
|
||||
function stopRun() {
|
||||
if (currentRun) {
|
||||
if (currentRun && temp) {
|
||||
currentRun["total"] = [performance.now() - temp.start];
|
||||
data.push(currentRun);
|
||||
data = data.slice(-100);
|
||||
currentRun = undefined;
|
||||
temp = undefined;
|
||||
if (set) set(data);
|
||||
}
|
||||
}
|
||||
|
||||
function addPoint(name: string, value: number) {
|
||||
function addPoint(name: string, value?: number) {
|
||||
if (!currentRun) return;
|
||||
currentRun[name] = currentRun[name] || [];
|
||||
currentRun[name].push(value);
|
||||
if (value === undefined) {
|
||||
if (temp) {
|
||||
lastPoint = name;
|
||||
temp[name] = performance.now();
|
||||
}
|
||||
} else {
|
||||
currentRun[name] = currentRun[name] || [];
|
||||
currentRun[name].push(value);
|
||||
}
|
||||
}
|
||||
|
||||
function get() {
|
||||
@ -59,11 +76,21 @@ export function createPerformanceStore(): PerformanceStore {
|
||||
});
|
||||
}
|
||||
|
||||
function endPoint(name = lastPoint) {
|
||||
if (name === lastPoint) lastPoint = undefined;
|
||||
if (name && currentRun && temp && name in temp) {
|
||||
currentRun[name] = currentRun[name] || [];
|
||||
currentRun[name].push(performance.now() - temp[name]);
|
||||
delete temp[name];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
startRun,
|
||||
stopRun,
|
||||
addPoint,
|
||||
endPoint,
|
||||
mergeData,
|
||||
get
|
||||
}
|
||||
|
@ -1,11 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { T } from "@threlte/core";
|
||||
import {
|
||||
MeshLineGeometry,
|
||||
MeshLineMaterial,
|
||||
Text,
|
||||
useTexture,
|
||||
} from "@threlte/extras";
|
||||
import { T, useThrelte } from "@threlte/core";
|
||||
import { MeshLineGeometry, MeshLineMaterial, Text } from "@threlte/extras";
|
||||
import {
|
||||
type Group,
|
||||
type BufferGeometry,
|
||||
@ -16,15 +11,20 @@
|
||||
import { AppSettings } from "../settings/app-settings";
|
||||
import Camera from "./Camera.svelte";
|
||||
|
||||
const d = useThrelte();
|
||||
|
||||
export const invalidate = d.invalidate;
|
||||
|
||||
export let geometries: BufferGeometry[];
|
||||
export let lines: Vector3[][];
|
||||
export let scene;
|
||||
let geos: Group;
|
||||
$: scene = geos;
|
||||
export let geoGroup: Group;
|
||||
|
||||
export let centerCamera: boolean = true;
|
||||
let center = new Vector3(0, 4, 0);
|
||||
|
||||
const matcap = useTexture("/matcap_green.jpg");
|
||||
|
||||
function getPosition(geo: BufferGeometry, i: number) {
|
||||
return [
|
||||
geo.attributes.position.array[i],
|
||||
@ -48,9 +48,6 @@
|
||||
<T.GridHelper args={[20, 20]} />
|
||||
{/if}
|
||||
|
||||
<T.DirectionalLight position={[0, 10, 10]} />
|
||||
<T.AmbientLight intensity={2} />
|
||||
|
||||
<T.Group bind:ref={geos}>
|
||||
{#each geometries as geo}
|
||||
{#if $AppSettings.showIndices}
|
||||
@ -67,15 +64,9 @@
|
||||
<T.PointsMaterial size={0.25} />
|
||||
</T.Points>
|
||||
{/if}
|
||||
{#await matcap then value}
|
||||
<T.Mesh geometry={geo}>
|
||||
<T.MeshMatcapMaterial
|
||||
matcap={value}
|
||||
wireframe={$AppSettings.wireframe}
|
||||
/>
|
||||
</T.Mesh>
|
||||
{/await}
|
||||
{/each}
|
||||
|
||||
<T.Group bind:ref={geoGroup}></T.Group>
|
||||
</T.Group>
|
||||
|
||||
{#if $AppSettings.showStemLines && lines}
|
||||
|
@ -1,155 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { Canvas } from "@threlte/core";
|
||||
import Scene from "./Scene.svelte";
|
||||
import {
|
||||
BufferAttribute,
|
||||
BufferGeometry,
|
||||
Float32BufferAttribute,
|
||||
Vector3,
|
||||
} from "three";
|
||||
import { decodeFloat, fastHashArrayBuffer } from "@nodes/utils";
|
||||
import { BufferGeometry, Group, Vector3 } from "three";
|
||||
|
||||
import { updateGeometries } from "./updateGeometries";
|
||||
import { decodeFloat, splitNestedArray } from "@nodes/utils";
|
||||
import type { PerformanceStore } from "$lib/performance";
|
||||
import { AppSettings } from "$lib/settings/app-settings";
|
||||
|
||||
export let centerCamera: boolean = true;
|
||||
export let perf: PerformanceStore;
|
||||
export let scene: Group;
|
||||
|
||||
let geoGroup: Group;
|
||||
|
||||
let geometries: BufferGeometry[] = [];
|
||||
let lines: Vector3[][] = [];
|
||||
|
||||
function fastArrayHash(arr: ArrayBuffer) {
|
||||
let ints = new Uint8Array(arr);
|
||||
|
||||
const sampleDistance = Math.max(Math.floor(ints.length / 100), 1);
|
||||
const sampleCount = Math.floor(ints.length / sampleDistance);
|
||||
|
||||
let hash = new Uint8Array(sampleCount);
|
||||
|
||||
for (let i = 0; i < sampleCount; i++) {
|
||||
const index = i * sampleDistance;
|
||||
hash[i] = ints[index];
|
||||
}
|
||||
|
||||
return fastHashArrayBuffer(hash.buffer);
|
||||
}
|
||||
|
||||
function createGeometryFromEncodedData(
|
||||
encodedData: Int32Array,
|
||||
geometry = new BufferGeometry(),
|
||||
): BufferGeometry {
|
||||
// Extract data from the encoded array
|
||||
let index = 1;
|
||||
// const geometryType = encodedData[index++];
|
||||
const vertexCount = encodedData[index++];
|
||||
const faceCount = encodedData[index++];
|
||||
|
||||
// Indices
|
||||
const indicesEnd = index + faceCount * 3;
|
||||
const indices = encodedData.subarray(index, indicesEnd);
|
||||
index = indicesEnd;
|
||||
|
||||
// Vertices
|
||||
const vertices = new Float32Array(
|
||||
encodedData.buffer,
|
||||
index * 4,
|
||||
vertexCount * 3,
|
||||
);
|
||||
index = index + vertexCount * 3;
|
||||
let hash = fastArrayHash(vertices);
|
||||
let posAttribute = geometry.getAttribute(
|
||||
"position",
|
||||
) as BufferAttribute | null;
|
||||
|
||||
if (geometry.userData?.hash === hash) {
|
||||
return geometry;
|
||||
}
|
||||
|
||||
if (posAttribute && posAttribute.count === vertexCount) {
|
||||
posAttribute.set(vertices, 0);
|
||||
posAttribute.needsUpdate = true;
|
||||
} else {
|
||||
geometry.setAttribute(
|
||||
"position",
|
||||
new Float32BufferAttribute(vertices, 3),
|
||||
);
|
||||
}
|
||||
|
||||
const normals = new Float32Array(
|
||||
encodedData.buffer,
|
||||
index * 4,
|
||||
vertexCount * 3,
|
||||
);
|
||||
index = index + vertexCount * 3;
|
||||
|
||||
if (
|
||||
geometry.userData?.faceCount !== faceCount ||
|
||||
geometry.userData?.vertexCount !== vertexCount
|
||||
) {
|
||||
// Add data to geometry
|
||||
geometry.setIndex([...indices]);
|
||||
}
|
||||
|
||||
const normalsAttribute = geometry.getAttribute(
|
||||
"normal",
|
||||
) as BufferAttribute | null;
|
||||
if (normalsAttribute && normalsAttribute.count === vertexCount) {
|
||||
normalsAttribute.set(normals, 0);
|
||||
normalsAttribute.needsUpdate = true;
|
||||
} else {
|
||||
geometry.setAttribute("normal", new Float32BufferAttribute(normals, 3));
|
||||
}
|
||||
|
||||
geometry.userData = {
|
||||
vertexCount,
|
||||
faceCount,
|
||||
hash,
|
||||
};
|
||||
|
||||
return geometry;
|
||||
}
|
||||
|
||||
function parse_args(input: Int32Array) {
|
||||
let index = 0;
|
||||
const length = input.length;
|
||||
let res: Int32Array[] = [];
|
||||
|
||||
let nextBracketIndex = 0;
|
||||
let argStartIndex = 0;
|
||||
let depth = -1;
|
||||
|
||||
while (index < length) {
|
||||
const value = input[index];
|
||||
|
||||
if (index === nextBracketIndex) {
|
||||
nextBracketIndex = index + input[index + 1] + 1;
|
||||
if (value === 0) {
|
||||
depth++;
|
||||
} else {
|
||||
depth--;
|
||||
}
|
||||
|
||||
if (depth === 1 && value === 0) {
|
||||
// if opening bracket
|
||||
argStartIndex = index + 2;
|
||||
}
|
||||
|
||||
if (depth === 0 && value === 1) {
|
||||
// if closing bracket
|
||||
res.push(input.slice(argStartIndex, index));
|
||||
argStartIndex = index + 2;
|
||||
}
|
||||
|
||||
index = nextBracketIndex;
|
||||
continue;
|
||||
}
|
||||
|
||||
// we should not be here
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
let invalidate: () => void;
|
||||
|
||||
function createLineGeometryFromEncodedData(encodedData: Int32Array) {
|
||||
const positions: Vector3[] = [];
|
||||
@ -166,16 +34,13 @@
|
||||
return positions;
|
||||
}
|
||||
|
||||
export let result: Int32Array;
|
||||
$: result && updateGeometries();
|
||||
function updateGeometries() {
|
||||
let a = performance.now();
|
||||
const inputs = parse_args(result);
|
||||
let b = performance.now();
|
||||
perf?.addPoint("split-result", b - a);
|
||||
export const update = function update(result: Int32Array) {
|
||||
perf?.addPoint("split-result");
|
||||
const inputs = splitNestedArray(result);
|
||||
perf?.endPoint();
|
||||
|
||||
if ($AppSettings.showStemLines) {
|
||||
a = performance.now();
|
||||
perf?.addPoint("create-lines");
|
||||
lines = inputs
|
||||
.map((input) => {
|
||||
if (input[0] === 0) {
|
||||
@ -183,31 +48,27 @@
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as Vector3[][];
|
||||
b = performance.now();
|
||||
perf?.addPoint("create-lines", b - a);
|
||||
perf.endPoint();
|
||||
}
|
||||
|
||||
let totalVertices = 0;
|
||||
let totalFaces = 0;
|
||||
perf?.addPoint("update-geometries");
|
||||
|
||||
const { totalVertices, totalFaces } = updateGeometries(inputs, geoGroup);
|
||||
perf?.endPoint();
|
||||
|
||||
a = performance.now();
|
||||
geometries = inputs
|
||||
.map((input, i) => {
|
||||
if (input[0] === 1) {
|
||||
let geo = createGeometryFromEncodedData(input);
|
||||
totalVertices += geo.userData.vertexCount;
|
||||
totalFaces += geo.userData.faceCount;
|
||||
return geo;
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as BufferGeometry[];
|
||||
b = performance.now();
|
||||
perf?.addPoint("create-geometries", b - a);
|
||||
perf?.addPoint("total-vertices", totalVertices);
|
||||
perf?.addPoint("total-faces", totalFaces);
|
||||
}
|
||||
invalidate();
|
||||
};
|
||||
</script>
|
||||
|
||||
<Canvas>
|
||||
<Scene {geometries} {lines} {centerCamera} />
|
||||
<Scene
|
||||
bind:scene
|
||||
bind:geoGroup
|
||||
bind:invalidate
|
||||
{geometries}
|
||||
{lines}
|
||||
{centerCamera}
|
||||
/>
|
||||
</Canvas>
|
||||
|
146
app/src/lib/result-viewer/updateGeometries.ts
Normal file
146
app/src/lib/result-viewer/updateGeometries.ts
Normal file
@ -0,0 +1,146 @@
|
||||
import { fastHashArrayBuffer } from "@nodes/utils";
|
||||
import { BufferAttribute, BufferGeometry, Float32BufferAttribute, Mesh, MeshMatcapMaterial, TextureLoader, type Group } from "three";
|
||||
|
||||
function fastArrayHash(arr: ArrayBuffer) {
|
||||
let ints = new Uint8Array(arr);
|
||||
|
||||
const sampleDistance = Math.max(Math.floor(ints.length / 100), 1);
|
||||
const sampleCount = Math.floor(ints.length / sampleDistance);
|
||||
|
||||
let hash = new Uint8Array(sampleCount);
|
||||
|
||||
for (let i = 0; i < sampleCount; i++) {
|
||||
const index = i * sampleDistance;
|
||||
hash[i] = ints[index];
|
||||
}
|
||||
|
||||
return fastHashArrayBuffer(hash.buffer);
|
||||
}
|
||||
|
||||
const loader = new TextureLoader();
|
||||
const matcap = loader.load('/matcap_green.jpg');
|
||||
matcap.colorSpace = "srgb";
|
||||
const material = new MeshMatcapMaterial({
|
||||
color: 0xffffff,
|
||||
matcap
|
||||
});
|
||||
|
||||
function createGeometryFromEncodedData(
|
||||
encodedData: Int32Array,
|
||||
geometry = new BufferGeometry(),
|
||||
): BufferGeometry {
|
||||
// Extract data from the encoded array
|
||||
let index = 1;
|
||||
// const geometryType = encodedData[index++];
|
||||
const vertexCount = encodedData[index++];
|
||||
const faceCount = encodedData[index++];
|
||||
|
||||
let hash = fastArrayHash(encodedData);
|
||||
|
||||
if (geometry.userData?.hash === hash) {
|
||||
return geometry;
|
||||
}
|
||||
|
||||
// Indices
|
||||
const indicesEnd = index + faceCount * 3;
|
||||
const indices = encodedData.subarray(index, indicesEnd);
|
||||
index = indicesEnd;
|
||||
|
||||
// Vertices
|
||||
const vertices = new Float32Array(
|
||||
encodedData.buffer,
|
||||
index * 4,
|
||||
vertexCount * 3,
|
||||
);
|
||||
index = index + vertexCount * 3;
|
||||
|
||||
let posAttribute = geometry.getAttribute(
|
||||
"position",
|
||||
) as BufferAttribute | null;
|
||||
|
||||
if (posAttribute && posAttribute.count === vertexCount) {
|
||||
posAttribute.set(vertices, 0);
|
||||
posAttribute.needsUpdate = true;
|
||||
} else {
|
||||
geometry.setAttribute(
|
||||
"position",
|
||||
new Float32BufferAttribute(vertices, 3),
|
||||
);
|
||||
}
|
||||
|
||||
const normals = new Float32Array(
|
||||
encodedData.buffer,
|
||||
index * 4,
|
||||
vertexCount * 3,
|
||||
);
|
||||
index = index + vertexCount * 3;
|
||||
|
||||
if (
|
||||
geometry.userData?.faceCount !== faceCount ||
|
||||
geometry.userData?.vertexCount !== vertexCount
|
||||
) {
|
||||
// Add data to geometry
|
||||
geometry.setIndex([...indices]);
|
||||
}
|
||||
|
||||
const normalsAttribute = geometry.getAttribute(
|
||||
"normal",
|
||||
) as BufferAttribute | null;
|
||||
if (normalsAttribute && normalsAttribute.count === vertexCount) {
|
||||
normalsAttribute.set(normals, 0);
|
||||
normalsAttribute.needsUpdate = true;
|
||||
} else {
|
||||
geometry.setAttribute("normal", new Float32BufferAttribute(normals, 3));
|
||||
}
|
||||
|
||||
geometry.userData = {
|
||||
vertexCount,
|
||||
faceCount,
|
||||
hash,
|
||||
};
|
||||
|
||||
return geometry;
|
||||
}
|
||||
|
||||
let meshes: Mesh[] = [];
|
||||
|
||||
|
||||
export function updateGeometries(inputs: Int32Array[], group: Group) {
|
||||
let totalVertices = 0;
|
||||
let totalFaces = 0;
|
||||
let newGeometries = [];
|
||||
|
||||
for (let i = 0; i < Math.max(meshes.length, inputs.length); i++) {
|
||||
let existingMesh = meshes[i];
|
||||
let input = inputs[i];
|
||||
|
||||
if (input) {
|
||||
if (input[0] !== 1) {
|
||||
continue
|
||||
}
|
||||
totalVertices += input[1];
|
||||
totalFaces += input[2];
|
||||
} else {
|
||||
if (existingMesh) {
|
||||
existingMesh.visible = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (existingMesh) {
|
||||
createGeometryFromEncodedData(input, existingMesh.geometry);
|
||||
} else {
|
||||
let geo = createGeometryFromEncodedData(input);
|
||||
const mesh = new Mesh(geo, material);
|
||||
meshes[i] = mesh;
|
||||
newGeometries.push(mesh);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for (let i = 0; i < newGeometries.length; i++) {
|
||||
group.add(newGeometries[i]);
|
||||
}
|
||||
|
||||
return { totalFaces, totalVertices };
|
||||
}
|
@ -128,9 +128,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
|
||||
|
||||
async execute(graph: Graph, settings: Record<string, unknown>) {
|
||||
|
||||
this.perf?.startRun();
|
||||
|
||||
let a0 = performance.now();
|
||||
this.perf?.addPoint("runtime");
|
||||
|
||||
let a = performance.now();
|
||||
|
||||
@ -250,18 +248,20 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
|
||||
// return the result of the parent of the output node
|
||||
const res = results[outputNode.id];
|
||||
|
||||
this.perf?.addPoint("runtime", performance.now() - a0);
|
||||
|
||||
this.perf?.stopRun();
|
||||
|
||||
if (this.cache) {
|
||||
this.cache.size = sortedNodes.length * 2;
|
||||
}
|
||||
|
||||
this.perf?.endPoint("runtime");
|
||||
|
||||
return res as unknown as Int32Array;
|
||||
|
||||
}
|
||||
|
||||
getPerformanceData() {
|
||||
return this.perf?.get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class MemoryRuntimeCache implements RuntimeCache {
|
||||
|
@ -93,6 +93,8 @@
|
||||
.content {
|
||||
background: var(--layer-1);
|
||||
position: relative;
|
||||
max-height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
|
@ -10,8 +10,10 @@ export const AppSettings = localStore("node-settings", {
|
||||
showIndices: false,
|
||||
showVertices: false,
|
||||
showPerformancePanel: false,
|
||||
showBenchmarkPanel: false,
|
||||
centerCamera: true,
|
||||
showStemLines: false,
|
||||
useWorker: true,
|
||||
amount: 5
|
||||
});
|
||||
|
||||
@ -69,6 +71,11 @@ export const AppSettingTypes = {
|
||||
label: "Wireframe",
|
||||
value: false,
|
||||
},
|
||||
useWorker: {
|
||||
type: "boolean",
|
||||
label: "Execute runtime in worker",
|
||||
value: true,
|
||||
},
|
||||
showIndices: {
|
||||
type: "boolean",
|
||||
label: "Show Indices",
|
||||
@ -79,6 +86,11 @@ export const AppSettingTypes = {
|
||||
label: "Show Performance Panel",
|
||||
value: false,
|
||||
},
|
||||
showBenchmarkPanel: {
|
||||
type: "boolean",
|
||||
label: "Show Benchmark Panel",
|
||||
value: false,
|
||||
},
|
||||
showVertices: {
|
||||
type: "boolean",
|
||||
label: "Show Vertices",
|
||||
@ -104,6 +116,18 @@ export const AppSettingTypes = {
|
||||
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'"
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,6 @@
|
||||
? filterInputs(node.tmp.type.inputs)
|
||||
: undefined;
|
||||
$: store = node ? createStore(node.props, nodeDefinition) : undefined;
|
||||
$: console.log(nodeDefinition, store);
|
||||
|
||||
let lastPropsHash = "";
|
||||
function updateNode() {
|
||||
|
88
app/src/lib/settings/panels/BenchmarkPanel.svelte
Normal file
88
app/src/lib/settings/panels/BenchmarkPanel.svelte
Normal file
@ -0,0 +1,88 @@
|
||||
<script lang="ts">
|
||||
import localStore from "$lib/helpers/localStore";
|
||||
import { Integer } from "@nodes/ui";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
export let run: () => Promise<any>;
|
||||
|
||||
let isRunning = false;
|
||||
let amount = localStore<number>("nodes.benchmark.samples", 500);
|
||||
let samples = 0;
|
||||
let warmUp = writable(0);
|
||||
let warmUpAmount = 10;
|
||||
let state = "";
|
||||
let result = "";
|
||||
|
||||
const copyContent = async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy: ", err);
|
||||
}
|
||||
};
|
||||
|
||||
async function benchmark() {
|
||||
if (isRunning) return;
|
||||
isRunning = true;
|
||||
|
||||
samples = 0;
|
||||
$warmUp = 0;
|
||||
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
|
||||
// warm up
|
||||
for (let i = 0; i < warmUpAmount; i++) {
|
||||
await run();
|
||||
$warmUp = i + 1;
|
||||
}
|
||||
|
||||
let results = [];
|
||||
|
||||
// perform run
|
||||
for (let i = 0; i < $amount; i++) {
|
||||
const a = performance.now();
|
||||
await run();
|
||||
samples = i;
|
||||
const b = performance.now();
|
||||
results.push(b - a);
|
||||
console.log(b - a);
|
||||
}
|
||||
result = results.join(" ");
|
||||
}
|
||||
</script>
|
||||
|
||||
{state}
|
||||
|
||||
<div class="wrapper" class:running={isRunning}>
|
||||
{#if isRunning}
|
||||
<p>WarmUp ({$warmUp}/{warmUpAmount})</p>
|
||||
<progress value={$warmUp} max={warmUpAmount}
|
||||
>{Math.floor(($warmUp / warmUpAmount) * 100)}%</progress
|
||||
>
|
||||
<p>Progress ({samples}/{$amount})</p>
|
||||
<progress value={samples} max={$amount}
|
||||
>{Math.floor((samples / $amount) * 100)}%</progress
|
||||
>
|
||||
|
||||
{#if result}
|
||||
<textarea readonly>{result}</textarea>
|
||||
<div>
|
||||
<button on:click={() => copyContent(result)}>Copy</button>
|
||||
<button on:click={() => (isRunning = false)}>reset</button>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<label for="bench-samples">Samples</label>
|
||||
<Integer id="bench-sample" bind:value={$amount} max={1000} />
|
||||
<button on:click={benchmark} disabled={isRunning}> start </button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
padding: 1em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
}
|
||||
</style>
|
53
app/src/lib/settings/panels/ExportSettings.svelte
Normal file
53
app/src/lib/settings/panels/ExportSettings.svelte
Normal file
@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import type { Scene } from "three";
|
||||
import { OBJExporter } from "three/addons/exporters/OBJExporter.js";
|
||||
import { GLTFExporter } from "three/addons/exporters/GLTFExporter.js";
|
||||
import FileSaver from "file-saver";
|
||||
|
||||
// Download
|
||||
const download = (
|
||||
data: string,
|
||||
name: string,
|
||||
mimetype: string,
|
||||
extension: string,
|
||||
) => {
|
||||
if (typeof data !== "string") data = JSON.stringify(data);
|
||||
const blob = new Blob([data], { type: mimetype + ";charset=utf-8" });
|
||||
FileSaver.saveAs(blob, name + "." + extension);
|
||||
};
|
||||
|
||||
// export const json = (data, name = 'default') => {
|
||||
// download(JSON.stringify(data), name, 'application/json', 'json');
|
||||
// };
|
||||
//
|
||||
// export const obj = (data, name = 'default') => {
|
||||
// };
|
||||
|
||||
export let scene: Scene;
|
||||
|
||||
function exportGltf() {
|
||||
const exporter = new GLTFExporter();
|
||||
exporter.parse(
|
||||
scene,
|
||||
(gltf) => {
|
||||
// download .gltf file
|
||||
download(gltf, "plant", "text/plain", "gltf");
|
||||
},
|
||||
(err) => {
|
||||
console.log(err);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function exportObj() {
|
||||
const exporter = new OBJExporter();
|
||||
const result = exporter.parse(scene);
|
||||
// download .obj file
|
||||
download(result, "plant", "text/plain", "obj");
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-2">
|
||||
<button on:click={exportObj}> export obj </button>
|
||||
<button on:click={exportGltf}> export gltf </button>
|
||||
</div>
|
@ -4,10 +4,11 @@
|
||||
|
||||
export let keymap: ReturnType<typeof createKeyMap>;
|
||||
const keys = keymap?.keys;
|
||||
export let title = "Keymap";
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<h3>Editor</h3>
|
||||
<h3>{title}</h3>
|
||||
|
||||
<section>
|
||||
{#each $keys as key}
|
||||
|
@ -7,12 +7,15 @@ const cache = new MemoryRuntimeCache();
|
||||
const nodeRegistry = new RemoteNodeRegistry("");
|
||||
const executor = new MemoryRuntimeExecutor(nodeRegistry, cache);
|
||||
|
||||
const performanceStore = createPerformanceStore();
|
||||
const performanceStore = createPerformanceStore("worker");
|
||||
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);
|
||||
performanceStore.startRun();
|
||||
let res = await executor.execute(graph, settings);
|
||||
performanceStore.stopRun();
|
||||
return res;
|
||||
}
|
||||
|
||||
export function getPerformanceData() {
|
||||
|
@ -10,7 +10,7 @@
|
||||
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 { 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";
|
||||
@ -20,15 +20,28 @@
|
||||
import GraphSettings from "$lib/settings/panels/GraphSettings.svelte";
|
||||
import NestedSettings from "$lib/settings/panels/NestedSettings.svelte";
|
||||
import { createPerformanceStore } from "$lib/performance";
|
||||
import type { Scene } from "three";
|
||||
import ExportSettings from "$lib/settings/panels/ExportSettings.svelte";
|
||||
import {
|
||||
MemoryRuntimeCache,
|
||||
MemoryRuntimeExecutor,
|
||||
} from "$lib/runtime-executor";
|
||||
import { fastHashString } from "@nodes/utils";
|
||||
import BenchmarkPanel from "$lib/settings/panels/BenchmarkPanel.svelte";
|
||||
|
||||
let performanceStore = createPerformanceStore("page");
|
||||
|
||||
const nodeRegistry = new RemoteNodeRegistry("");
|
||||
const workerRuntime = new WorkerRuntimeExecutor();
|
||||
const runtimeCache = new MemoryRuntimeCache();
|
||||
const memoryRuntime = new MemoryRuntimeExecutor(nodeRegistry, runtimeCache);
|
||||
memoryRuntime.perf = performanceStore;
|
||||
|
||||
let performanceStore = createPerformanceStore();
|
||||
$: runtime = $AppSettings.useWorker ? workerRuntime : memoryRuntime;
|
||||
|
||||
let activeNode: Node | undefined;
|
||||
|
||||
let graphResult: Int32Array;
|
||||
let scene: Scene;
|
||||
let updateViewerResult: (result: Int32Array) => void;
|
||||
|
||||
let graph = localStorage.getItem("graph")
|
||||
? JSON.parse(localStorage.getItem("graph")!)
|
||||
@ -36,11 +49,22 @@
|
||||
|
||||
let manager: GraphManager;
|
||||
let managerStatus: Writable<"loading" | "error" | "idle">;
|
||||
$: if (manager) {
|
||||
setContext("graphManager", manager);
|
||||
|
||||
async function randomGenerate() {
|
||||
const g = manager.serialize();
|
||||
const s = { ...$graphSettings, randomSeed: true };
|
||||
const res = await handleResult(g, s);
|
||||
return res;
|
||||
}
|
||||
|
||||
let keymap: ReturnType<typeof createKeyMap>;
|
||||
let applicationKeymap = createKeyMap([
|
||||
{
|
||||
key: "r",
|
||||
description: "Regenerate the plant model",
|
||||
callback: randomGenerate,
|
||||
},
|
||||
]);
|
||||
let graphSettings = writable<Record<string, any>>({});
|
||||
let graphSettingTypes = {};
|
||||
|
||||
@ -50,42 +74,59 @@
|
||||
| {
|
||||
graph: Graph;
|
||||
settings: Record<string, any>;
|
||||
hash: number;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
async function handleResult(_graph: Graph, _settings: Record<string, any>) {
|
||||
if (!_settings) return;
|
||||
const inputHash = fastHashString(
|
||||
JSON.stringify(_graph) + JSON.stringify(_settings),
|
||||
);
|
||||
if (isWorking) {
|
||||
unfinished = {
|
||||
graph: _graph,
|
||||
settings: _settings,
|
||||
hash: inputHash,
|
||||
};
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
isWorking = true;
|
||||
performanceStore.startRun();
|
||||
try {
|
||||
let a = performance.now();
|
||||
graphResult = await workerRuntime.execute(_graph, _settings);
|
||||
const graphResult = await runtime.execute(_graph, _settings);
|
||||
let b = performance.now();
|
||||
let perfData = await workerRuntime.getPerformanceData();
|
||||
let lastRun = perfData.at(-1);
|
||||
if (lastRun) {
|
||||
lastRun["worker-transfer"] = [b - a - lastRun.runtime[0]];
|
||||
performanceStore.mergeData(lastRun);
|
||||
|
||||
if ($AppSettings.useWorker) {
|
||||
let perfData = await runtime.getPerformanceData();
|
||||
let lastRun = perfData?.at(-1);
|
||||
if (lastRun?.total) {
|
||||
lastRun.runtime = lastRun.total;
|
||||
delete lastRun.total;
|
||||
performanceStore.mergeData(lastRun);
|
||||
performanceStore.addPoint(
|
||||
"worker-transfer",
|
||||
b - a - lastRun.runtime[0],
|
||||
);
|
||||
}
|
||||
}
|
||||
isWorking = false;
|
||||
|
||||
updateViewerResult(graphResult);
|
||||
} catch (error) {
|
||||
console.log("errors", error);
|
||||
} finally {
|
||||
performanceStore.stopRun();
|
||||
isWorking = false;
|
||||
}
|
||||
|
||||
performanceStore.stopRun();
|
||||
performanceStore.startRun();
|
||||
|
||||
if (unfinished) {
|
||||
if (unfinished && unfinished.hash === inputHash) {
|
||||
let d = unfinished;
|
||||
unfinished = undefined;
|
||||
handleResult(d.graph, d.settings);
|
||||
await handleResult(d.graph, d.settings);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$: if (AppSettings) {
|
||||
@ -97,6 +138,18 @@
|
||||
AppSettingTypes.debug.stressTest.loadTree.callback = () => {
|
||||
graph = templates.tree($AppSettings.amount);
|
||||
};
|
||||
//@ts-ignore
|
||||
AppSettingTypes.debug.stressTest.lottaFaces.callback = () => {
|
||||
graph = templates.lottaFaces;
|
||||
};
|
||||
//@ts-ignore
|
||||
AppSettingTypes.debug.stressTest.lottaNodes.callback = () => {
|
||||
graph = templates.lottaNodes;
|
||||
};
|
||||
//@ts-ignore
|
||||
AppSettingTypes.debug.stressTest.lottaNodesAndFaces.callback = () => {
|
||||
graph = templates.lottaNodesAndFaces;
|
||||
};
|
||||
}
|
||||
|
||||
function handleSave(event: CustomEvent<Graph>) {
|
||||
@ -104,13 +157,15 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:document on:keydown={applicationKeymap.handleKeyboardEvent} />
|
||||
<div class="wrapper manager-{$managerStatus}">
|
||||
<header></header>
|
||||
<Grid.Row>
|
||||
<Grid.Cell>
|
||||
<Viewer
|
||||
result={graphResult}
|
||||
perf={performanceStore}
|
||||
bind:scene
|
||||
bind:update={updateViewerResult}
|
||||
centerCamera={$AppSettings.centerCamera}
|
||||
/>
|
||||
</Grid.Cell>
|
||||
@ -143,10 +198,14 @@
|
||||
title="Keyboard Shortcuts"
|
||||
icon="i-tabler-keyboard"
|
||||
>
|
||||
<Keymap title="Application" keymap={applicationKeymap} />
|
||||
{#if keymap}
|
||||
<Keymap {keymap} />
|
||||
<Keymap title="Node-Editor" {keymap} />
|
||||
{/if}
|
||||
</Panel>
|
||||
<Panel id="exports" title="Exporter" icon="i-tabler-package-export">
|
||||
<ExportSettings {scene} />
|
||||
</Panel>
|
||||
<Panel
|
||||
id="node-store"
|
||||
classes="text-green-400"
|
||||
@ -166,6 +225,15 @@
|
||||
<PerformanceViewer data={$performanceStore} />
|
||||
{/if}
|
||||
</Panel>
|
||||
<Panel
|
||||
id="benchmark"
|
||||
title="Benchmark"
|
||||
classes="text-red-400"
|
||||
hidden={!$AppSettings.showBenchmarkPanel}
|
||||
icon="i-tabler-graph"
|
||||
>
|
||||
<BenchmarkPanel run={randomGenerate} />
|
||||
</Panel>
|
||||
<Panel
|
||||
id="graph-settings"
|
||||
title="Graph Settings"
|
||||
@ -190,8 +258,6 @@
|
||||
</Grid.Row>
|
||||
</div>
|
||||
|
||||
<span class="font-red" />
|
||||
|
||||
<style>
|
||||
header {
|
||||
/* border-bottom: solid thin var(--outline); */
|
||||
|
Reference in New Issue
Block a user