feat: some tweaks
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m41s

This commit is contained in:
max_richter 2024-05-03 01:33:55 +02:00
parent 4c235fe24a
commit cf5b36490f
8 changed files with 235 additions and 211 deletions

View File

@ -11,6 +11,8 @@ const logger = createLogger("graph-manager");
logger.mute(); logger.mute();
const clone = "structuredClone" in self ? self.structuredClone : (args: any) => JSON.parse(JSON.stringify(args));
function areSocketsCompatible(output: string | undefined, inputs: string | string[] | undefined) { function areSocketsCompatible(output: string | undefined, inputs: string | string[] | undefined) {
if (Array.isArray(inputs) && output) { if (Array.isArray(inputs) && output) {
return inputs.includes(output); return inputs.includes(output);
@ -67,7 +69,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
const serialized = { id: this.graph.id, settings: this.settings, nodes, edges }; const serialized = { id: this.graph.id, settings: this.settings, nodes, edges };
logger.groupEnd(); logger.groupEnd();
return serialized; return clone(serialized);
} }

View File

@ -155,3 +155,55 @@ export function humanizeDuration(durationInMilliseconds: number) {
return durationString.trim(); return durationString.trim();
} }
export function debounceAsyncFunction<T extends any[], R>(func: (...args: T) => Promise<R>): (...args: T) => Promise<R> {
let currentPromise: Promise<R> | null = null;
let nextArgs: T | null = null;
let resolveNext: ((result: R) => void) | null = null;
const debouncedFunction = async (...args: T): Promise<R> => {
if (currentPromise) {
// Store the latest arguments and create a new promise to resolve them later
nextArgs = args;
return new Promise<R>((resolve) => {
resolveNext = resolve;
});
} else {
// Execute the function immediately
try {
currentPromise = func(...args);
const result = await currentPromise;
return result;
} finally {
currentPromise = null;
// If there are stored arguments, call the function again with the latest arguments
if (nextArgs) {
const argsToUse = nextArgs;
const resolver = resolveNext;
nextArgs = null;
resolveNext = null;
resolver!(await debouncedFunction(...argsToUse));
}
}
}
};
return debouncedFunction;
}
export function withArgsChangeOnly<T extends any[], R>(func: (...args: T) => R): (...args: T) => R {
let lastArgs: T | undefined = undefined;
let lastResult: R;
return (...args: T): R => {
// Check if arguments are the same as last call
if (lastArgs && args.length === lastArgs.length && args.every((val, index) => val === lastArgs?.[index])) {
return lastResult; // Return cached result if arguments haven't changed
}
// Call the function with new arguments
lastResult = func(...args);
lastArgs = args; // Update stored arguments
return lastResult; // Return new result
};
}

View File

@ -4,8 +4,6 @@
export let node: NodeDefinition; export let node: NodeDefinition;
$: console.log({ node });
let dragging = false; let dragging = false;
let nodeData = { let nodeData = {

View File

@ -176,7 +176,7 @@
} }
</script> </script>
{#key $activeType} {#key $activeType && data}
{#if $activeType === "cache-hit"} {#if $activeType === "cache-hit"}
<Monitor <Monitor
title="Cache Hits" title="Cache Hits"
@ -191,121 +191,123 @@
points={constructPoints($activeType)} points={constructPoints($activeType)}
/> />
{/if} {/if}
{/key}
<div class="p-4 performance-tabler"> <div class="p-4 performance-tabler">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Checkbox id="show-total" bind:value={showAverage} /> <Checkbox id="show-total" bind:value={showAverage} />
<label for="show-total">Show Average</label> <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> </div>
{/key}
{#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> <style>
h3 { h3 {

View File

@ -1,37 +1,36 @@
<script lang="ts"> <script lang="ts">
import type { Scene } from "three"; import type { Group } from "three";
import { OBJExporter } from "three/addons/exporters/OBJExporter.js"; import type { OBJExporter } from "three/addons/exporters/OBJExporter.js";
import { GLTFExporter } from "three/addons/exporters/GLTFExporter.js"; import type { GLTFExporter } from "three/addons/exporters/GLTFExporter.js";
import FileSaver from "file-saver"; import FileSaver from "file-saver";
// Download // Download
const download = ( const download = (
data: string, data: ArrayBuffer | string,
name: string, name: string,
mimetype: string, mimetype: string,
extension: string, extension: string,
) => { ) => {
if (typeof data !== "string") data = JSON.stringify(data);
const blob = new Blob([data], { type: mimetype + ";charset=utf-8" }); const blob = new Blob([data], { type: mimetype + ";charset=utf-8" });
FileSaver.saveAs(blob, name + "." + extension); FileSaver.saveAs(blob, name + "." + extension);
}; };
// export const json = (data, name = 'default') => { export let scene: Group;
// download(JSON.stringify(data), name, 'application/json', 'json');
// };
//
// export const obj = (data, name = 'default') => {
// };
export let scene: Scene; let gltfExporter: GLTFExporter;
async function exportGltf() {
const exporter =
gltfExporter ||
(await import("three/addons/exporters/GLTFExporter.js").then((m) => {
gltfExporter = new m.GLTFExporter();
return gltfExporter;
}));
function exportGltf() {
const exporter = new GLTFExporter();
exporter.parse( exporter.parse(
scene, scene,
(gltf) => { (gltf) => {
// download .gltf file // download .gltf file
download(gltf, "plant", "text/plain", "gltf"); download(gltf as ArrayBuffer, "plant", "text/plain", "gltf");
}, },
(err) => { (err) => {
console.log(err); console.log(err);
@ -39,8 +38,15 @@
); );
} }
function exportObj() { let objExporter: OBJExporter;
const exporter = new OBJExporter();
async function exportObj() {
const exporter =
objExporter ||
(await import("three/addons/exporters/OBJExporter.js").then((m) => {
objExporter = new m.OBJExporter();
return objExporter;
}));
const result = exporter.parse(scene); const result = exporter.parse(scene);
// download .obj file // download .obj file
download(result, "plant", "text/plain", "obj"); download(result, "plant", "text/plain", "obj");

View File

@ -19,7 +19,7 @@
import GraphSettings from "$lib/settings/panels/GraphSettings.svelte"; import GraphSettings from "$lib/settings/panels/GraphSettings.svelte";
import NestedSettings from "$lib/settings/panels/NestedSettings.svelte"; import NestedSettings from "$lib/settings/panels/NestedSettings.svelte";
import { createPerformanceStore } from "$lib/performance"; import { createPerformanceStore } from "$lib/performance";
import type { Scene } from "three"; import type { Group, Scene } from "three";
import ExportSettings from "$lib/settings/panels/ExportSettings.svelte"; import ExportSettings from "$lib/settings/panels/ExportSettings.svelte";
import { import {
MemoryRuntimeCache, MemoryRuntimeCache,
@ -28,6 +28,7 @@
import { IndexDBCache } from "$lib/node-registry-cache"; import { IndexDBCache } from "$lib/node-registry-cache";
import { decodeNestedArray, fastHashString } from "@nodes/utils"; import { decodeNestedArray, fastHashString } from "@nodes/utils";
import BenchmarkPanel from "$lib/settings/panels/BenchmarkPanel.svelte"; import BenchmarkPanel from "$lib/settings/panels/BenchmarkPanel.svelte";
import { debounceAsyncFunction, withArgsChangeOnly } from "$lib/helpers";
let performanceStore = createPerformanceStore("page"); let performanceStore = createPerformanceStore("page");
@ -53,7 +54,7 @@
$: runtime = $AppSettings.useWorker ? workerRuntime : memoryRuntime; $: runtime = $AppSettings.useWorker ? workerRuntime : memoryRuntime;
let activeNode: Node | undefined; let activeNode: Node | undefined;
let scene: Scene; let scene: Group;
let updateViewerResult: (result: Int32Array) => void; let updateViewerResult: (result: Int32Array) => void;
let graph = localStorage.getItem("graph") let graph = localStorage.getItem("graph")
@ -69,8 +70,7 @@
async function randomGenerate() { async function randomGenerate() {
const g = manager.serialize(); const g = manager.serialize();
const s = { ...$graphSettings, randomSeed: true }; const s = { ...$graphSettings, randomSeed: true };
const res = await handleResult(g, s); await handleUpdate(g, s);
return res;
} }
let keymap: ReturnType<typeof createKeyMap>; let keymap: ReturnType<typeof createKeyMap>;
@ -84,71 +84,36 @@
let graphSettings = writable<Record<string, any>>({}); let graphSettings = writable<Record<string, any>>({});
let graphSettingTypes = {}; let graphSettingTypes = {};
let isWorking = false; const handleUpdate = debounceAsyncFunction(
async (g: Graph, s: Record<string, any>) => {
performanceStore.startRun();
try {
let a = performance.now();
const graphResult = await runtime.execute(g, s);
let b = performance.now();
let unfinished: if ($AppSettings.useWorker) {
| { let perfData = await runtime.getPerformanceData();
graph: Graph; let lastRun = perfData?.at(-1);
settings: Record<string, any>; if (lastRun?.total) {
hash: number; lastRun.runtime = lastRun.total;
} delete lastRun.total;
| undefined; performanceStore.mergeData(lastRun);
performanceStore.addPoint(
async function handleResult(_graph: Graph, _settings: Record<string, any>) { "worker-transfer",
if (!_settings) return; b - a - lastRun.runtime[0],
if ($managerStatus !== "idle") return; );
const inputHash = fastHashString( }
JSON.stringify(_graph) + JSON.stringify(_settings),
);
if (isWorking) {
unfinished = {
graph: _graph,
settings: _settings,
hash: inputHash,
};
return false;
}
isWorking = true;
performanceStore.startRun();
try {
let a = performance.now();
const graphResult = await runtime.execute(_graph, _settings);
let b = performance.now();
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],
);
} }
updateViewerResult(graphResult);
} catch (error) {
console.log("errors", error);
} finally {
performanceStore.stopRun();
} }
},
updateViewerResult(graphResult); );
} catch (error) {
console.log("errors", error);
} finally {
performanceStore.stopRun();
isWorking = false;
}
if (unfinished && unfinished.hash === inputHash) {
let d = unfinished;
unfinished = undefined;
await handleResult(d.graph, d.settings);
}
return true;
}
$: if ($managerStatus === "idle") {
handleResult(manager.serialize(), $graphSettings);
}
$: if (AppSettings) { $: if (AppSettings) {
//@ts-ignore //@ts-ignore
@ -203,7 +168,7 @@
bind:showHelp={$AppSettings.showHelp} bind:showHelp={$AppSettings.showHelp}
bind:settings={graphSettings} bind:settings={graphSettings}
bind:settingTypes={graphSettingTypes} bind:settingTypes={graphSettingTypes}
on:result={(ev) => handleResult(ev.detail, $graphSettings)} on:result={(ev) => handleUpdate(ev.detail, $graphSettings)}
on:save={handleSave} on:save={handleSave}
/> />
<Settings> <Settings>

View File

@ -17,13 +17,6 @@
"options": ["x", "y", "z"], "options": ["x", "y", "z"],
"description": "Along which axis should we rotate?" "description": "Along which axis should we rotate?"
}, },
"spread": {
"type": "boolean",
"internal": true,
"hidden": true,
"value": true,
"description": "If multiple objects are connected, should we rotate them as one or spread them?"
},
"angle": { "angle": {
"type": "float", "type": "float",
"min": 0, "min": 0,
@ -31,6 +24,13 @@
"step": 0.05, "step": 0.05,
"value": 0, "value": 0,
"description": "Rotation angle" "description": "Rotation angle"
},
"spread": {
"type": "boolean",
"internal": true,
"hidden": true,
"value": true,
"description": "If multiple objects are connected, should we rotate them as one or spread them?"
} }
} }
} }

View File

@ -18,8 +18,7 @@ pub fn execute(input: &[i32]) -> Vec<i32> {
let plants = split_args(args[0]); let plants = split_args(args[0]);
let axis = evaluate_int(args[1]); // 0 =x, 1 = y, 2 = z let axis = evaluate_int(args[1]); // 0 =x, 1 = y, 2 = z
let spread = evaluate_int(args[2]); let spread = evaluate_int(args[3]);
let angle = evaluate_float(args[3]);
let output: Vec<Vec<i32>> = plants let output: Vec<Vec<i32>> = plants
.iter() .iter()
@ -34,7 +33,7 @@ pub fn execute(input: &[i32]) -> Vec<i32> {
let path = wrap_path_mut(&mut path_data); let path = wrap_path_mut(&mut path_data);
let length = path.get_length() as f64; let angle = evaluate_float(args[2]);
let origin = [path.points[0], path.points[1], path.points[2]]; let origin = [path.points[0], path.points[1], path.points[2]];