This commit is contained in:
2024-12-19 18:28:17 +01:00
parent 53f400a4f6
commit 33d5ed14dd
16 changed files with 232 additions and 240 deletions

View File

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

View File

@@ -0,0 +1,145 @@
<script lang="ts">
import localStore from "$lib/helpers/localStore";
import { setContext } from "svelte";
import { derived } from "svelte/store";
let panels: Record<
string,
{
icon: string;
id: string;
classes: string;
visible?: boolean;
}
> = {};
const activePanel = localStore<keyof typeof panels | false>(
"nodes.settings.activePanel",
false,
);
$: keys = panels
? (Object.keys(panels) as unknown as (keyof typeof panels)[]).filter(
(key) => !!panels[key]?.id,
)
: [];
setContext("setVisibility", (id: string, visible: boolean) => {
panels[id].visible = visible;
panels = { ...panels };
});
setContext("registerPanel", (id: string, icon: string, classes: string) => {
panels[id] = { id, icon, classes };
return derived(activePanel, ($activePanel) => {
return $activePanel === id;
});
});
function setActivePanel(panel: keyof typeof panels | false) {
if (panel === $activePanel) {
$activePanel = false;
} else if (panel) {
$activePanel = panel;
} else {
$activePanel = false;
}
}
</script>
<div class="wrapper" class:visible={$activePanel}>
<div class="tabs">
<button
aria-label="Close"
on:click={() => {
setActivePanel($activePanel ? false : keys[0]);
}}
>
<span class="absolute i-tabler-chevron-left w-6 h-6 block"></span>
</button>
{#each keys as panel (panels[panel].id)}
{#if panels[panel].visible !== false}
<button
aria-label={panel}
class="tab {panels[panel].classes}"
class:active={panel === $activePanel}
on:click={() => setActivePanel(panel)}
>
<span class={`block w-6 h-6 ${panels[panel].icon}`}></span>
</button>
{/if}
{/each}
</div>
<div class="content">
<slot />
</div>
</div>
<style>
.wrapper {
top: 0px;
position: absolute;
display: grid;
z-index: 2;
grid-template-columns: 30px 1fr;
height: 100%;
right: 0px;
transform: translateX(calc(100% - 30px));
transition:
transform 0.2s,
background 0.2s ease;
width: 30%;
min-width: 350px;
}
.content {
background: var(--layer-1);
z-index: 10;
position: relative;
max-height: 100vh;
overflow-y: auto;
}
.tabs {
display: flex;
flex-direction: column;
border-right: solid thin var(--outline);
}
.tabs > button {
height: 35px;
padding: 5px;
border-radius: 0px;
background: none;
border: none;
display: flex;
align-items: center;
border-bottom: solid thin var(--outline);
border-left: solid thin var(--outline);
background: var(--layer-0);
}
.tabs > button > span {
opacity: 0.5;
}
.tabs > button.active {
background: var(--layer-1);
}
.tabs > button.active span {
opacity: 1;
}
.visible .tabs {
margin-left: -1px;
}
.visible > .tabs button:first-child > span {
transform: scaleX(-1);
}
.visible {
transform: translateX(0);
}
</style>

View File

@@ -0,0 +1,82 @@
<script lang="ts">
import type { Node, NodeInput } from "@nodes/types";
import NestedSettings from "$lib/settings/NestedSettings.svelte";
import type { GraphManager } from "$lib/graph-interface/graph-manager";
type Props = {
manager: GraphManager;
node: Node;
};
const { manager, node }: Props = $props();
const nodeDefinition = filterInputs(node.tmp?.type?.inputs);
function filterInputs(inputs?: Record<string, NodeInput>) {
const _inputs = $state.snapshot(inputs);
return Object.fromEntries(
Object.entries(structuredClone(_inputs ?? {}))
.filter(([_key, value]) => {
return value.hidden === true;
})
.map(([key, value]) => {
//@ts-ignore
value.__node_type = node?.tmp?.type.id;
//@ts-ignore
value.__node_input = key;
return [key, value];
}),
);
}
type Store = Record<string, number | number[]>;
let store = $state<Store>(createStore(node?.props, nodeDefinition));
function createStore(
props: Node["props"],
inputs: Record<string, NodeInput>,
): Store {
const store: Store = {};
Object.keys(inputs).forEach((key) => {
if (props) {
//@ts-ignore
store[key] = props[key] || inputs[key].value;
}
});
return store;
}
let lastPropsHash = "";
function updateNode() {
if (!node || !store) return;
let needsUpdate = false;
Object.keys(store).forEach((_key: string) => {
node.props = node.props || {};
const key = _key as keyof typeof store;
if (node && store) {
needsUpdate = true;
node.props[key] = store[key];
}
});
let propsHash = JSON.stringify(node.props);
if (propsHash === lastPropsHash) {
return;
}
lastPropsHash = propsHash;
if (needsUpdate) {
manager.execute();
}
}
$effect(() => {
if (store) {
updateNode();
}
});
</script>
<NestedSettings
id="activeNodeSettings"
bind:value={store}
type={nodeDefinition}
/>

View File

@@ -0,0 +1,24 @@
<script lang="ts">
import type { Node } from "@nodes/types";
import type { GraphManager } from "$lib/graph-interface/graph-manager";
import ActiveNodeSelected from "./ActiveNodeSelected.svelte";
type Props = {
manager: GraphManager;
node: Node | undefined;
};
const { manager, node }: Props = $props();
</script>
{#if node}
{#key node.id}
{#if node}
<ActiveNodeSelected {manager} {node} />
{:else}
<p class="mx-4">Active Node has no Settings</p>
{/if}
{/key}
{:else}
<p class="mx-4">No active node</p>
{/if}

View File

@@ -0,0 +1,143 @@
<script lang="ts">
import localStore from "$lib/helpers/localStore";
import { Integer } from "@nodes/ui";
import { writable } from "svelte/store";
import { humanizeDuration } from "$lib/helpers";
import Monitor from "$lib/performance/Monitor.svelte";
function calculateStandardDeviation(array: number[]) {
const n = array.length;
const mean = array.reduce((a, b) => a + b) / n;
return Math.sqrt(
array.map((x) => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n,
);
}
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:
| { stdev: number; avg: number; duration: number; samples: number[] }
| undefined;
const copyContent = async (text?: string | number) => {
if (!text) return;
if (typeof text !== "string") {
text = (Math.floor(text * 100) / 100).toString();
}
try {
await navigator.clipboard.writeText(text);
} catch (err) {
console.error("Failed to copy: ", err);
}
};
async function benchmark() {
if (isRunning) return;
isRunning = true;
result = undefined;
samples = 0;
$warmUp = 0;
await new Promise((r) => setTimeout(r, 50));
// warm up
for (let i = 0; i < warmUpAmount; i++) {
await run();
$warmUp = i + 1;
}
let a = performance.now();
let results = [];
// perform run
for (let i = 0; i < $amount; i++) {
const a = performance.now();
await run();
samples = i;
const b = performance.now();
await new Promise((r) => setTimeout(r, 20));
results.push(b - a);
}
result = {
stdev: calculateStandardDeviation(results),
samples: results,
duration: performance.now() - a,
avg: results.reduce((a, b) => a + b) / results.length,
};
}
</script>
{state}
<div class="wrapper" class:running={isRunning}>
{#if isRunning}
{#if result}
<h3>Finished ({humanizeDuration(result.duration)})</h3>
<div class="monitor-wrapper">
<Monitor points={result.samples} />
</div>
<label for="bench-avg">Average </label>
<button
id="bench-avg"
on:keydown={(ev) => ev.key === "Enter" && copyContent(result?.avg)}
on:click={() => copyContent(result?.avg)}
>{Math.floor(result.avg * 100) / 100}</button
>
<i
role="button"
tabindex="0"
on:keydown={(ev) => ev.key === "Enter" && copyContent(result?.avg)}
on:click={() => copyContent(result?.avg)}>(click to copy)</i
>
<label for="bench-stdev">Standard Deviation σ</label>
<button id="bench-stdev" on:click={() => copyContent(result?.stdev)}
>{Math.floor(result.stdev * 100) / 100}</button
>
<i
role="button"
tabindex="0"
on:keydown={(ev) => ev.key === "Enter" && copyContent(result?.avg)}
on:click={() => copyContent(result?.stdev + "")}>(click to copy)</i
>
<div>
<button on:click={() => (isRunning = false)}>reset</button>
</div>
{:else}
<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}
{: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;
}
.monitor-wrapper {
border: solid thin var(--outline);
border-bottom: none;
}
i {
opacity: 0.5;
font-size: 0.8em;
}
</style>

View File

@@ -0,0 +1,59 @@
<script lang="ts">
import type { Group } from "three";
import type { OBJExporter } from "three/addons/exporters/OBJExporter.js";
import type { GLTFExporter } from "three/addons/exporters/GLTFExporter.js";
import FileSaver from "file-saver";
// Download
const download = (
data: ArrayBuffer | string,
name: string,
mimetype: string,
extension: string,
) => {
const blob = new Blob([data], { type: mimetype + ";charset=utf-8" });
FileSaver.saveAs(blob, name + "." + extension);
};
export let scene: Group;
let gltfExporter: GLTFExporter;
async function exportGltf() {
const exporter =
gltfExporter ||
(await import("three/addons/exporters/GLTFExporter.js").then((m) => {
gltfExporter = new m.GLTFExporter();
return gltfExporter;
}));
exporter.parse(
scene,
(gltf) => {
// download .gltf file
download(gltf as ArrayBuffer, "plant", "text/plain", "gltf");
},
(err) => {
console.log(err);
},
);
}
let objExporter: 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);
// 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

@@ -0,0 +1,66 @@
<script lang="ts">
import type { createKeyMap } from "$lib/helpers/createKeyMap";
import { ShortCut } from "@nodes/ui";
import { get } from "svelte/store";
type Props = {
keymaps: {
keymap: ReturnType<typeof createKeyMap>;
title: string;
}[];
};
let { keymaps }: Props = $props();
console.log({ keymaps });
</script>
<table class="wrapper">
<tbody>
{#each keymaps as keymap}
<tr>
<td colspan="2">
<h3>{keymap.title}</h3>
</td>
</tr>
{#each get(keymap.keymap?.keys) as key}
<tr>
{#if key.description}
<td class="command-wrapper">
<ShortCut
alt={key.alt}
ctrl={key.ctrl}
shift={key.shift}
key={key.key}
/>
</td>
<td>{key.description}</td>
{/if}
</tr>
{/each}
{/each}
</tbody>
</table>
<style>
.wrapper {
padding: 1em;
}
h3 {
margin: 0;
}
.command-wrapper {
display: flex;
justify-content: right;
align-items: center;
}
td {
font-size: 0.9em;
margin: 0;
padding: 7px;
padding-left: 0;
align-items: center;
}
</style>