feat: add benchmark settings panel
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m59s

This commit is contained in:
max_richter 2024-05-01 23:05:04 +02:00
parent 8bf2958e1d
commit d9afec5bf6
39 changed files with 1253 additions and 741 deletions

View File

@ -18,6 +18,7 @@
"@types/three": "^0.164.0", "@types/three": "^0.164.0",
"@unocss/reset": "^0.59.4", "@unocss/reset": "^0.59.4",
"comlink": "^4.4.1", "comlink": "^4.4.1",
"file-saver": "^2.0.5",
"jsondiffpatch": "^0.6.0", "jsondiffpatch": "^0.6.0",
"three": "^0.164.1" "three": "^0.164.1"
}, },
@ -27,6 +28,7 @@
"@sveltejs/adapter-static": "^3.0.1", "@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/vite-plugin-svelte": "next", "@sveltejs/vite-plugin-svelte": "next",
"@tsconfig/svelte": "^5.0.4", "@tsconfig/svelte": "^5.0.4",
"@types/file-saver": "^2.0.7",
"@unocss/preset-icons": "^0.59.4", "@unocss/preset-icons": "^0.59.4",
"svelte": "5.0.0-next.118", "svelte": "5.0.0-next.118",
"svelte-check": "^3.7.0", "svelte-check": "^3.7.0",

View File

@ -12,7 +12,7 @@
import Camera from "../Camera.svelte"; import Camera from "../Camera.svelte";
import GraphView from "./GraphView.svelte"; import GraphView from "./GraphView.svelte";
import type { Node, NodeId, Node as NodeType, Socket } from "@nodes/types"; 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 FloatingEdge from "../edges/FloatingEdge.svelte";
import { import {
activeNodeId, activeNodeId,
@ -28,6 +28,7 @@
import { createWasmWrapper } from "@nodes/utils"; import { createWasmWrapper } from "@nodes/utils";
import HelpView from "../HelpView.svelte"; import HelpView from "../HelpView.svelte";
import FileSaver from "file-saver";
export let manager: GraphManager; export let manager: GraphManager;
@ -612,6 +613,7 @@
keymap.addShortcut({ keymap.addShortcut({
key: "a", key: "a",
ctrl: true, ctrl: true,
preventDefault: true,
description: "Select all nodes", description: "Select all nodes",
callback: () => { callback: () => {
if (!isBodyFocused()) return; if (!isBodyFocused()) return;
@ -637,7 +639,6 @@
ctrl: true, ctrl: true,
description: "Redo", description: "Redo",
callback: () => { callback: () => {
if (!isBodyFocused()) return;
manager.redo(); manager.redo();
for (const node of $nodes.values()) { for (const node of $nodes.values()) {
updateNodePosition(node); 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({ keymap.addShortcut({
key: ["Delete", "Backspace", "x"], key: ["Delete", "Backspace", "x"],
description: "Delete selected nodes", description: "Delete selected nodes",
@ -833,7 +848,9 @@
}); });
}); });
} else if (event.dataTransfer.files.length) { } else if (event.dataTransfer.files.length) {
const files = event.dataTransfer.files; const file = event.dataTransfer.files[0];
if (file.type === "application/wasm") {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => { reader.onload = (e) => {
const buffer = e.target?.result as Buffer; const buffer = e.target?.result as Buffer;
@ -841,9 +858,21 @@
const wrapper = createWasmWrapper(buffer); const wrapper = createWasmWrapper(buffer);
const definition = wrapper.get_definition(); const definition = wrapper.get_definition();
const res = NodeDefinitionSchema.parse(definition); const res = NodeDefinitionSchema.parse(definition);
console.log(res);
} }
}; };
reader.readAsArrayBuffer(files[0]); 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 <input
type="file" type="file"
accept="application/wasm" accept="application/wasm,application/json"
id="drop-zone" id="drop-zone"
disabled={!isDragging} disabled={!isDragging}
on:dragend={handleDragEnd} on:dragend={handleDragEnd}
on:dragleave={handleDragEnd} on:dragleave={handleDragEnd}
/> />
<label for="drop-zone" /> <label for="drop-zone"></label>
{#if showHelp} {#if showHelp}
<HelpView registry={manager.registry} /> <HelpView registry={manager.registry} />

View File

@ -20,7 +20,7 @@ export function grid(width: number, height: number) {
visible: false, visible: false,
}, },
position: [x * 30, y * 40], 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", type: i == 0 ? "max/plantarium/float" : "max/plantarium/math",
}); });

View File

@ -1,3 +1,7 @@
export { grid } from "./grid"; export { grid } from "./grid";
export { tree } from "./tree"; export { tree } from "./tree";
export { plant } from "./plant"; 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";

View 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"]]}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,7 @@ type Shortcut = {
shift?: boolean, shift?: boolean,
ctrl?: boolean, ctrl?: boolean,
alt?: boolean, alt?: boolean,
preventDefault?: boolean,
description?: string, description?: string,
callback: (event: KeyboardEvent) => void callback: (event: KeyboardEvent) => void
} }
@ -17,8 +18,11 @@ export function createKeyMap(keys: Shortcut[]) {
const store = writable(new Map(keys.map(k => [getShortcutId(k), k]))); const store = writable(new Map(keys.map(k => [getShortcutId(k), k])));
return { return {
handleKeyboardEvent: (event: KeyboardEvent) => { handleKeyboardEvent: (event: KeyboardEvent) => {
const activeElement = document.activeElement as HTMLElement;
if (activeElement?.tagName === "INPUT" || activeElement?.tagName === "TEXTAREA") return;
const key = [...get(store).values()].find(k => { const key = [...get(store).values()].find(k => {
if (Array.isArray(k.key) ? !k.key.includes(event.key) : k.key !== event.key) return false; if (Array.isArray(k.key) ? !k.key.includes(event.key) : k.key !== event.key) return false;
if ("shift" in k && k.shift !== event.shiftKey) return false; if ("shift" in k && k.shift !== event.shiftKey) return false;
@ -26,6 +30,7 @@ export function createKeyMap(keys: Shortcut[]) {
if ("alt" in k && k.alt !== event.altKey) return false; if ("alt" in k && k.alt !== event.altKey) return false;
return true; return true;
}); });
if (key && key.preventDefault) event.preventDefault();
key?.callback(event); key?.callback(event);
}, },
addShortcut: (shortcut: Shortcut) => { addShortcut: (shortcut: Shortcut) => {

View 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>

View File

@ -4,6 +4,7 @@
import { Checkbox } from "@nodes/ui"; import { Checkbox } from "@nodes/ui";
import localStore from "$lib/helpers/localStore"; import localStore from "$lib/helpers/localStore";
import { type PerformanceData } from "./store"; import { type PerformanceData } from "./store";
import BarSplit from "./BarSplit.svelte";
export let data: PerformanceData; export let data: PerformanceData;
@ -42,7 +43,7 @@
} }
function getLast(key: string) { function getLast(key: string) {
return data.at(-1)?.[key][0] || 0; return data.at(-1)?.[key]?.[0] || 0;
} }
function getLasts() { function getLasts() {
@ -53,13 +54,13 @@
if (onlyLast) { if (onlyLast) {
return ( return (
getLast("runtime") + getLast("runtime") +
getLast("create-geometries") + getLast("update-geometries") +
getLast("worker-transfer") getLast("worker-transfer")
); );
} }
return ( return (
getAverage("runtime") + getAverage("runtime") +
getAverage("create-geometries") + getAverage("update-geometries") +
getAverage("worker-transfer") getAverage("worker-transfer")
); );
} }
@ -73,7 +74,7 @@
const viewerKeys = [ const viewerKeys = [
"total-vertices", "total-vertices",
"total-faces", "total-faces",
"create-geometries", "update-geometries",
"split-result", "split-result",
]; ];
@ -116,8 +117,8 @@
return data.map((run) => { return data.map((run) => {
return ( return (
run["runtime"].reduce((acc, v) => acc + v, 0) + run["runtime"].reduce((acc, v) => acc + v, 0) +
run["create-geometries"].reduce((acc, v) => acc + v, 0) + run["update-geometries"].reduce((acc, v) => acc + v, 0) +
run["worker-transfer"].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 data.map((run) => {
return ( return (
run["runtime"][0] + run["runtime"][0] +
run["create-geometries"][0] + run["update-geometries"][0] +
run["worker-transfer"][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) { function getTitle(t: string) {
if (t.includes("/")) { if (t.includes("/")) {
return `Node ${t.split("/").slice(-1).join("/")}`; return `Node ${t.split("/").slice(-1).join("/")}`;
@ -159,7 +176,7 @@
} }
</script> </script>
{#key $activeType && data} {#key $activeType}
{#if $activeType === "cache-hit"} {#if $activeType === "cache-hit"}
<Monitor <Monitor
title="Cache Hits" title="Cache Hits"
@ -174,14 +191,22 @@
points={constructPoints($activeType)} points={constructPoints($activeType)}
/> />
{/if} {/if}
{/key}
<div class="p-4"> <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> </div>
{#if data.length !== 0} {#if data.length !== 0}
<BarSplit
labels={["worker-transfer", "runtime", "update-geometries"]}
values={getSplitValues()}
/>
<h3>General</h3> <h3>General</h3>
<table> <table>
<tbody> <tbody>
<tr> <tr>
@ -193,9 +218,7 @@
on:click={() => ($activeType = "total")} on:click={() => ($activeType = "total")}
> >
total<span total<span
>({Math.floor( >({Math.floor(1000 / getTotalPerformance(showAverage))}fps)</span
1000 / getTotalPerformance(showAverage),
)}fps)</span
> >
</td> </td>
</tr> </tr>
@ -283,7 +306,6 @@
<p>No runs available</p> <p>No runs available</p>
{/if} {/if}
</div> </div>
{/key}
<style> <style>
h3 { h3 {
@ -296,6 +318,9 @@
opacity: 0.3; opacity: 0.3;
margin-left: 4px; margin-left: 4px;
} }
table {
margin-bottom: 70px;
}
td { td {
padding-right: 10px; padding-right: 10px;
padding-block: 5px; padding-block: 5px;

View File

@ -6,15 +6,18 @@ export interface PerformanceStore extends Readable<PerformanceData> {
startRun(): void; startRun(): void;
stopRun(): void; stopRun(): void;
addPoint(name: string, value?: number): void; addPoint(name: string, value?: number): void;
endPoint(name?: string): void;
mergeData(data: PerformanceData[number]): void; mergeData(data: PerformanceData[number]): void;
get: () => PerformanceData; get: () => PerformanceData;
} }
export function createPerformanceStore(): PerformanceStore { export function createPerformanceStore(id?: string): PerformanceStore {
let data: PerformanceData = []; let data: PerformanceData = [];
let currentRun: Record<string, number[]> | undefined; let currentRun: Record<string, number[]> | undefined;
let temp: Record<string, number> | undefined;
let lastPoint: string | undefined;
let set: (v: PerformanceData) => void; let set: (v: PerformanceData) => void;
@ -23,23 +26,37 @@ export function createPerformanceStore(): PerformanceStore {
}); });
function startRun() { function startRun() {
if (currentRun) return;
currentRun = {}; currentRun = {};
lastPoint = undefined;
temp = {
start: performance.now()
}
} }
function stopRun() { function stopRun() {
if (currentRun) { if (currentRun && temp) {
currentRun["total"] = [performance.now() - temp.start];
data.push(currentRun); data.push(currentRun);
data = data.slice(-100); data = data.slice(-100);
currentRun = undefined; currentRun = undefined;
temp = undefined;
if (set) set(data); if (set) set(data);
} }
} }
function addPoint(name: string, value: number) { function addPoint(name: string, value?: number) {
if (!currentRun) return; if (!currentRun) return;
if (value === undefined) {
if (temp) {
lastPoint = name;
temp[name] = performance.now();
}
} else {
currentRun[name] = currentRun[name] || []; currentRun[name] = currentRun[name] || [];
currentRun[name].push(value); currentRun[name].push(value);
} }
}
function get() { function get() {
return data; return data;
@ -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 { return {
subscribe, subscribe,
startRun, startRun,
stopRun, stopRun,
addPoint, addPoint,
endPoint,
mergeData, mergeData,
get get
} }

View File

@ -1,11 +1,6 @@
<script lang="ts"> <script lang="ts">
import { T } from "@threlte/core"; import { T, useThrelte } from "@threlte/core";
import { import { MeshLineGeometry, MeshLineMaterial, Text } from "@threlte/extras";
MeshLineGeometry,
MeshLineMaterial,
Text,
useTexture,
} from "@threlte/extras";
import { import {
type Group, type Group,
type BufferGeometry, type BufferGeometry,
@ -16,15 +11,20 @@
import { AppSettings } from "../settings/app-settings"; import { AppSettings } from "../settings/app-settings";
import Camera from "./Camera.svelte"; import Camera from "./Camera.svelte";
const d = useThrelte();
export const invalidate = d.invalidate;
export let geometries: BufferGeometry[]; export let geometries: BufferGeometry[];
export let lines: Vector3[][]; export let lines: Vector3[][];
export let scene;
let geos: Group; let geos: Group;
$: scene = geos;
export let geoGroup: Group;
export let centerCamera: boolean = true; export let centerCamera: boolean = true;
let center = new Vector3(0, 4, 0); let center = new Vector3(0, 4, 0);
const matcap = useTexture("/matcap_green.jpg");
function getPosition(geo: BufferGeometry, i: number) { function getPosition(geo: BufferGeometry, i: number) {
return [ return [
geo.attributes.position.array[i], geo.attributes.position.array[i],
@ -48,9 +48,6 @@
<T.GridHelper args={[20, 20]} /> <T.GridHelper args={[20, 20]} />
{/if} {/if}
<T.DirectionalLight position={[0, 10, 10]} />
<T.AmbientLight intensity={2} />
<T.Group bind:ref={geos}> <T.Group bind:ref={geos}>
{#each geometries as geo} {#each geometries as geo}
{#if $AppSettings.showIndices} {#if $AppSettings.showIndices}
@ -67,15 +64,9 @@
<T.PointsMaterial size={0.25} /> <T.PointsMaterial size={0.25} />
</T.Points> </T.Points>
{/if} {/if}
{#await matcap then value}
<T.Mesh geometry={geo}>
<T.MeshMatcapMaterial
matcap={value}
wireframe={$AppSettings.wireframe}
/>
</T.Mesh>
{/await}
{/each} {/each}
<T.Group bind:ref={geoGroup}></T.Group>
</T.Group> </T.Group>
{#if $AppSettings.showStemLines && lines} {#if $AppSettings.showStemLines && lines}

View File

@ -1,155 +1,23 @@
<script lang="ts"> <script lang="ts">
import { Canvas } from "@threlte/core"; import { Canvas } from "@threlte/core";
import Scene from "./Scene.svelte"; import Scene from "./Scene.svelte";
import { import { BufferGeometry, Group, Vector3 } from "three";
BufferAttribute,
BufferGeometry, import { updateGeometries } from "./updateGeometries";
Float32BufferAttribute, import { decodeFloat, splitNestedArray } from "@nodes/utils";
Vector3,
} from "three";
import { decodeFloat, fastHashArrayBuffer } from "@nodes/utils";
import type { PerformanceStore } from "$lib/performance"; import type { PerformanceStore } from "$lib/performance";
import { AppSettings } from "$lib/settings/app-settings"; import { AppSettings } from "$lib/settings/app-settings";
export let centerCamera: boolean = true; export let centerCamera: boolean = true;
export let perf: PerformanceStore; export let perf: PerformanceStore;
export let scene: Group;
let geoGroup: Group;
let geometries: BufferGeometry[] = []; let geometries: BufferGeometry[] = [];
let lines: Vector3[][] = []; let lines: Vector3[][] = [];
function fastArrayHash(arr: ArrayBuffer) { let invalidate: () => void;
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;
}
function createLineGeometryFromEncodedData(encodedData: Int32Array) { function createLineGeometryFromEncodedData(encodedData: Int32Array) {
const positions: Vector3[] = []; const positions: Vector3[] = [];
@ -166,16 +34,13 @@
return positions; return positions;
} }
export let result: Int32Array; export const update = function update(result: Int32Array) {
$: result && updateGeometries(); perf?.addPoint("split-result");
function updateGeometries() { const inputs = splitNestedArray(result);
let a = performance.now(); perf?.endPoint();
const inputs = parse_args(result);
let b = performance.now();
perf?.addPoint("split-result", b - a);
if ($AppSettings.showStemLines) { if ($AppSettings.showStemLines) {
a = performance.now(); perf?.addPoint("create-lines");
lines = inputs lines = inputs
.map((input) => { .map((input) => {
if (input[0] === 0) { if (input[0] === 0) {
@ -183,31 +48,27 @@
} }
}) })
.filter(Boolean) as Vector3[][]; .filter(Boolean) as Vector3[][];
b = performance.now(); perf.endPoint();
perf?.addPoint("create-lines", b - a);
} }
let totalVertices = 0; perf?.addPoint("update-geometries");
let totalFaces = 0;
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-vertices", totalVertices);
perf?.addPoint("total-faces", totalFaces); perf?.addPoint("total-faces", totalFaces);
} invalidate();
};
</script> </script>
<Canvas> <Canvas>
<Scene {geometries} {lines} {centerCamera} /> <Scene
bind:scene
bind:geoGroup
bind:invalidate
{geometries}
{lines}
{centerCamera}
/>
</Canvas> </Canvas>

View 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 };
}

View File

@ -128,9 +128,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
async execute(graph: Graph, settings: Record<string, unknown>) { async execute(graph: Graph, settings: Record<string, unknown>) {
this.perf?.startRun(); this.perf?.addPoint("runtime");
let a0 = performance.now();
let a = performance.now(); let a = performance.now();
@ -250,18 +248,20 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
// return the result of the parent of the output node // return the result of the parent of the output node
const res = results[outputNode.id]; const res = results[outputNode.id];
this.perf?.addPoint("runtime", performance.now() - a0);
this.perf?.stopRun();
if (this.cache) { if (this.cache) {
this.cache.size = sortedNodes.length * 2; this.cache.size = sortedNodes.length * 2;
} }
this.perf?.endPoint("runtime");
return res as unknown as Int32Array; return res as unknown as Int32Array;
} }
getPerformanceData() {
return this.perf?.get();
}
} }
export class MemoryRuntimeCache implements RuntimeCache { export class MemoryRuntimeCache implements RuntimeCache {

View File

@ -93,6 +93,8 @@
.content { .content {
background: var(--layer-1); background: var(--layer-1);
position: relative; position: relative;
max-height: 100vh;
overflow-y: auto;
} }
.tabs { .tabs {

View File

@ -10,8 +10,10 @@ export const AppSettings = localStore("node-settings", {
showIndices: false, showIndices: false,
showVertices: false, showVertices: false,
showPerformancePanel: false, showPerformancePanel: false,
showBenchmarkPanel: false,
centerCamera: true, centerCamera: true,
showStemLines: false, showStemLines: false,
useWorker: true,
amount: 5 amount: 5
}); });
@ -69,6 +71,11 @@ export const AppSettingTypes = {
label: "Wireframe", label: "Wireframe",
value: false, value: false,
}, },
useWorker: {
type: "boolean",
label: "Execute runtime in worker",
value: true,
},
showIndices: { showIndices: {
type: "boolean", type: "boolean",
label: "Show Indices", label: "Show Indices",
@ -79,6 +86,11 @@ export const AppSettingTypes = {
label: "Show Performance Panel", label: "Show Performance Panel",
value: false, value: false,
}, },
showBenchmarkPanel: {
type: "boolean",
label: "Show Benchmark Panel",
value: false,
},
showVertices: { showVertices: {
type: "boolean", type: "boolean",
label: "Show Vertices", label: "Show Vertices",
@ -104,6 +116,18 @@ export const AppSettingTypes = {
type: "button", type: "button",
label: "Load Tree" 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'"
}
}, },
} }
} }

View File

@ -43,7 +43,6 @@
? filterInputs(node.tmp.type.inputs) ? filterInputs(node.tmp.type.inputs)
: undefined; : undefined;
$: store = node ? createStore(node.props, nodeDefinition) : undefined; $: store = node ? createStore(node.props, nodeDefinition) : undefined;
$: console.log(nodeDefinition, store);
let lastPropsHash = ""; let lastPropsHash = "";
function updateNode() { function updateNode() {

View 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>

View 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>

View File

@ -4,10 +4,11 @@
export let keymap: ReturnType<typeof createKeyMap>; export let keymap: ReturnType<typeof createKeyMap>;
const keys = keymap?.keys; const keys = keymap?.keys;
export let title = "Keymap";
</script> </script>
<div class="wrapper"> <div class="wrapper">
<h3>Editor</h3> <h3>{title}</h3>
<section> <section>
{#each $keys as key} {#each $keys as key}

View File

@ -7,12 +7,15 @@ const cache = new MemoryRuntimeCache();
const nodeRegistry = new RemoteNodeRegistry(""); const nodeRegistry = new RemoteNodeRegistry("");
const executor = new MemoryRuntimeExecutor(nodeRegistry, cache); const executor = new MemoryRuntimeExecutor(nodeRegistry, cache);
const performanceStore = createPerformanceStore(); const performanceStore = createPerformanceStore("worker");
executor.perf = performanceStore; executor.perf = performanceStore;
export async function executeGraph(graph: Graph, settings: Record<string, unknown>): Promise<Int32Array> { export async function executeGraph(graph: Graph, settings: Record<string, unknown>): Promise<Int32Array> {
await nodeRegistry.load(graph.nodes.map((n) => n.type)); 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() { export function getPerformanceData() {

View File

@ -10,7 +10,7 @@
import { AppSettingTypes, AppSettings } from "$lib/settings/app-settings"; import { AppSettingTypes, AppSettings } from "$lib/settings/app-settings";
import { writable, type Writable } from "svelte/store"; import { writable, type Writable } from "svelte/store";
import Keymap from "$lib/settings/panels/Keymap.svelte"; 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 NodeStore from "$lib/node-store/NodeStore.svelte";
import type { GraphManager } from "$lib/graph-interface/graph-manager"; import type { GraphManager } from "$lib/graph-interface/graph-manager";
import { setContext } from "svelte"; import { setContext } from "svelte";
@ -20,15 +20,28 @@
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 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 nodeRegistry = new RemoteNodeRegistry("");
const workerRuntime = new WorkerRuntimeExecutor(); 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 activeNode: Node | undefined;
let scene: Scene;
let graphResult: Int32Array; let updateViewerResult: (result: Int32Array) => void;
let graph = localStorage.getItem("graph") let graph = localStorage.getItem("graph")
? JSON.parse(localStorage.getItem("graph")!) ? JSON.parse(localStorage.getItem("graph")!)
@ -36,11 +49,22 @@
let manager: GraphManager; let manager: GraphManager;
let managerStatus: Writable<"loading" | "error" | "idle">; 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 keymap: ReturnType<typeof createKeyMap>;
let applicationKeymap = createKeyMap([
{
key: "r",
description: "Regenerate the plant model",
callback: randomGenerate,
},
]);
let graphSettings = writable<Record<string, any>>({}); let graphSettings = writable<Record<string, any>>({});
let graphSettingTypes = {}; let graphSettingTypes = {};
@ -50,42 +74,59 @@
| { | {
graph: Graph; graph: Graph;
settings: Record<string, any>; settings: Record<string, any>;
hash: number;
} }
| undefined; | undefined;
async function handleResult(_graph: Graph, _settings: Record<string, any>) { async function handleResult(_graph: Graph, _settings: Record<string, any>) {
if (!_settings) return; if (!_settings) return;
const inputHash = fastHashString(
JSON.stringify(_graph) + JSON.stringify(_settings),
);
if (isWorking) { if (isWorking) {
unfinished = { unfinished = {
graph: _graph, graph: _graph,
settings: _settings, settings: _settings,
hash: inputHash,
}; };
return; return false;
} }
isWorking = true; isWorking = true;
performanceStore.startRun();
try { try {
let a = performance.now(); let a = performance.now();
graphResult = await workerRuntime.execute(_graph, _settings); const graphResult = await runtime.execute(_graph, _settings);
let b = performance.now(); let b = performance.now();
let perfData = await workerRuntime.getPerformanceData();
let lastRun = perfData.at(-1); if ($AppSettings.useWorker) {
if (lastRun) { let perfData = await runtime.getPerformanceData();
lastRun["worker-transfer"] = [b - a - lastRun.runtime[0]]; let lastRun = perfData?.at(-1);
if (lastRun?.total) {
lastRun.runtime = lastRun.total;
delete lastRun.total;
performanceStore.mergeData(lastRun); performanceStore.mergeData(lastRun);
performanceStore.addPoint(
"worker-transfer",
b - a - lastRun.runtime[0],
);
} }
isWorking = false; }
updateViewerResult(graphResult);
} catch (error) { } catch (error) {
console.log("errors", error); console.log("errors", error);
} finally {
performanceStore.stopRun();
isWorking = false;
} }
performanceStore.stopRun(); if (unfinished && unfinished.hash === inputHash) {
performanceStore.startRun();
if (unfinished) {
let d = unfinished; let d = unfinished;
unfinished = undefined; unfinished = undefined;
handleResult(d.graph, d.settings); await handleResult(d.graph, d.settings);
} }
return true;
} }
$: if (AppSettings) { $: if (AppSettings) {
@ -97,6 +138,18 @@
AppSettingTypes.debug.stressTest.loadTree.callback = () => { AppSettingTypes.debug.stressTest.loadTree.callback = () => {
graph = templates.tree($AppSettings.amount); 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>) { function handleSave(event: CustomEvent<Graph>) {
@ -104,13 +157,15 @@
} }
</script> </script>
<svelte:document on:keydown={applicationKeymap.handleKeyboardEvent} />
<div class="wrapper manager-{$managerStatus}"> <div class="wrapper manager-{$managerStatus}">
<header></header> <header></header>
<Grid.Row> <Grid.Row>
<Grid.Cell> <Grid.Cell>
<Viewer <Viewer
result={graphResult}
perf={performanceStore} perf={performanceStore}
bind:scene
bind:update={updateViewerResult}
centerCamera={$AppSettings.centerCamera} centerCamera={$AppSettings.centerCamera}
/> />
</Grid.Cell> </Grid.Cell>
@ -143,10 +198,14 @@
title="Keyboard Shortcuts" title="Keyboard Shortcuts"
icon="i-tabler-keyboard" icon="i-tabler-keyboard"
> >
<Keymap title="Application" keymap={applicationKeymap} />
{#if keymap} {#if keymap}
<Keymap {keymap} /> <Keymap title="Node-Editor" {keymap} />
{/if} {/if}
</Panel> </Panel>
<Panel id="exports" title="Exporter" icon="i-tabler-package-export">
<ExportSettings {scene} />
</Panel>
<Panel <Panel
id="node-store" id="node-store"
classes="text-green-400" classes="text-green-400"
@ -166,6 +225,15 @@
<PerformanceViewer data={$performanceStore} /> <PerformanceViewer data={$performanceStore} />
{/if} {/if}
</Panel> </Panel>
<Panel
id="benchmark"
title="Benchmark"
classes="text-red-400"
hidden={!$AppSettings.showBenchmarkPanel}
icon="i-tabler-graph"
>
<BenchmarkPanel run={randomGenerate} />
</Panel>
<Panel <Panel
id="graph-settings" id="graph-settings"
title="Graph Settings" title="Graph Settings"
@ -190,8 +258,6 @@
</Grid.Row> </Grid.Row>
</div> </div>
<span class="font-red" />
<style> <style>
header { header {
/* border-bottom: solid thin var(--outline); */ /* border-bottom: solid thin var(--outline); */

View File

@ -9,13 +9,13 @@
}, },
"strength": { "strength": {
"type": "float", "type": "float",
"min": 0.1, "min": 0,
"max": 1 "max": 1
}, },
"curviness": { "curviness": {
"type": "float", "type": "float",
"hidden": true, "hidden": true,
"min": 0.1, "min": 0,
"max": 1 "max": 1
}, },
"depth": { "depth": {

View File

@ -1,8 +1,9 @@
use glam::Vec3; use glam::Vec3;
use macros::include_definition_file; use macros::include_definition_file;
use utils::{ use utils::{
concat_args, evaluate_float, evaluate_int, geometry::wrap_path_mut, log, reset_call_count, concat_args, evaluate_float, evaluate_int,
set_panic_hook, split_args, geometry::{wrap_path, wrap_path_mut},
log, reset_call_count, set_panic_hook, split_args,
}; };
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
@ -34,58 +35,64 @@ pub fn execute(input: &[i32]) -> Vec<i32> {
let output: Vec<Vec<i32>> = plants let output: Vec<Vec<i32>> = plants
.iter() .iter()
.map(|_path_data| { .map(|_path_data| {
let mut path_data = _path_data.to_vec(); let path_data = _path_data.to_vec();
if path_data[2] != 0 || path_data[3] < (max_depth - depth + 1) { if path_data[2] != 0 || path_data[3] < (max_depth - depth + 1) {
return path_data; return path_data;
} }
let path = wrap_path_mut(&mut path_data); let path = wrap_path(&path_data);
let mut output_data = path_data.clone();
let output = wrap_path_mut(&mut output_data);
let mut offset_vec = Vec3::ZERO; let mut offset_vec = Vec3::ZERO;
for i in 1..path.length { let original_length = path.get_length();
// let alpha = i as f32 / (path.length - 1) as f32;
let start_index = (i - 1) * 4; for i in 0..path.length - 1 {
let end_index = start_index + 4; let alpha = i as f32 / (path.length - 1) as f32;
let start_index = i * 4;
let start_point = Vec3::from_slice(&path.points[start_index..start_index + 3]); let start_point = Vec3::from_slice(&path.points[start_index..start_index + 3]);
let end_point = Vec3::from_slice(&path.points[end_index..end_index + 3]); let end_point = Vec3::from_slice(&path.points[start_index + 4..start_index + 7]);
let length = (end_point - start_point).length(); let direction = end_point - start_point;
let normalised = (end_point - start_point).normalize(); let length = direction.length();
let strength = evaluate_float(args[1]);
let down_point = Vec3::new(0.0, -length * strength, 0.0);
let curviness = evaluate_float(args[2]); let curviness = evaluate_float(args[2]);
let strength =
evaluate_float(args[1]) / curviness.max(0.0001) * evaluate_float(args[1]);
let mut mid_point = lerp_vec3( log!(
normalised, "length: {}, curviness: {}, strength: {}",
down_point, length,
curviness * (i as f32 / path.length as f32).sqrt(), curviness,
strength
); );
let down_point = Vec3::new(0.0, -length * strength, 0.0);
let mut mid_point = lerp_vec3(direction, down_point, curviness * alpha.sqrt());
if mid_point[0] == 0.0 && mid_point[2] == 0.0 { if mid_point[0] == 0.0 && mid_point[2] == 0.0 {
mid_point[0] += 0.0001; mid_point[0] += 0.0001;
mid_point[2] += 0.0001; mid_point[2] += 0.0001;
} }
mid_point = mid_point.normalize(); // Correct midpoint length
mid_point *= mid_point.length() / length;
mid_point *= length;
let final_end_point = start_point + mid_point; let final_end_point = start_point + mid_point;
let offset_end_point = end_point + offset_vec; let offset_end_point = end_point + offset_vec;
path.points[end_index] = offset_end_point[0]; output.points[start_index + 4] = offset_end_point[0];
path.points[end_index + 1] = offset_end_point[1]; output.points[start_index + 5] = offset_end_point[1];
path.points[end_index + 2] = offset_end_point[2]; output.points[start_index + 6] = offset_end_point[2];
let offset = final_end_point - end_point; offset_vec += final_end_point - end_point;
offset_vec += offset;
} }
path_data log!("length: {} final: {}", original_length, output.get_length());
output_data
}) })
.collect(); .collect();

View File

@ -1,4 +1,3 @@
use glam::Vec3;
use macros::include_definition_file; use macros::include_definition_file;
use noise::{HybridMulti, MultiFractal, NoiseFn, OpenSimplex}; use noise::{HybridMulti, MultiFractal, NoiseFn, OpenSimplex};
use utils::{ use utils::{
@ -66,15 +65,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 p0 = Vec3::new(path.points[0], path.points[1], path.points[2]); let length = path.get_length() as f64;
let p2 = Vec3::new(
path.points[path.length * 4 - 3],
path.points[path.length * 4 - 2],
path.points[path.length * 4 - 1],
);
let length = (p2 - p0).length() as f64;
for i in 0..path.length { for i in 0..path.length {
let a = i as f64 / (path.length - 1) as f64; let a = i as f64 / (path.length - 1) as f64;

View File

@ -1,5 +1,6 @@
export type { NodeInput } from "./inputs"; export type { NodeInput } from "./inputs";
export type { NodeRegistry, RuntimeExecutor, RuntimeCache } from "./components"; export type { NodeRegistry, RuntimeExecutor, RuntimeCache } from "./components";
export type { Node, NodeDefinition, Socket, NodeId, Edge, Graph } from "./types"; export type { Node, NodeDefinition, Socket, NodeId, Edge, Graph } from "./types";
export { NodeSchema, GraphSchema } from "./types";
export { NodeDefinitionSchema } from "./types"; export { NodeDefinitionSchema } from "./types";

View File

@ -3,10 +3,18 @@ import { NodeInputSchema } from "./inputs";
export type NodeId = `${string}/${string}/${string}`; export type NodeId = `${string}/${string}/${string}`;
export const NodeSchema = z.object({
id: z.number(),
type: z.string(),
props: z.record(z.union([z.number(), z.array(z.number())])).optional(),
meta: z.object({
title: z.string().optional(),
lastModified: z.string().optional(),
}).optional(),
position: z.tuple([z.number(), z.number()])
});
export type Node = { export type Node = {
id: number;
type: NodeId;
props?: Record<string, number | number[]>,
tmp?: { tmp?: {
depth?: number; depth?: number;
mesh?: any; mesh?: any;
@ -22,13 +30,8 @@ export type Node = {
ref?: HTMLElement; ref?: HTMLElement;
visible?: boolean; visible?: boolean;
isMoving?: boolean; isMoving?: boolean;
},
meta?: {
title?: string;
lastModified?: string;
},
position: [x: number, y: number]
} }
} & z.infer<typeof NodeSchema>;
export const NodeDefinitionSchema = z.object({ export const NodeDefinitionSchema = z.object({
id: z.string(), id: z.string(),
@ -50,16 +53,17 @@ export type Socket = {
position: [number, number]; position: [number, number];
}; };
export type Edge = [Node, number, Node, string]; export type Edge = [Node, number, Node, string];
export type Graph = { export const GraphSchema = z.object({
id: number; id: z.number().optional(),
meta?: { meta: z.object({
title?: string; title: z.string().optional(),
lastModified?: string; lastModified: z.string().optional(),
}, }).optional(),
settings?: Record<string, any>, settings: z.record(z.any()).optional(),
nodes: Node[]; nodes: z.array(NodeSchema),
edges: [number, number, number, string][]; edges: z.array(z.tuple([z.number(), z.number(), z.number(), z.string()])),
} });
export type Graph = z.infer<typeof GraphSchema> & { nodes: Node[] };

View File

@ -1,25 +1,25 @@
<script lang="ts"> <script lang="ts">
import Checkbox from "./elements/Checkbox.svelte"; import Checkbox from './elements/Checkbox.svelte';
import Float from "./elements/Float.svelte"; import Float from './elements/Float.svelte';
import Integer from "./elements/Integer.svelte"; import Integer from './elements/Integer.svelte';
import Select from "./elements/Select.svelte"; import Select from './elements/Select.svelte';
import type { NodeInput } from "@nodes/types"; import type { NodeInput } from '@nodes/types';
import Vec3 from "./elements/Vec3.svelte"; import Vec3 from './elements/Vec3.svelte';
export let input: NodeInput; export let input: NodeInput;
export let value: any; export let value: any;
export let id: string; export let id: string;
</script> </script>
{#if input.type === "float"} {#if input.type === 'float'}
<Float {id} bind:value min={input?.min} max={input?.max} /> <Float {id} bind:value min={input?.min} max={input?.max} />
{:else if input.type === "integer"} {:else if input.type === 'integer'}
<Integer {id} bind:value min={input?.min} max={input?.max} /> <Integer {id} bind:value min={input?.min} max={input?.max} />
{:else if input.type === "boolean"} {:else if input.type === 'boolean'}
<Checkbox {id} bind:value /> <Checkbox {id} bind:value />
{:else if input.type === "select"} {:else if input.type === 'select'}
<Select {id} bind:value options={input.options} /> <Select {id} bind:value options={input?.options || []} />
{:else if input.type === "vec3"} {:else if input.type === 'vec3'}
<Vec3 {id} bind:value /> <Vec3 {id} bind:value />
{/if} {/if}

View File

@ -1,13 +1,13 @@
<script lang="ts"> <script lang="ts">
export let value: boolean; export let value: boolean;
$: if (typeof value === "string") { $: if (typeof value === 'string') {
value = value === "true"; value = value === 'true';
} else if (typeof value === "number") { } else if (typeof value === 'number') {
value = value === 1; value = value === 1;
} }
export let id = ""; export let id = '';
</script> </script>
<input {id} type="checkbox" bind:checked={value} /> <input {id} type="checkbox" bind:checked={value} />
@ -24,7 +24,7 @@
</label> </label>
<style> <style>
input[type="checkbox"] { input[type='checkbox'] {
position: absolute; position: absolute;
overflow: hidden; overflow: hidden;
clip: rect(0 0 0 0); clip: rect(0 0 0 0);
@ -49,7 +49,7 @@
color: rgb(0, 0, 0); color: rgb(0, 0, 0);
} }
input + label::before { input + label::before {
content: " "; content: ' ';
display: inline; display: inline;
vertical-align: middle; vertical-align: middle;
margin-right: 3px; margin-right: 3px;
@ -61,7 +61,7 @@
box-shadow: none; box-shadow: none;
} }
input:checked + label::after { input:checked + label::after {
content: " "; content: ' ';
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: 12px 12px; background-size: 12px 12px;
background-position: center center; background-position: center center;

View File

@ -1,11 +1,19 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte'; let {
onchange,
export let value = 0.5; value = $bindable(),
export let step = 0.01; id,
export let min = 0; step = 0.01,
export let max = 1; min = 0,
export let id = ''; max = 1
}: {
onchange?: (num: number) => void;
value?: number;
id?: string;
step?: number;
min?: number;
max?: number;
} = $props();
if (min > max) { if (min > max) {
[min, max] = [max, min]; [min, max] = [max, min];
@ -18,31 +26,32 @@
return +parseFloat(input + '').toPrecision(2); return +parseFloat(input + '').toPrecision(2);
} }
const dispatch = createEventDispatcher();
let inputEl: HTMLInputElement; let inputEl: HTMLInputElement;
$: if ((value || 0).toString().length > 5) { $effect(() => {
if ((value || 0).toString().length > 5) {
value = strip(value || 0); value = strip(value || 0);
} }
$: value !== undefined && handleChange(); });
$effect(() => {
if (value !== undefined) handleChange();
});
let oldValue: number; let oldValue: number;
function handleChange() { function handleChange() {
if (value === oldValue) return; if (value === oldValue) return;
oldValue = value; oldValue = value;
dispatch('change', parseFloat(value + '')); onchange?.(value);
} }
$: width = Number.isFinite(value) let width = $derived(
? Math.max((value?.toString().length ?? 1) * 8, 50) + 'px' Number.isFinite(value) ? Math.max((value?.toString().length ?? 1) * 8, 50) + 'px' : '20px'
: '20px'; );
let isMouseDown = false; let isMouseDown = $state(false);
/* let downX = 0; */
/* let downY = 0; */
let downV = 0; let downV = 0;
let vx = 0; let vx = 0;
/* let vy = 0; */
let rect: DOMRect; let rect: DOMRect;
function handleMouseDown(ev: MouseEvent) { function handleMouseDown(ev: MouseEvent) {
@ -53,8 +62,6 @@
isMouseDown = true; isMouseDown = true;
downV = value; downV = value;
/* downX = ev.clientX; */
/* downY = ev.clientY; */
rect = inputEl.getBoundingClientRect(); rect = inputEl.getBoundingClientRect();
window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mousemove', handleMouseMove);
@ -78,16 +85,6 @@
min = value; min = value;
} }
// setTimeout(() => {
// if (value >= 0) {
// max = getBoundingValue(value);
// min = 0;
// } else {
// min = getBoundingValue(value);
// max = 0;
// }
// }, 500);
document.body.style.cursor = 'unset'; document.body.style.cursor = 'unset';
window.removeEventListener('mouseup', handleMouseUp); window.removeEventListener('mouseup', handleMouseUp);
window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mousemove', handleMouseMove);
@ -122,9 +119,9 @@
{step} {step}
{max} {max}
{min} {min}
on:keydown={handleKeyDown} onkeydown={handleKeyDown}
on:mousedown={handleMouseDown} onmousedown={handleMouseDown}
on:mouseup={handleMouseUp} onmouseup={handleMouseUp}
type="number" type="number"
style={`width:${width};`} style={`width:${width};`}
/> />

View File

@ -1,15 +1,19 @@
<svelte:options accessors />
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte"; let {
const dispatch = createEventDispatcher(); min = 0,
max = 10,
// Styling step = 1,
export let min: number | undefined = undefined; value = $bindable(),
export let max: number | undefined = undefined; id,
export let step = 1; onchange
export let value = 0; }: {
export let id = ""; min?: number;
max?: number;
step?: number;
value?: number;
id?: string;
onchange?: (num: number) => void;
} = $props();
if (!value) { if (!value) {
value = 0; value = 0;
@ -17,24 +21,25 @@
let inputEl: HTMLInputElement; let inputEl: HTMLInputElement;
let wrapper: HTMLDivElement; let wrapper: HTMLDivElement;
$: value !== undefined && update(); $effect(() => {
if (value !== undefined) {
update();
}
});
let prev = -1; let prev = -1;
function update() { function update() {
if (prev === value) return; if (prev === value) return;
prev = value; prev = value;
dispatch("change", parseFloat(value + "")); onchange?.(value);
} }
$: width = Number.isFinite(value) let width = $derived(
? Math.max((value?.toString().length ?? 1) * 8, 30) + "px" Number.isFinite(value) ? Math.max((value?.toString().length ?? 1) * 8, 30) + 'px' : '20px'
: "20px"; );
function handleChange(change: number) { function handleChange(change: number) {
value = Math.max( value = Math.max(min ?? -Infinity, Math.min(+value + change, max ?? Infinity));
min ?? -Infinity,
Math.min(+value + change, max ?? Infinity),
);
} }
let downX = 0; let downX = 0;
@ -48,10 +53,10 @@
downX = ev.clientX; downX = ev.clientX;
rect = wrapper.getBoundingClientRect(); rect = wrapper.getBoundingClientRect();
window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener('mousemove', handleMouseMove);
window.addEventListener("mousemove", handleMouseMove); window.addEventListener('mousemove', handleMouseMove);
window.addEventListener("mouseup", handleMouseUp); window.addEventListener('mouseup', handleMouseUp);
document.body.style.cursor = "ew-resize"; document.body.style.cursor = 'ew-resize';
} }
function handleMouseUp() { function handleMouseUp() {
@ -61,13 +66,13 @@
inputEl.blur(); inputEl.blur();
} }
document.body.style.cursor = "unset"; document.body.style.cursor = 'unset';
window.removeEventListener("mouseup", handleMouseUp); window.removeEventListener('mouseup', handleMouseUp);
window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener('mousemove', handleMouseMove);
} }
function handleMouseMove(ev: MouseEvent) { function handleMouseMove(ev: MouseEvent) {
if (!ev.ctrlKey && typeof min === "number" && typeof max === "number") { if (!ev.ctrlKey && typeof min === 'number' && typeof max === 'number') {
const vx = (ev.clientX - rect.left) / rect.width; const vx = (ev.clientX - rect.left) / rect.width;
value = Math.max(Math.min(Math.round(min + (max - min) * vx), max), min); value = Math.max(Math.min(Math.round(min + (max - min) * vx), max), min);
} else { } else {
@ -83,16 +88,14 @@
role="slider" role="slider"
tabindex="0" tabindex="0"
aria-valuenow={value} aria-valuenow={value}
on:mousedown={handleMouseDown} onmousedown={handleMouseDown}
on:mouseup={handleMouseUp} onmouseup={handleMouseUp}
> >
{#if typeof min !== "undefined" && typeof max !== "undefined"} {#if typeof min !== 'undefined' && typeof max !== 'undefined'}
<span <span class="overlay" style={`width: ${Math.min((value - min) / (max - min), 1) * 100}%`}
class="overlay" ></span>
style={`width: ${Math.min((value - min) / (max - min), 1) * 100}%`}
/>
{/if} {/if}
<button on:click={() => handleChange(-step)}>-</button> <button onclick={() => handleChange(-step)}>-</button>
<input <input
bind:value bind:value
bind:this={inputEl} bind:this={inputEl}
@ -104,7 +107,7 @@
style={`width:${width};`} style={`width:${width};`}
/> />
<button on:click={() => handleChange(+step)}>+</button> <button onclick={() => handleChange(+step)}>+</button>
</div> </div>
<style> <style>
@ -120,7 +123,7 @@
border-radius: var(--border-radius, 2px); border-radius: var(--border-radius, 2px);
} }
input[type="number"] { input[type='number'] {
-webkit-appearance: textfield; -webkit-appearance: textfield;
-moz-appearance: textfield; -moz-appearance: textfield;
appearance: textfield; appearance: textfield;
@ -132,8 +135,8 @@
width: 72%; width: 72%;
} }
input[type="number"]::-webkit-inner-spin-button, input[type='number']::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button { input[type='number']::-webkit-outer-spin-button {
-webkit-appearance: none; -webkit-appearance: none;
} }
@ -157,7 +160,7 @@
margin-inline: 6px; margin-inline: 6px;
} }
div input[type="number"] { div input[type='number'] {
color: var(--text-color); color: var(--text-color);
background-color: transparent; background-color: transparent;
padding: var(--padding, 6px); padding: var(--padding, 6px);

View File

@ -1,7 +1,9 @@
<script lang="ts"> <script lang="ts">
export let options: string[] = []; let {
export let value: number = 0; id,
export let id = ""; value = $bindable(),
options
}: { id: string; value: number; options: string[] } = $props();
</script> </script>
<select {id} bind:value> <select {id} bind:value>

View File

@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import Float from "./Float.svelte"; import Float from './Float.svelte';
export let value = [0, 0, 0]; let { value = $bindable(), id }: { value: number[]; id: string } = $props();
export let id = "";
</script> </script>
<div> <div>
@ -21,8 +20,7 @@
outline: none; outline: none;
border: solid thin var(--outline); border: solid thin var(--outline);
border-top: solid thin color-mix(in srgb, var(--outline) 50%, transparent); border-top: solid thin color-mix(in srgb, var(--outline) 50%, transparent);
border-bottom: solid thin border-bottom: solid thin color-mix(in srgb, var(--outline) 50%, transparent);
color-mix(in srgb, var(--outline) 50%, transparent);
} }
div > :global(.component-wrapper:nth-child(3)) { div > :global(.component-wrapper:nth-child(3)) {
border-top: none !important; border-top: none !important;

View File

@ -1,7 +1,34 @@
<script lang="ts"> <script lang="ts">
import "$lib/app.css"; import '$lib/app.css';
import Slider from "$lib/elements/Float.svelte"; import Float from '$lib/elements/Float.svelte';
import Integer from '$lib/elements/Integer.svelte';
import Vec3 from '$lib/elements/Vec3.svelte';
let intValue = $state(0);
let floatValue = $state(0.2);
let vecValue = $state([0.2, 0.3, 0.4]);
</script> </script>
<Slider id="asd" /> <main>
<section>
<h3>Integer {intValue}</h3>
<Integer bind:value={intValue} />
</section>
<section>
<h3>Float {floatValue}</h3>
<Float bind:value={floatValue} />
</section>
<section>
<h3>Vec3 {JSON.stringify(vecValue)}</h3>
<Vec3 bind:value={vecValue} />
</section>
</main>
<style>
main {
max-width: 800px;
margin: 0 auto;
}
</style>

View File

@ -102,3 +102,47 @@ function decode_recursive(dense: number[] | Int32Array, index = 0) {
export function decodeNestedArray(dense: number[] | Int32Array) { export function decodeNestedArray(dense: number[] | Int32Array) {
return decode_recursive(dense, 0)[0]; return decode_recursive(dense, 0)[0];
} }
export function splitNestedArray(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;
}

View File

@ -11,12 +11,52 @@ pub struct PathDataMut<'a> {
pub points: &'a mut [f32], pub points: &'a mut [f32],
} }
impl PathDataMut<'_> {
pub fn get_length(&self) -> f32 {
let mut l = 0.0;
for i in 0..(self.length - 1) {
let a = vec3(
self.points[i * 4],
self.points[i * 4 + 1],
self.points[i * 4 + 2],
);
let b = vec3(
self.points[(i + 1) * 4],
self.points[(i + 1) * 4 + 1],
self.points[(i + 1) * 4 + 2],
);
l += (b - a).length();
}
l
}
}
pub struct PathData<'a> { pub struct PathData<'a> {
pub depth: i32, pub depth: i32,
pub length: usize, pub length: usize,
pub points: &'a [f32], pub points: &'a [f32],
} }
impl PathData<'_> {
pub fn get_length(&self) -> f32 {
let mut l = 0.0;
for i in 0..(self.length - 1) {
let a = vec3(
self.points[i * 4],
self.points[i * 4 + 1],
self.points[i * 4 + 2],
);
let b = vec3(
self.points[(i + 1) * 4],
self.points[(i + 1) * 4 + 1],
self.points[(i + 1) * 4 + 2],
);
l += (b - a).length();
}
l
}
}
pub fn create_multiple_paths(amount: usize, point_amount: usize, depth: i32) -> Vec<i32> { pub fn create_multiple_paths(amount: usize, point_amount: usize, depth: i32) -> Vec<i32> {
let output_size = amount * (point_amount * 4 + PATH_HEADER_SIZE + 4) + 4; let output_size = amount * (point_amount * 4 + PATH_HEADER_SIZE + 4) + 4;

View File

@ -162,7 +162,12 @@ pub fn evaluate_vec3(input_args: &[i32]) -> Vec<f32> {
} }
pub fn evaluate_float(arg: &[i32]) -> f32 { pub fn evaluate_float(arg: &[i32]) -> f32 {
decode_float(evaluate_int(arg)) let res = decode_float(evaluate_int(arg));
if res.is_nan() {
0.0
} else {
res
}
} }
pub fn evaluate_int(input_args: &[i32]) -> i32 { pub fn evaluate_int(input_args: &[i32]) -> i32 {

View File

@ -34,6 +34,9 @@ importers:
comlink: comlink:
specifier: ^4.4.1 specifier: ^4.4.1
version: 4.4.1 version: 4.4.1
file-saver:
specifier: ^2.0.5
version: 2.0.5
jsondiffpatch: jsondiffpatch:
specifier: ^0.6.0 specifier: ^0.6.0
version: 0.6.0 version: 0.6.0
@ -56,6 +59,9 @@ importers:
'@tsconfig/svelte': '@tsconfig/svelte':
specifier: ^5.0.4 specifier: ^5.0.4
version: 5.0.4 version: 5.0.4
'@types/file-saver':
specifier: ^2.0.7
version: 2.0.7
'@unocss/preset-icons': '@unocss/preset-icons':
specifier: ^0.59.4 specifier: ^0.59.4
version: 0.59.4 version: 0.59.4
@ -747,6 +753,9 @@ packages:
'@types/estree@1.0.5': '@types/estree@1.0.5':
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
'@types/file-saver@2.0.7':
resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==}
'@types/json-schema@7.0.15': '@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@ -1314,6 +1323,9 @@ packages:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0} engines: {node: ^10.12.0 || >=12.0.0}
file-saver@2.0.5:
resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==}
fill-range@7.0.1: fill-range@7.0.1:
resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -3006,6 +3018,8 @@ snapshots:
'@types/estree@1.0.5': {} '@types/estree@1.0.5': {}
'@types/file-saver@2.0.7': {}
'@types/json-schema@7.0.15': {} '@types/json-schema@7.0.15': {}
'@types/node@20.12.7': '@types/node@20.12.7':
@ -3735,6 +3749,8 @@ snapshots:
dependencies: dependencies:
flat-cache: 3.2.0 flat-cache: 3.2.0
file-saver@2.0.5: {}
fill-range@7.0.1: fill-range@7.0.1:
dependencies: dependencies:
to-regex-range: 5.0.1 to-regex-range: 5.0.1