feat: migrate some more stuff to svelte-5, mainly app settings
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 4s
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 4s
This commit is contained in:
@@ -94,6 +94,7 @@
|
||||
|
||||
.content {
|
||||
background: var(--layer-1);
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
max-height: 100vh;
|
||||
overflow-y: auto;
|
||||
|
||||
153
app/src/lib/settings/app-settings.svelte.ts
Normal file
153
app/src/lib/settings/app-settings.svelte.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { localState } from "$lib/helpers/localState.svelte";
|
||||
import type { NodeInput } from "@nodes/types";
|
||||
|
||||
const themes = ["dark", "light", "catppuccin", "solarized", "high-contrast", "nord", "dracula"];
|
||||
|
||||
export const AppSettingTypes = {
|
||||
theme: {
|
||||
type: "select",
|
||||
options: themes,
|
||||
label: "Theme",
|
||||
value: themes[0],
|
||||
},
|
||||
showGrid: {
|
||||
type: "boolean",
|
||||
label: "Show Grid",
|
||||
value: true,
|
||||
},
|
||||
centerCamera: {
|
||||
type: "boolean",
|
||||
label: "Center Camera",
|
||||
value: true
|
||||
},
|
||||
nodeInterface: {
|
||||
title: "Node Interface",
|
||||
showNodeGrid: {
|
||||
type: "boolean",
|
||||
label: "Show Grid",
|
||||
value: true
|
||||
},
|
||||
snapToGrid: {
|
||||
type: "boolean",
|
||||
label: "Snap to Grid",
|
||||
value: true
|
||||
},
|
||||
showHelp: {
|
||||
type: "boolean",
|
||||
label: "Show Help",
|
||||
value: false
|
||||
}
|
||||
},
|
||||
debug: {
|
||||
wireframe: {
|
||||
type: "boolean",
|
||||
label: "Wireframe",
|
||||
value: false,
|
||||
},
|
||||
useWorker: {
|
||||
type: "boolean",
|
||||
label: "Execute runtime in worker",
|
||||
value: true,
|
||||
},
|
||||
showIndices: {
|
||||
type: "boolean",
|
||||
label: "Show Indices",
|
||||
value: false,
|
||||
},
|
||||
showPerformancePanel: {
|
||||
type: "boolean",
|
||||
label: "Show Performance Panel",
|
||||
value: false,
|
||||
},
|
||||
showBenchmarkPanel: {
|
||||
type: "boolean",
|
||||
label: "Show Benchmark Panel",
|
||||
value: false,
|
||||
},
|
||||
showVertices: {
|
||||
type: "boolean",
|
||||
label: "Show Vertices",
|
||||
value: false,
|
||||
},
|
||||
showStemLines: {
|
||||
type: "boolean",
|
||||
label: "Show Stem Lines",
|
||||
value: false,
|
||||
},
|
||||
stressTest: {
|
||||
title: "Stress Test",
|
||||
amount: {
|
||||
type: "integer",
|
||||
min: 2,
|
||||
max: 15
|
||||
},
|
||||
loadGrid: {
|
||||
type: "button",
|
||||
label: "Load Grid"
|
||||
},
|
||||
loadTree: {
|
||||
type: "button",
|
||||
label: "Load Tree"
|
||||
},
|
||||
lottaFaces: {
|
||||
type: "button",
|
||||
label: "Load 'lots of faces'"
|
||||
},
|
||||
lottaNodes: {
|
||||
type: "button",
|
||||
label: "Load 'lots of nodes'"
|
||||
},
|
||||
lottaNodesAndFaces: {
|
||||
type: "button",
|
||||
label: "Load 'lots of nodes and faces'"
|
||||
}
|
||||
},
|
||||
}
|
||||
} as const
|
||||
|
||||
type IsInputDefinition<T> = T extends NodeInput ? T : never;
|
||||
type HasTitle = { title: string };
|
||||
type ExtractSettingsValues<T> = {
|
||||
[K in keyof T]: T[K] extends HasTitle
|
||||
? ExtractSettingsValues<Omit<T[K], 'title'>>
|
||||
: T[K] extends IsInputDefinition<T[K]>
|
||||
? T[K] extends { value: any }
|
||||
? T[K]['value']
|
||||
: never
|
||||
: T[K] extends Record<string, any>
|
||||
? ExtractSettingsValues<T[K]>
|
||||
: never;
|
||||
};
|
||||
|
||||
function settingsToStore<T>(settings: T): ExtractSettingsValues<T> {
|
||||
const result = {} as any;
|
||||
for (const key in settings) {
|
||||
const value = settings[key];
|
||||
if (value && typeof value === 'object') {
|
||||
if ('value' in value) {
|
||||
result[key] = value.value;
|
||||
} else {
|
||||
result[key] = settingsToStore(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export const appSettings = localState("app-settings", settingsToStore(AppSettingTypes));
|
||||
|
||||
$effect.root(() => {
|
||||
$effect(() => {
|
||||
const { theme } = $state.snapshot(appSettings);
|
||||
const classes = document.body.parentElement?.classList;
|
||||
const newClassName = `theme-${theme}`;
|
||||
if (classes) {
|
||||
for (const className of classes) {
|
||||
if (className.startsWith("theme-") && className !== newClassName) {
|
||||
classes.remove(className);
|
||||
}
|
||||
}
|
||||
}
|
||||
document.body?.parentElement?.classList.add(newClassName);
|
||||
});
|
||||
});
|
||||
@@ -11,7 +11,7 @@
|
||||
function filterInputs(inputs: Record<string, NodeInput>) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(inputs)
|
||||
.filter(([key, value]) => {
|
||||
.filter(([_key, value]) => {
|
||||
return value.hidden === true;
|
||||
})
|
||||
.map(([key, value]) => {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import type { OBJExporter } from "three/addons/exporters/OBJExporter.js";
|
||||
import type { GLTFExporter } from "three/addons/exporters/GLTFExporter.js";
|
||||
import FileSaver from "file-saver";
|
||||
import { appSettings } from "../app-settings.svelte";
|
||||
|
||||
// Download
|
||||
const download = (
|
||||
@@ -51,6 +52,8 @@
|
||||
// download .obj file
|
||||
download(result, "plant", "text/plain", "obj");
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<div class="p-2">
|
||||
|
||||
@@ -1,70 +1,100 @@
|
||||
<script lang="ts">
|
||||
import localStore from "$lib/helpers/localStore";
|
||||
import NestedSettings from "./NestedSettings.svelte";
|
||||
import {localState} from "$lib/helpers/localState.svelte";
|
||||
import type { NodeInput } from "@nodes/types";
|
||||
import Input from "@nodes/ui";
|
||||
import type { Writable } from "svelte/store";
|
||||
|
||||
type Button = { type: "button"; label?: string; callback: () => void };
|
||||
type Button = { type: "button"; label?: string };
|
||||
|
||||
type InputType = NodeInput | Button;
|
||||
|
||||
interface Nested {
|
||||
[key: string]: (Nested & { __title?: string }) | InputType;
|
||||
[key: string]: (Nested & { title?: string }) | InputType;
|
||||
}
|
||||
|
||||
export let id: string;
|
||||
type Props = {
|
||||
id: string;
|
||||
key?: string;
|
||||
value: Record<string, unknown> | string | number | boolean;
|
||||
type: Nested;
|
||||
depth?: number;
|
||||
};
|
||||
|
||||
$: expandedDetails = localStore<Record<string, boolean>>(
|
||||
`nodes.settings.expanded.${id}`,
|
||||
{},
|
||||
);
|
||||
let { id, key = "", value = $bindable(), type, depth = 0 }: Props = $props();
|
||||
|
||||
export let settings: Nested;
|
||||
export let store: Writable<Record<string, any>>;
|
||||
export let depth = 0;
|
||||
|
||||
const keys = Object.keys(settings).filter((key) => key !== "__title");
|
||||
function isNodeInput(v: InputType | Nested): v is InputType {
|
||||
return v && "type" in v;
|
||||
}
|
||||
|
||||
let internalValue = $state(Array.isArray(type?.[key]?.options) ? type[key]?.options?.indexOf(value?.[key]) : value?.[key]);
|
||||
|
||||
let openSections = localState("open-details", {});
|
||||
let open = $state(openSections[id]);
|
||||
if(depth > 0 && !isNodeInput(type[key])){
|
||||
$effect(() => {
|
||||
if(open !== undefined){}
|
||||
openSections[id] = open;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
$effect(() => {
|
||||
if(key === "" || internalValue === undefined) return;
|
||||
if(isNodeInput(type[key]) && Array.isArray(type[key]?.options) && typeof internalValue === "number"){
|
||||
value[key] = type[key].options?.[internalValue];
|
||||
}else{
|
||||
value[key] = internalValue;
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if $store}
|
||||
{#each keys as key}
|
||||
{@const value = settings[key]}
|
||||
<div class="wrapper" class:first-level={depth === 0}>
|
||||
{#if value !== undefined && isNodeInput(value)}
|
||||
<div
|
||||
class="input input-{settings[key].type}"
|
||||
data-node-type={value?.__node_type || null}
|
||||
data-node-input={value?.__node_input || null}
|
||||
>
|
||||
{#if value.type === "button"}
|
||||
<button on:click={() => value?.callback?.()}
|
||||
>{value.label || key}</button
|
||||
>
|
||||
{:else if "setting" in value && value.setting !== undefined}
|
||||
<label for={key}>{settings[key].label || key}</label>
|
||||
<Input id={key} input={value} bind:value={$store[value?.setting]} />
|
||||
{:else}
|
||||
<label for={key}>{settings[key].label || key}</label>
|
||||
<Input id={key} input={value} bind:value={$store[key]} />
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
{#if depth > 0}
|
||||
<hr />
|
||||
{/if}
|
||||
{#if key && isNodeInput(type?.[key]) }
|
||||
<div class="input input-{type[key].type}">
|
||||
{#if type[key].type === "button"}
|
||||
<button onclick={() => console.log(type[key])}>
|
||||
{type[key].label || key}
|
||||
</button>
|
||||
{:else}
|
||||
<label for={id}>{type[key].label || key}</label>
|
||||
<Input id={id} input={type[key]} bind:value={internalValue} />
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
{#if depth === 0}
|
||||
{#each Object.keys(type).filter((key) => key !== "title") as childKey}
|
||||
<NestedSettings
|
||||
id={`${id}.${childKey}`}
|
||||
key={childKey}
|
||||
value={value as Record<string, unknown>}
|
||||
type={type as Nested}
|
||||
depth={depth + 1}
|
||||
/>
|
||||
{/each}
|
||||
{#if depth > 0}
|
||||
<hr />
|
||||
{/if}
|
||||
{:else if key && type?.[key]}
|
||||
{#if depth > 0}
|
||||
<hr />
|
||||
{/if}
|
||||
<details bind:open>
|
||||
<summary>{type[key]?.title||key}</summary>
|
||||
<div class="content">
|
||||
{#each Object.keys(type[key]).filter((key) => key !== "title") as childKey}
|
||||
<NestedSettings
|
||||
id={`${id}.${childKey}`}
|
||||
key={childKey}
|
||||
value={value[key] as Record<string, unknown>}
|
||||
type={type[key] as Nested}
|
||||
depth={depth + 1}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
|
||||
{/if}
|
||||
|
||||
<details bind:open={$expandedDetails[key]}>
|
||||
<summary>{settings[key]?.__title || key}</summary>
|
||||
<div class="content">
|
||||
<svelte:self settings={settings[key]} {store} depth={depth + 1} />
|
||||
</div>
|
||||
</details>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
|
||||
Reference in New Issue
Block a user