feat: wire up planty with nodarium/app
This commit is contained in:
@@ -20,6 +20,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodarium/ui": "workspace:*",
|
"@nodarium/ui": "workspace:*",
|
||||||
"@nodarium/utils": "workspace:*",
|
"@nodarium/utils": "workspace:*",
|
||||||
|
"@nodarium/planty": "workspace:*",
|
||||||
"@sveltejs/kit": "^2.50.2",
|
"@sveltejs/kit": "^2.50.2",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@threlte/core": "8.3.1",
|
"@threlte/core": "8.3.1",
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@source "../../packages/ui/**/*.svelte";
|
@source "../../packages/ui/**/*.svelte";
|
||||||
|
@source "../../packages/planty/src/lib/**/*.svelte";
|
||||||
|
|
||||||
@plugin "@iconify/tailwind4" {
|
@plugin "@iconify/tailwind4" {
|
||||||
prefix: "i";
|
prefix: "i";
|
||||||
icon-sets: from-folder("custom", "./src/lib/icons");
|
icon-sets: from-folder("custom", "./src/lib/icons");
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ export { default as lottaNodes } from './lotta-nodes.json';
|
|||||||
export { plant } from './plant';
|
export { plant } from './plant';
|
||||||
export { default as simple } from './simple.json';
|
export { default as simple } from './simple.json';
|
||||||
export { tree } from './tree';
|
export { tree } from './tree';
|
||||||
|
export { default as tutorial } from './tutorial.json';
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"resolution.circle": 54,
|
"resolution.circle": 54,
|
||||||
"resolution.curve": 20,
|
"resolution.curve": 20,
|
||||||
"randomSeed": true
|
"randomSeed": false
|
||||||
},
|
},
|
||||||
"meta": {
|
"meta": {
|
||||||
"title": "New Project",
|
"title": "New Project",
|
||||||
@@ -27,9 +27,9 @@
|
|||||||
],
|
],
|
||||||
"type": "max/plantarium/stem",
|
"type": "max/plantarium/stem",
|
||||||
"props": {
|
"props": {
|
||||||
"amount": 50,
|
"amount": 4,
|
||||||
"length": 4,
|
"length": 4,
|
||||||
"thickness": 1
|
"thickness": 0.2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
24
app/src/lib/graph-templates/tutorial.json
Normal file
24
app/src/lib/graph-templates/tutorial.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"settings": {
|
||||||
|
"resolution.circle": 54,
|
||||||
|
"resolution.curve": 20,
|
||||||
|
"randomSeed": false
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"title": "New Project",
|
||||||
|
"lastModified": "2026-02-03T16:56:40.375Z"
|
||||||
|
},
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"position": [
|
||||||
|
215,
|
||||||
|
85
|
||||||
|
],
|
||||||
|
"type": "max/plantarium/output",
|
||||||
|
"props": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"edges": []
|
||||||
|
}
|
||||||
@@ -28,13 +28,14 @@
|
|||||||
key?: string;
|
key?: string;
|
||||||
value: SettingsValue;
|
value: SettingsValue;
|
||||||
type: SettingsType;
|
type: SettingsType;
|
||||||
|
onButtonClick?: (id: string) => void;
|
||||||
depth?: number;
|
depth?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Local persistent state for <details> sections
|
// Local persistent state for <details> sections
|
||||||
const openSections = localState<Record<string, boolean>>('open-details', {});
|
const openSections = localState<Record<string, boolean>>('open-details', {});
|
||||||
|
|
||||||
let { id, key = '', value = $bindable(), type, depth = 0 }: Props = $props();
|
let { id, key = '', value = $bindable(), type, onButtonClick, depth = 0 }: Props = $props();
|
||||||
|
|
||||||
function isNodeInput(v: SettingsNode | undefined): v is InputType {
|
function isNodeInput(v: SettingsNode | undefined): v is InputType {
|
||||||
return !!v && typeof v === 'object' && 'type' in v;
|
return !!v && typeof v === 'object' && 'type' in v;
|
||||||
@@ -107,11 +108,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleClick() {
|
|
||||||
const callback = value[key] as unknown as () => void;
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
open = openSections.value[id];
|
open = openSections.value[id];
|
||||||
|
|
||||||
@@ -130,7 +126,7 @@
|
|||||||
{@const inputType = type[key]}
|
{@const inputType = type[key]}
|
||||||
<div class="input input-{inputType.type}" class:first-level={depth === 1}>
|
<div class="input input-{inputType.type}" class:first-level={depth === 1}>
|
||||||
{#if inputType.type === 'button'}
|
{#if inputType.type === 'button'}
|
||||||
<button onclick={handleClick}>
|
<button onclick={() => onButtonClick?.(id)}>
|
||||||
{inputType.label || key}
|
{inputType.label || key}
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -143,6 +139,7 @@
|
|||||||
{:else if depth === 0}
|
{:else if depth === 0}
|
||||||
{#each Object.keys(type ?? {}).filter((k) => k !== 'title') as childKey (childKey)}
|
{#each Object.keys(type ?? {}).filter((k) => k !== 'title') as childKey (childKey)}
|
||||||
<NestedSettings
|
<NestedSettings
|
||||||
|
{onButtonClick}
|
||||||
id={`${id}.${childKey}`}
|
id={`${id}.${childKey}`}
|
||||||
key={childKey}
|
key={childKey}
|
||||||
bind:value
|
bind:value
|
||||||
@@ -160,6 +157,7 @@
|
|||||||
<div class="content">
|
<div class="content">
|
||||||
{#each Object.keys(type[key] as SettingsGroup).filter((k) => k !== 'title') as childKey (childKey)}
|
{#each Object.keys(type[key] as SettingsGroup).filter((k) => k !== 'title') as childKey (childKey)}
|
||||||
<NestedSettings
|
<NestedSettings
|
||||||
|
{onButtonClick}
|
||||||
id={`${id}.${childKey}`}
|
id={`${id}.${childKey}`}
|
||||||
key={childKey}
|
key={childKey}
|
||||||
bind:value={value[key] as SettingsValue}
|
bind:value={value[key] as SettingsValue}
|
||||||
@@ -221,6 +219,9 @@
|
|||||||
|
|
||||||
button {
|
button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
background: var(--color-layer-2);
|
||||||
|
padding-block: 5px;
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ export const AppSettingTypes = {
|
|||||||
label: 'Center Camera',
|
label: 'Center Camera',
|
||||||
value: true
|
value: true
|
||||||
},
|
},
|
||||||
|
clippy: {
|
||||||
|
type: 'button',
|
||||||
|
label: '🌱 Open Planty'
|
||||||
|
},
|
||||||
nodeInterface: {
|
nodeInterface: {
|
||||||
title: 'Node Interface',
|
title: 'Node Interface',
|
||||||
backgroundType: {
|
backgroundType: {
|
||||||
@@ -109,9 +113,8 @@ export const AppSettingTypes = {
|
|||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type SettingsToStore<T> = T extends { type: 'button' } ? () => void
|
type SettingsToStore<T> = T extends { value: infer V } ? V extends readonly string[] ? V[number]
|
||||||
: T extends { value: infer V } ? V extends readonly string[] ? V[number]
|
: V
|
||||||
: V
|
|
||||||
: T extends object ? {
|
: T extends object ? {
|
||||||
-readonly [K in keyof T as T[K] extends object ? K : never]: SettingsToStore<T[K]>;
|
-readonly [K in keyof T as T[K] extends object ? K : never]: SettingsToStore<T[K]>;
|
||||||
}
|
}
|
||||||
|
|||||||
264
app/src/lib/tutorial/tutorial-config.ts
Normal file
264
app/src/lib/tutorial/tutorial-config.ts
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
import type { PlantyConfig } from '@nodarium/planty';
|
||||||
|
|
||||||
|
export const tutorialConfig: PlantyConfig = {
|
||||||
|
id: 'nodarium-tutorial',
|
||||||
|
avatar: {
|
||||||
|
name: 'Planty',
|
||||||
|
defaultPosition: 'bottom-right'
|
||||||
|
},
|
||||||
|
start: 'intro',
|
||||||
|
nodes: {
|
||||||
|
// ── Entry ──────────────────────────────────────────────────────────────
|
||||||
|
intro: {
|
||||||
|
position: 'center',
|
||||||
|
text:
|
||||||
|
"# Hi, I'm Planty! 🌱\nI'll show you around Nodarium — a tool for building 3D plants by connecting nodes together.\nHow much detail do you want?",
|
||||||
|
choices: [
|
||||||
|
{ label: '🌱 Show me the basics', next: 'tour_canvas' },
|
||||||
|
{ label: '🤓 I want the technical details', next: 'tour_canvas_nerd' },
|
||||||
|
{ label: 'Skip the tour for now', next: null }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Simple path ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
tour_canvas: {
|
||||||
|
position: 'bottom-left',
|
||||||
|
hook: 'setup-default',
|
||||||
|
highlight: { selector: '.graph-wrapper', padding: 12 },
|
||||||
|
text:
|
||||||
|
'This is the **graph canvas**. Nodes connect together to build a plant — the 3D model updates automatically whenever you make a change.\nEach node does one specific job: grow stems, add noise, produce output.',
|
||||||
|
waitFor: 'click',
|
||||||
|
next: 'tour_viewer'
|
||||||
|
},
|
||||||
|
|
||||||
|
tour_viewer: {
|
||||||
|
position: 'top-left',
|
||||||
|
highlight: { selector: '.cell:first-child', padding: 8 },
|
||||||
|
text:
|
||||||
|
'This is the **3D viewer** — the live preview of your plant.\nLeft-click to rotate · right-click to pan · scroll to zoom.',
|
||||||
|
waitFor: 'click',
|
||||||
|
next: 'try_params'
|
||||||
|
},
|
||||||
|
|
||||||
|
try_params: {
|
||||||
|
position: 'bottom-right',
|
||||||
|
highlight: { selector: '.graph-wrapper', padding: 12 },
|
||||||
|
text:
|
||||||
|
'Click a node to select it. Its settings appear **directly on the node** — try changing a value and watch the plant update.\nThe sidebar shows extra hidden settings for the selected node.',
|
||||||
|
waitFor: 'click',
|
||||||
|
next: 'start_building'
|
||||||
|
},
|
||||||
|
|
||||||
|
start_building: {
|
||||||
|
position: 'center',
|
||||||
|
hook: 'load-tutorial-template',
|
||||||
|
text:
|
||||||
|
"Now let's build your own plant from scratch!\nI've loaded a blank project — it only has an **Output** node. Your goal: connect nodes to make a plant.",
|
||||||
|
waitFor: 'click',
|
||||||
|
next: 'add_stem_node'
|
||||||
|
},
|
||||||
|
|
||||||
|
add_stem_node: {
|
||||||
|
position: 'bottom-right',
|
||||||
|
highlight: { selector: '.graph-wrapper', padding: 12 },
|
||||||
|
text:
|
||||||
|
"Open the **Add Menu** with **Shift+A** or **right-click** on the canvas.\nAdd a **Stem** node, then connect its output socket to the Output node's input.",
|
||||||
|
waitFor: 'click',
|
||||||
|
next: 'add_noise_node'
|
||||||
|
},
|
||||||
|
|
||||||
|
add_noise_node: {
|
||||||
|
position: 'bottom-right',
|
||||||
|
highlight: { selector: '.graph-wrapper', padding: 12 },
|
||||||
|
text:
|
||||||
|
'Add a **Noise** node the same way.\nConnect: Stem → Noise input, then Noise output → Output.\nThis makes the stems grow in organic, curved shapes.',
|
||||||
|
waitFor: 'click',
|
||||||
|
next: 'add_random_node'
|
||||||
|
},
|
||||||
|
|
||||||
|
add_random_node: {
|
||||||
|
position: 'bottom-right',
|
||||||
|
highlight: { selector: '.graph-wrapper', padding: 12 },
|
||||||
|
text:
|
||||||
|
"Let's add some randomness! Add a **Random** node and connect its output to the **thickness** or **length** input of the Stem node.\nThe default min/max range is small — **Ctrl+drag** any number field to exceed its normal limits.",
|
||||||
|
waitFor: 'click',
|
||||||
|
next: 'prompt_regenerate'
|
||||||
|
},
|
||||||
|
|
||||||
|
prompt_regenerate: {
|
||||||
|
position: 'bottom-right',
|
||||||
|
highlight: { selector: '.graph-wrapper', padding: 12 },
|
||||||
|
text:
|
||||||
|
'Now press **R** to regenerate. Each press gives the Random node a new value — your plant changes every run!',
|
||||||
|
waitFor: 'click',
|
||||||
|
next: 'tour_sidebar'
|
||||||
|
},
|
||||||
|
|
||||||
|
tour_sidebar: {
|
||||||
|
position: 'right',
|
||||||
|
highlight: { selector: '.tabs', padding: 4 },
|
||||||
|
text:
|
||||||
|
'The **sidebar** holds all your tools:\n⚙️ Settings · ⌨️ Shortcuts · 📦 Export · 📁 Projects · 📊 Graph Settings\nEnable **Advanced Mode** in Settings to unlock performance and benchmark panels.',
|
||||||
|
waitFor: 'click',
|
||||||
|
next: 'save_project'
|
||||||
|
},
|
||||||
|
|
||||||
|
save_project: {
|
||||||
|
position: 'right',
|
||||||
|
highlight: { selector: '.tabs', padding: 4 },
|
||||||
|
text:
|
||||||
|
'Your work is saved in the **Projects panel** — rename it, create new projects, or switch between them anytime.',
|
||||||
|
waitFor: 'click',
|
||||||
|
next: 'congrats'
|
||||||
|
},
|
||||||
|
|
||||||
|
congrats: {
|
||||||
|
position: 'center',
|
||||||
|
text:
|
||||||
|
"# You're all set! 🎉\nYou know how to build plants, tweak parameters, and save your work.\nWant to explore more?",
|
||||||
|
choices: [
|
||||||
|
{ label: '🔗 How do node connections work?', next: 'connections_intro' },
|
||||||
|
{ label: '💡 Ideas for improving this plant', next: 'improvements_hint' },
|
||||||
|
{ label: '⌨️ Keyboard shortcuts', next: 'shortcuts_tour' },
|
||||||
|
{ label: "I'm ready to build!", next: null }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Technical / nerd path ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
tour_canvas_nerd: {
|
||||||
|
position: 'bottom-left',
|
||||||
|
hook: 'setup-default',
|
||||||
|
highlight: { selector: '.graph-wrapper', padding: 12 },
|
||||||
|
text:
|
||||||
|
"The **graph canvas** renders a directed acyclic graph. Each node is an individual **WASM module** executed in isolation — inputs in, output out. The 3D model updates automatically on every change.\nI've loaded a starter graph so you can see it in action.",
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
label: '🔍 Explore Node Sourcecode',
|
||||||
|
onclick: () => {
|
||||||
|
window.open(
|
||||||
|
'https://git.max-richter.dev/max/nodarium/src/branch/main/nodes/max/plantarium',
|
||||||
|
'__blank'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
waitFor: 'click',
|
||||||
|
next: 'tour_viewer_nerd'
|
||||||
|
},
|
||||||
|
|
||||||
|
tour_viewer_nerd: {
|
||||||
|
position: 'top-left',
|
||||||
|
highlight: { selector: '.cell:first-child', padding: 8 },
|
||||||
|
text:
|
||||||
|
'The **3D viewer** uses `@threlte/core` (Svelte + Three.js). Mesh data streams from WASM execution results. OrbitControls: left-drag rotate, right-drag pan, scroll zoom.',
|
||||||
|
waitFor: 'click',
|
||||||
|
next: 'tour_runtime_nerd'
|
||||||
|
},
|
||||||
|
|
||||||
|
tour_runtime_nerd: {
|
||||||
|
position: 'right',
|
||||||
|
text:
|
||||||
|
'By default, nodes execute in a **WebWorker** for better performance. You can switch to main-thread execution by disabling **Debug → Execute in WebWorker** in Settings.\nEnable **Advanced Mode** to unlock the Performance and Benchmark panels.',
|
||||||
|
waitFor: 'click',
|
||||||
|
next: 'start_building'
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Deep dives (shared between paths) ─────────────────────────────────
|
||||||
|
|
||||||
|
connections_intro: {
|
||||||
|
position: 'bottom-right',
|
||||||
|
text:
|
||||||
|
'Node sockets are **type-checked**. The coloured dots tell you what kind of data flows through:\n🔵 `number` · 🟢 `vec3` · 🟣 `shape` · ⚪ `*` (wildcard)',
|
||||||
|
waitFor: 'click',
|
||||||
|
next: 'connections_rules'
|
||||||
|
},
|
||||||
|
|
||||||
|
connections_rules: {
|
||||||
|
position: 'right',
|
||||||
|
text:
|
||||||
|
'Drag from an output socket to an input socket to connect them.\n• Types must match (or use `*`)\n• No circular loops\n• Optional inputs can stay empty\nInvalid connections snap back automatically.',
|
||||||
|
choices: [
|
||||||
|
{ label: '🔧 Node parameters', next: 'params_intro' },
|
||||||
|
{ label: '🐛 Debug node', next: 'debug_intro' },
|
||||||
|
{ label: 'Start building!', next: null }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
params_intro: {
|
||||||
|
position: 'bottom-right',
|
||||||
|
highlight: { selector: '.graph-wrapper', padding: 12 },
|
||||||
|
text:
|
||||||
|
'Click any node to select it. Basic settings are shown **on the node itself**.\nThe sidebar under *Graph Settings → Active Node* shows the full list:\n**Number** — drag or type · **Vec3** — X/Y/Z · **Select** — dropdown · **Color** — picker',
|
||||||
|
waitFor: 'click',
|
||||||
|
next: 'params_tip'
|
||||||
|
},
|
||||||
|
|
||||||
|
params_tip: {
|
||||||
|
position: 'right',
|
||||||
|
text:
|
||||||
|
'Pro tips:\n• Parameters can be connected from other nodes — drag an edge to the input socket\n• The **Random Seed** in Graph Settings gives you the same result every run\n• **f** key smart-connects two selected nodes · **Ctrl+Delete** removes a node and restores its edges',
|
||||||
|
choices: [
|
||||||
|
{ label: '🔗 How connections work', next: 'connections_intro' },
|
||||||
|
{ label: '💡 Plant improvement ideas', next: 'improvements_hint' },
|
||||||
|
{ label: 'Start building!', next: null }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
debug_intro: {
|
||||||
|
position: 'bottom-right',
|
||||||
|
text:
|
||||||
|
'Add a **Debug node** from the Add Menu (Shift+A or right-click). It accepts `*` wildcard inputs — connect any socket to inspect the data flowing through.\nEnable **Advanced Mode** in Settings to also see Performance and Graph Source panels.',
|
||||||
|
waitFor: 'click',
|
||||||
|
next: 'debug_done'
|
||||||
|
},
|
||||||
|
|
||||||
|
debug_done: {
|
||||||
|
position: 'center',
|
||||||
|
text: 'The Debug node is your best friend when building complex graphs.\nAnything else?',
|
||||||
|
choices: [
|
||||||
|
{ label: '🔗 Connection types', next: 'connections_intro' },
|
||||||
|
{ label: '🔧 Node parameters', next: 'params_intro' },
|
||||||
|
{ label: 'Start building!', next: null }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
shortcuts_tour: {
|
||||||
|
position: 'bottom-right',
|
||||||
|
text:
|
||||||
|
'**Essential shortcuts:**\n`R` — Regenerate\n`Shift+A` / right-click — Add node\n`f` — Smart-connect selected nodes\n`.` — Center camera\n`Ctrl+Z` / `Ctrl+Y` — Undo / Redo\n`Delete` — Remove selected · `Ctrl+Delete` — Remove and restore edges',
|
||||||
|
waitFor: 'click',
|
||||||
|
next: 'shortcuts_done'
|
||||||
|
},
|
||||||
|
|
||||||
|
shortcuts_done: {
|
||||||
|
position: 'right',
|
||||||
|
text:
|
||||||
|
'All shortcuts are also listed in the sidebar under the ⌨️ icon.\nReady to build something?',
|
||||||
|
choices: [
|
||||||
|
{ label: '🔗 Node connections', next: 'connections_intro' },
|
||||||
|
{ label: '🔧 Parameters', next: 'params_intro' },
|
||||||
|
{ label: "Let's build! 🌿", next: null }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
export_tour: {
|
||||||
|
position: 'right',
|
||||||
|
text:
|
||||||
|
'Export your 3D model from the **📦 Export** panel:\n**GLB** — standard for 3D apps (Blender, Three.js)\n**OBJ** — legacy format · **STL** — 3D printing · **PNG** — screenshot',
|
||||||
|
waitFor: 'click',
|
||||||
|
next: 'congrats'
|
||||||
|
},
|
||||||
|
|
||||||
|
improvements_hint: {
|
||||||
|
position: 'center',
|
||||||
|
text:
|
||||||
|
'# Ideas to grow your plant 🌿\n• Add a **Vec3** node → connect to *origin* on the Stem to spread stems across 3D space\n• Use a **Random** node on a parameter so each run produces a unique shape\n• Chain **multiple Stem nodes** with different settings for complex branching\n• Add a **Gravity** or **Branch** node for even more organic results',
|
||||||
|
choices: [
|
||||||
|
{ label: '⌨️ Keyboard shortcuts', next: 'shortcuts_tour' },
|
||||||
|
{ label: "Let's build! 🌿", next: null }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -22,13 +22,16 @@
|
|||||||
import ExportSettings from '$lib/sidebar/panels/ExportSettings.svelte';
|
import ExportSettings from '$lib/sidebar/panels/ExportSettings.svelte';
|
||||||
import GraphSource from '$lib/sidebar/panels/GraphSource.svelte';
|
import GraphSource from '$lib/sidebar/panels/GraphSource.svelte';
|
||||||
import Keymap from '$lib/sidebar/panels/Keymap.svelte';
|
import Keymap from '$lib/sidebar/panels/Keymap.svelte';
|
||||||
|
import { panelState } from '$lib/sidebar/PanelState.svelte';
|
||||||
import Sidebar from '$lib/sidebar/Sidebar.svelte';
|
import Sidebar from '$lib/sidebar/Sidebar.svelte';
|
||||||
|
import { tutorialConfig } from '$lib/tutorial/tutorial-config';
|
||||||
|
import { Planty } from '@nodarium/planty';
|
||||||
import type { Graph, NodeInstance } from '@nodarium/types';
|
import type { Graph, NodeInstance } from '@nodarium/types';
|
||||||
import { createPerformanceStore } from '@nodarium/utils';
|
import { createPerformanceStore } from '@nodarium/utils';
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import type { Group } from 'three';
|
import type { Group } from 'three';
|
||||||
|
|
||||||
let performanceStore = createPerformanceStore();
|
let performanceStore = createPerformanceStore();
|
||||||
|
let planty = $state<ReturnType<typeof Planty>>();
|
||||||
|
|
||||||
const { data } = $props();
|
const { data } = $props();
|
||||||
|
|
||||||
@@ -130,35 +133,65 @@
|
|||||||
|
|
||||||
const handleUpdate = debounceAsyncFunction(update);
|
const handleUpdate = debounceAsyncFunction(update);
|
||||||
|
|
||||||
onMount(() => {
|
function handleSettingsButton(id: string) {
|
||||||
appSettings.value.debug.stressTest = {
|
switch (id) {
|
||||||
...appSettings.value.debug.stressTest,
|
case 'general.clippy':
|
||||||
loadGrid: () => {
|
planty?.start();
|
||||||
|
break;
|
||||||
|
case 'general.debug.stressTest.loadGrid':
|
||||||
manager.load(
|
manager.load(
|
||||||
templates.grid(
|
templates.grid(
|
||||||
appSettings.value.debug.stressTest.amount,
|
appSettings.value.debug.stressTest.amount,
|
||||||
appSettings.value.debug.stressTest.amount
|
appSettings.value.debug.stressTest.amount
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
},
|
break;
|
||||||
loadTree: () => {
|
case 'general.debug.stressTest.loadTree':
|
||||||
manager.load(templates.tree(appSettings.value.debug.stressTest.amount));
|
manager.load(templates.tree(appSettings.value.debug.stressTest.amount));
|
||||||
},
|
break;
|
||||||
lottaFaces: () => {
|
case 'general.debug.stressTest.lottaFaces':
|
||||||
manager.load(templates.lottaFaces as unknown as Graph);
|
manager.load(templates.lottaFaces as unknown as Graph);
|
||||||
},
|
break;
|
||||||
lottaNodes: () => {
|
case 'general.debug.stressTest.lottaNodes':
|
||||||
manager.load(templates.lottaNodes as unknown as Graph);
|
manager.load(templates.lottaNodes as unknown as Graph);
|
||||||
},
|
break;
|
||||||
lottaNodesAndFaces: () => {
|
case 'general.debug.stressTest.lottaNodesAndFaces':
|
||||||
manager.load(templates.lottaNodesAndFaces as unknown as Graph);
|
manager.load(templates.lottaNodesAndFaces as unknown as Graph);
|
||||||
}
|
break;
|
||||||
};
|
default:
|
||||||
});
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:document onkeydown={applicationKeymap.handleKeyboardEvent} />
|
<svelte:document onkeydown={applicationKeymap.handleKeyboardEvent} />
|
||||||
|
|
||||||
|
<Planty
|
||||||
|
bind:this={planty}
|
||||||
|
config={tutorialConfig}
|
||||||
|
hooks={{
|
||||||
|
'setup-default': () => {
|
||||||
|
const ts = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
||||||
|
pm.handleCreateProject(
|
||||||
|
structuredClone(templates.defaultPlant) as unknown as Graph,
|
||||||
|
`Tutorial Project (${ts})`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
'load-tutorial-template': () => {
|
||||||
|
if (!pm.graph) return;
|
||||||
|
const g = structuredClone(templates.tutorial) as unknown as Graph;
|
||||||
|
g.id = pm.graph.id;
|
||||||
|
g.meta = { ...pm.graph.meta };
|
||||||
|
pm.graph = g;
|
||||||
|
pm.saveGraph(g);
|
||||||
|
},
|
||||||
|
'before:save_project': () => panelState.setActivePanel('projects'),
|
||||||
|
'before:export_tour': () => panelState.setActivePanel('exports'),
|
||||||
|
'before:shortcuts_tour': () => panelState.setActivePanel('shortcuts'),
|
||||||
|
'after:save_project': () => panelState.setActivePanel('graph-settings'),
|
||||||
|
'before:tour_runtime_nerd': () => panelState.setActivePanel('general')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="wrapper manager-{manager?.status}">
|
<div class="wrapper manager-{manager?.status}">
|
||||||
<header></header>
|
<header></header>
|
||||||
<Grid.Row>
|
<Grid.Row>
|
||||||
@@ -192,6 +225,7 @@
|
|||||||
<Panel id="general" title="General" icon="i-[tabler--settings]">
|
<Panel id="general" title="General" icon="i-[tabler--settings]">
|
||||||
<NestedSettings
|
<NestedSettings
|
||||||
id="general"
|
id="general"
|
||||||
|
onButtonClick={handleSettingsButton}
|
||||||
bind:value={appSettings.value}
|
bind:value={appSettings.value}
|
||||||
type={AppSettingTypes}
|
type={AppSettingTypes}
|
||||||
/>
|
/>
|
||||||
@@ -211,13 +245,15 @@
|
|||||||
<Panel id="exports" title="Exporter" icon="i-[tabler--package-export]">
|
<Panel id="exports" title="Exporter" icon="i-[tabler--package-export]">
|
||||||
<ExportSettings {scene} />
|
<ExportSettings {scene} />
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel
|
{#if false}
|
||||||
id="node-store"
|
<Panel
|
||||||
title="Node Store"
|
id="node-store"
|
||||||
icon="i-[tabler--database] bg-green-400"
|
title="Node Store"
|
||||||
>
|
icon="i-[tabler--database] bg-green-400"
|
||||||
<NodeStore registry={nodeRegistry} />
|
>
|
||||||
</Panel>
|
<NodeStore registry={nodeRegistry} />
|
||||||
|
</Panel>
|
||||||
|
{/if}
|
||||||
<Panel
|
<Panel
|
||||||
id="performance"
|
id="performance"
|
||||||
title="Performance"
|
title="Performance"
|
||||||
@@ -274,6 +310,25 @@
|
|||||||
<style>
|
<style>
|
||||||
header {
|
header {
|
||||||
background-color: var(--color-layer-1);
|
background-color: var(--color-layer-1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 18px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity 0.15s, background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrapper {
|
.wrapper {
|
||||||
@@ -281,7 +336,7 @@
|
|||||||
width: 100vw;
|
width: 100vw;
|
||||||
color: white;
|
color: white;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 0px 1fr;
|
grid-template-rows: 36px 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrapper :global(canvas) {
|
.wrapper :global(canvas) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { playwright } from '@vitest/browser-playwright';
|
|||||||
import comlink from 'vite-plugin-comlink';
|
import comlink from 'vite-plugin-comlink';
|
||||||
import glsl from 'vite-plugin-glsl';
|
import glsl from 'vite-plugin-glsl';
|
||||||
import wasm from 'vite-plugin-wasm';
|
import wasm from 'vite-plugin-wasm';
|
||||||
|
import path from 'path';
|
||||||
import { defineConfig } from 'vitest/config';
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@@ -19,6 +20,11 @@ export default defineConfig({
|
|||||||
comlink()
|
comlink()
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@nodarium/planty': path.resolve(__dirname, '../packages/planty/src/lib/index.ts')
|
||||||
|
}
|
||||||
|
},
|
||||||
ssr: {
|
ssr: {
|
||||||
noExternal: ['three']
|
noExternal: ['three']
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user