Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
3dba3c2b39
|
|||
|
09a9f8ce2c
|
|||
|
0b48740a85
|
|||
|
985b5179af
|
@@ -57,7 +57,6 @@ jobs:
|
|||||||
echo "${{ secrets.GIT_SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
|
echo "${{ secrets.GIT_SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
|
||||||
chmod 600 ~/.ssh/id_ed25519
|
chmod 600 ~/.ssh/id_ed25519
|
||||||
ssh-keyscan -p 2222 -H git.max-richter.dev >> ~/.ssh/known_hosts
|
ssh-keyscan -p 2222 -H git.max-richter.dev >> ~/.ssh/known_hosts
|
||||||
ssh -vvv -p 2222 -i ~/.ssh/id_ed25519 -T git@git.max-richter.dev
|
|
||||||
|
|
||||||
- name: 📤 Push Results
|
- name: 📤 Push Results
|
||||||
env:
|
env:
|
||||||
@@ -67,13 +66,13 @@ jobs:
|
|||||||
git config --global user.email "nodarium-bot@max-richter.dev"
|
git config --global user.email "nodarium-bot@max-richter.dev"
|
||||||
|
|
||||||
# 2. Clone the benchmarks repo into a temp folder
|
# 2. Clone the benchmarks repo into a temp folder
|
||||||
git config --global core.sshCommand "ssh -vv -p 2222 -i ~/.ssh/id_ed25519 -o IdentitiesOnly=yes"
|
git config --global core.sshCommand "ssh -p 2222 -i ~/.ssh/id_ed25519 -o IdentitiesOnly=yes"
|
||||||
git clone git@git.max-richter.dev:max/nodarium-benchmarks.git target_bench_repo
|
git clone git@git.max-richter.dev:max/nodarium-benchmarks.git target_bench_repo
|
||||||
|
|
||||||
# 3. Create a directory structure based on the branch
|
# 3. Create a directory structure based on the branch
|
||||||
# This allows the UI to "switch between branches"
|
# This allows the UI to "switch between branches"
|
||||||
BRANCH_NAME="${{ github.ref_name }}"
|
SAFE_PR_NAME="${GITHUB_HEAD_REF//\//-}"
|
||||||
DEST_DIR="target_bench_repo/data/$BRANCH_NAME/$(date +%s)"
|
DEST_DIR="target_bench_repo/data/$SAFE_PR_NAME/$(date +%s)"
|
||||||
mkdir -p "$DEST_DIR"
|
mkdir -p "$DEST_DIR"
|
||||||
|
|
||||||
# 4. Copy the new results
|
# 4. Copy the new results
|
||||||
@@ -83,5 +82,5 @@ jobs:
|
|||||||
# 5. Commit and Push
|
# 5. Commit and Push
|
||||||
cd target_bench_repo
|
cd target_bench_repo
|
||||||
git add .
|
git add .
|
||||||
git commit -m "Update benchmarks for $BRANCH_NAME: ${{ github.sha }}"
|
git commit -m "Update benchmarks for $SAFE_PR_NAME: ${{ github.sha }}"
|
||||||
git push origin main
|
git push origin main
|
||||||
|
|||||||
+26
-2
@@ -1,5 +1,5 @@
|
|||||||
import type { Graph, Graph as GraphType, NodeId } from '@nodarium/types';
|
import type { Graph, Graph as GraphType, NodeId } from '@nodarium/types';
|
||||||
import { createLogger, createPerformanceStore } from '@nodarium/utils';
|
import { createLogger, createPerformanceStore, splitNestedArray } from '@nodarium/utils';
|
||||||
import { mkdir, writeFile } from 'node:fs/promises';
|
import { mkdir, writeFile } from 'node:fs/promises';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
import { MemoryRuntimeExecutor } from '../src/lib/runtime/runtime-executor.ts';
|
import { MemoryRuntimeExecutor } from '../src/lib/runtime/runtime-executor.ts';
|
||||||
@@ -20,6 +20,26 @@ const templates: Record<string, Graph> = {
|
|||||||
'default': defaultPlantTemplate as unknown as GraphType
|
'default': defaultPlantTemplate as unknown as GraphType
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function countGeometry(result: Int32Array): { totalVertices: number; totalFaces: number } {
|
||||||
|
const parts = splitNestedArray(result);
|
||||||
|
let totalVertices = 0;
|
||||||
|
let totalFaces = 0;
|
||||||
|
for (const part of parts) {
|
||||||
|
const type = part[0];
|
||||||
|
const vertexCount = part[1];
|
||||||
|
const faceCount = part[2];
|
||||||
|
if (type === 2) {
|
||||||
|
const instanceCount = part[3];
|
||||||
|
totalVertices += vertexCount * instanceCount;
|
||||||
|
totalFaces += faceCount * instanceCount;
|
||||||
|
} else {
|
||||||
|
totalVertices += vertexCount;
|
||||||
|
totalFaces += faceCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { totalVertices, totalFaces };
|
||||||
|
}
|
||||||
|
|
||||||
async function run(g: GraphType, amount: number) {
|
async function run(g: GraphType, amount: number) {
|
||||||
await registry.load(plantTemplate.nodes.map(n => n.type) as NodeId[]);
|
await registry.load(plantTemplate.nodes.map(n => n.type) as NodeId[]);
|
||||||
log.log('loaded ' + g.nodes.length + ' nodes');
|
log.log('loaded ' + g.nodes.length + ' nodes');
|
||||||
@@ -33,10 +53,14 @@ async function run(g: GraphType, amount: number) {
|
|||||||
|
|
||||||
log.log('executing');
|
log.log('executing');
|
||||||
r.perf = perfStore;
|
r.perf = perfStore;
|
||||||
|
let res;
|
||||||
for (let i = 0; i < amount; i++) {
|
for (let i = 0; i < amount; i++) {
|
||||||
r.perf?.startRun();
|
r.perf?.startRun();
|
||||||
await r.execute(g, { randomSeed: true });
|
res = await r.execute(g, { randomSeed: true });
|
||||||
r.perf?.stopRun();
|
r.perf?.stopRun();
|
||||||
|
const { totalVertices, totalFaces } = countGeometry(res!);
|
||||||
|
r.perf?.addToLastRun('total-vertices', totalVertices);
|
||||||
|
r.perf?.addToLastRun('total-faces', totalFaces);
|
||||||
}
|
}
|
||||||
log.log('finished');
|
log.log('finished');
|
||||||
return r.perf.get();
|
return r.perf.get();
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
currentUndoGroup: number | null = null;
|
currentUndoGroup: number | null = null;
|
||||||
|
|
||||||
// Group-related state
|
// Group-related state
|
||||||
groups: Map<string, NodeGroupDefinition> = new Map();
|
groups = new SvelteMap<string, NodeGroupDefinition>();
|
||||||
groupNodeDefinitions: Map<string, NodeDefinition> = new Map();
|
groupNodeDefinitions: Map<string, NodeDefinition> = new Map();
|
||||||
currentGroupContext: string | null = null;
|
currentGroupContext: string | null = null;
|
||||||
graphStack: { rootGraph: Graph; groupId: string; cameraPosition: [number, number, number] }[] = $state([]);
|
graphStack: { rootGraph: Graph; groupId: string; cameraPosition: [number, number, number] }[] = $state([]);
|
||||||
@@ -113,12 +113,15 @@ export class GraphManager extends EventEmitter<{
|
|||||||
let merged: Graph = this.serialize();
|
let merged: Graph = this.serialize();
|
||||||
for (let i = this.graphStack.length - 1; i >= 0; i--) {
|
for (let i = this.graphStack.length - 1; i >= 0; i--) {
|
||||||
const { rootGraph, groupId } = $state.snapshot(this.graphStack[i]);
|
const { rootGraph, groupId } = $state.snapshot(this.graphStack[i]);
|
||||||
|
// Prefer the live definition (may have been updated via addGroupSocket/rename)
|
||||||
|
// over the snapshot taken when we entered the group.
|
||||||
|
const currentDef = (this.graph.groups ?? rootGraph.groups)?.[groupId];
|
||||||
merged = {
|
merged = {
|
||||||
...rootGraph,
|
...rootGraph,
|
||||||
groups: {
|
groups: {
|
||||||
...rootGraph.groups,
|
...rootGraph.groups,
|
||||||
[groupId]: {
|
[groupId]: {
|
||||||
...rootGraph.groups?.[groupId]!,
|
...(currentDef as NodeGroupDefinition),
|
||||||
graph: { nodes: merged.nodes, edges: merged.edges }
|
graph: { nodes: merged.nodes, edges: merged.edges }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,9 +145,13 @@ export class GraphManager extends EventEmitter<{
|
|||||||
return {
|
return {
|
||||||
id: `__virtual/group/${group.id}` as NodeId,
|
id: `__virtual/group/${group.id}` as NodeId,
|
||||||
meta: { title: group.name },
|
meta: { title: group.name },
|
||||||
inputs: Object.fromEntries(
|
inputs: {
|
||||||
|
// Placeholder for the group-selector dropdown — counted in height/socket math
|
||||||
|
'__virtual/groupId': { type: 'select' as const, internal: true, label: '' } as NodeInput,
|
||||||
|
...Object.fromEntries(
|
||||||
group.inputs.map(s => [s.name, { type: s.type, external: true } as NodeInput])
|
group.inputs.map(s => [s.name, { type: s.type, external: true } as NodeInput])
|
||||||
),
|
)
|
||||||
|
},
|
||||||
outputs: group.outputs.map(s => s.type),
|
outputs: group.outputs.map(s => s.type),
|
||||||
execute(input: Int32Array): Int32Array { return input; }
|
execute(input: Int32Array): Int32Array { return input; }
|
||||||
};
|
};
|
||||||
@@ -153,7 +160,12 @@ export class GraphManager extends EventEmitter<{
|
|||||||
buildGroupInputNodeDef(group: NodeGroupDefinition): NodeDefinition {
|
buildGroupInputNodeDef(group: NodeGroupDefinition): NodeDefinition {
|
||||||
return {
|
return {
|
||||||
id: '__virtual/group/input' as NodeId,
|
id: '__virtual/group/input' as NodeId,
|
||||||
inputs: {},
|
meta: { title: 'Input' },
|
||||||
|
// Each group input socket gets a labeled row (external = no control widget,
|
||||||
|
// internal = no left-side socket dot; it's an output-only node).
|
||||||
|
inputs: Object.fromEntries(
|
||||||
|
group.inputs.map(s => [s.name, { type: s.type, external: true, internal: true }])
|
||||||
|
) as Record<string, NodeInput>,
|
||||||
outputs: group.inputs.map(s => s.type),
|
outputs: group.inputs.map(s => s.type),
|
||||||
execute(input: Int32Array): Int32Array { return input; }
|
execute(input: Int32Array): Int32Array { return input; }
|
||||||
};
|
};
|
||||||
@@ -434,11 +446,19 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const group = this.groups.get(this.currentGroupContext);
|
const group = this.groups.get(this.currentGroupContext);
|
||||||
if (!group) return;
|
if (!group) return;
|
||||||
|
|
||||||
const arr = kind === 'input' ? group.inputs : group.outputs;
|
const oldArr = kind === 'input' ? group.inputs : group.outputs;
|
||||||
const name = `${kind}_${arr.length}`;
|
const name = `${kind}_${oldArr.length}`;
|
||||||
arr.push({ name, type: socketType });
|
const updatedGroup: NodeGroupDefinition = {
|
||||||
|
...group,
|
||||||
|
inputs: kind === 'input' ? [...oldArr, { name, type: socketType }] : group.inputs,
|
||||||
|
outputs: kind === 'output' ? [...oldArr, { name, type: socketType }] : group.outputs
|
||||||
|
};
|
||||||
|
|
||||||
this._refreshGroupContext(group);
|
if (!this.graph.groups) this.graph.groups = {};
|
||||||
|
this.graph.groups[this.currentGroupContext] = updatedGroup;
|
||||||
|
|
||||||
|
// Reinitialize so virtual group input/output nodes pick up the new socket reactively
|
||||||
|
this._init(this.serialize());
|
||||||
this.save();
|
this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,10 +467,17 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const group = this.groups.get(this.currentGroupContext);
|
const group = this.groups.get(this.currentGroupContext);
|
||||||
if (!group) return;
|
if (!group) return;
|
||||||
|
|
||||||
const arr = kind === 'input' ? group.inputs : group.outputs;
|
const oldArr = kind === 'input' ? group.inputs : group.outputs;
|
||||||
arr.splice(index, 1);
|
const updatedGroup: NodeGroupDefinition = {
|
||||||
|
...group,
|
||||||
|
inputs: kind === 'input' ? oldArr.filter((_, i) => i !== index) : group.inputs,
|
||||||
|
outputs: kind === 'output' ? oldArr.filter((_, i) => i !== index) : group.outputs
|
||||||
|
};
|
||||||
|
|
||||||
this._refreshGroupContext(group);
|
if (!this.graph.groups) this.graph.groups = {};
|
||||||
|
this.graph.groups[this.currentGroupContext] = updatedGroup;
|
||||||
|
|
||||||
|
this._init(this.serialize());
|
||||||
this.save();
|
this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -479,6 +506,51 @@ export class GraphManager extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pruneUnusedGroups() {
|
||||||
|
const usedIds = new Set<string>();
|
||||||
|
|
||||||
|
// Scan nodes in the current graph
|
||||||
|
for (const node of this.nodes.values()) {
|
||||||
|
if (node.type === '__virtual/group/instance') {
|
||||||
|
const gid = node.props?.groupId as string | undefined;
|
||||||
|
if (gid) usedIds.add(gid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan nodes in every stacked (parent) graph
|
||||||
|
for (const { rootGraph } of this.graphStack) {
|
||||||
|
for (const node of rootGraph.nodes) {
|
||||||
|
if (node.type === '__virtual/group/instance' && node.props?.groupId) {
|
||||||
|
usedIds.add(node.props.groupId as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan internal graphs of all known groups (nested group nodes)
|
||||||
|
for (const group of this.groups.values()) {
|
||||||
|
for (const node of group.graph.nodes) {
|
||||||
|
if (node.type === '__virtual/group/instance' && node.props?.groupId) {
|
||||||
|
usedIds.add(node.props.groupId as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let changed = false;
|
||||||
|
for (const groupId of [...this.groups.keys()]) {
|
||||||
|
if (!usedIds.has(groupId)) {
|
||||||
|
this.groups.delete(groupId);
|
||||||
|
this.groupNodeDefinitions.delete(`__virtual/group/${groupId}`);
|
||||||
|
if (this.graph.groups) delete this.graph.groups[groupId];
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
this.execute();
|
||||||
|
this.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Group navigation ---
|
// --- Group navigation ---
|
||||||
|
|
||||||
enterGroup(nodeId: number, cameraPosition: [number, number, number] = [0, 0, 4]): boolean {
|
enterGroup(nodeId: number, cameraPosition: [number, number, number] = [0, 0, 4]): boolean {
|
||||||
@@ -984,6 +1056,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
removeNode(node: NodeInstance, { restoreEdges = false } = {}) {
|
removeNode(node: NodeInstance, { restoreEdges = false } = {}) {
|
||||||
|
if (node.type === '__virtual/group/input' || node.type === '__virtual/group/output') return;
|
||||||
const edgesToNode = this.edges.filter((edge) => edge[2].id === node.id);
|
const edgesToNode = this.edges.filter((edge) => edge[2].id === node.id);
|
||||||
const edgesFromNode = this.edges.filter((edge) => edge[0].id === node.id);
|
const edgesFromNode = this.edges.filter((edge) => edge[0].id === node.id);
|
||||||
for (const edge of [...edgesToNode, ...edgesFromNode]) {
|
for (const edge of [...edgesToNode, ...edgesFromNode]) {
|
||||||
@@ -1211,11 +1284,9 @@ export class GraphManager extends EventEmitter<{
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't emit save event while navigating inside a group
|
const fullState = this.graphStack.length > 0 ? this.serializeFullGraph() : state;
|
||||||
if (this.graphStack.length > 0) return;
|
this.emit('save', fullState);
|
||||||
|
logger.log('saving graphs', fullState);
|
||||||
this.emit('save', state);
|
|
||||||
logger.log('saving graphs', state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getParentsOfNode(node: NodeInstance) {
|
getParentsOfNode(node: NodeInstance) {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
|
|
||||||
let meshRef: Mesh | undefined = $state();
|
let meshRef: Mesh | undefined = $state();
|
||||||
|
|
||||||
const height = getNodeHeight(node.state.type!);
|
const height = $derived(getNodeHeight(node.state.type!));
|
||||||
|
|
||||||
const zoom = $derived(graphState.cameraPosition[2]);
|
const zoom = $derived(graphState.cameraPosition[2]);
|
||||||
|
|
||||||
|
|||||||
@@ -37,16 +37,38 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (node.type === '__virtual/group/instance') {
|
if (node.type === '__virtual/group/instance') {
|
||||||
|
const groupOptions = [...(manager?.groups?.entries() ?? [])].map(([id, g]) => ({
|
||||||
|
label: g.name,
|
||||||
|
value: id
|
||||||
|
}));
|
||||||
|
// Remove the static placeholder from the definition (height-only) and replace
|
||||||
|
// with a fully dynamic version that carries current names + value.
|
||||||
|
parameters = parameters.filter(([key]) => key !== '__virtual/groupId');
|
||||||
parameters = [['__virtual/groupId', {
|
parameters = [['__virtual/groupId', {
|
||||||
type: 'select',
|
type: 'select',
|
||||||
value: node.props?.groupId as string,
|
value: node.props?.groupId as string,
|
||||||
options: [...manager?.groups?.keys()]
|
options: groupOptions
|
||||||
}], ...parameters];
|
}], ...parameters];
|
||||||
}
|
}
|
||||||
|
|
||||||
return parameters;
|
return parameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
const props = node.props as Record<string, unknown> | undefined;
|
||||||
|
const virtualGroupId = props?.['__virtual/groupId'] as string | undefined;
|
||||||
|
if (!virtualGroupId) return;
|
||||||
|
const activeGroupId = props?.groupId as string | undefined;
|
||||||
|
if (virtualGroupId === activeGroupId) return;
|
||||||
|
const newGroupDef = manager?.groupNodeDefinitions.get(`__virtual/group/${virtualGroupId}`);
|
||||||
|
if (!newGroupDef) return;
|
||||||
|
const { children, parents, ref } = node.state;
|
||||||
|
node.props = { ...props, groupId: virtualGroupId, '__virtual/groupId': virtualGroupId };
|
||||||
|
node.state = { type: newGroupDef, children, parents, ref };
|
||||||
|
manager?.execute();
|
||||||
|
manager?.save();
|
||||||
|
});
|
||||||
|
|
||||||
const parameters = $derived(buildParameters(node, node?.state?.type?.inputs || {}));
|
const parameters = $derived(buildParameters(node, node?.state?.type?.inputs || {}));
|
||||||
|
|
||||||
const currentGroupId = $derived((node.props?.groupId as string) ?? '');
|
const currentGroupId = $derived((node.props?.groupId as string) ?? '');
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
// select input: use index into options
|
// select input: use index into options
|
||||||
if ('options' in node && Array.isArray(node.options)) {
|
if ('options' in node && Array.isArray(node.options)) {
|
||||||
if (typeof inputValue === 'string') {
|
if (typeof inputValue === 'string') {
|
||||||
return node.options.indexOf(inputValue);
|
return (node.options as string[]).indexOf(inputValue);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,6 @@
|
|||||||
type Props = { manager: GraphManager; groupId: string };
|
type Props = { manager: GraphManager; groupId: string };
|
||||||
const { manager, groupId }: Props = $props();
|
const { manager, groupId }: Props = $props();
|
||||||
|
|
||||||
$inspect({ groupId });
|
|
||||||
|
|
||||||
const group = $derived(manager.groups.get(groupId));
|
const group = $derived(manager.groups.get(groupId));
|
||||||
|
|
||||||
const COMMON_TYPES = ['plant', 'float', 'int', 'vec3', 'bool'];
|
const COMMON_TYPES = ['plant', 'float', 'int', 'vec3', 'bool'];
|
||||||
@@ -34,6 +32,10 @@
|
|||||||
function removeSocket(index: number) {
|
function removeSocket(index: number) {
|
||||||
manager.removeGroupSocket('input', index);
|
manager.removeGroupSocket('input', index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function prune() {
|
||||||
|
manager.pruneUnusedGroups();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="bg-layer-2 flex items-center h-[70px] border-b-1 border-outline pl-4">
|
<div class="bg-layer-2 flex items-center h-[70px] border-b-1 border-outline pl-4">
|
||||||
@@ -85,6 +87,11 @@
|
|||||||
<button class="add-btn" onclick={addSocket}>+ Add</button>
|
<button class="add-btn" onclick={addSocket}>+ Add</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1.5">
|
||||||
|
<span class="section-label">Maintenance</span>
|
||||||
|
<button class="danger-btn" onclick={prune}>Prune unused groups</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -145,4 +152,24 @@
|
|||||||
.add-btn:hover {
|
.add-btn:hover {
|
||||||
outline-color: var(--color-selected);
|
outline-color: var(--color-selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.danger-btn {
|
||||||
|
background: var(--color-layer-2);
|
||||||
|
color: var(--color-text);
|
||||||
|
border: none;
|
||||||
|
outline: 1px solid var(--color-outline);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 0.4em 0.7em;
|
||||||
|
font-size: 0.8em;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
opacity: 0.7;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-btn:hover {
|
||||||
|
outline-color: #e05050;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -61,7 +61,10 @@ export const NodeInputBooleanSchema = z.object({
|
|||||||
export const NodeInputSelectSchema = z.object({
|
export const NodeInputSelectSchema = z.object({
|
||||||
...DefaultOptionsSchema.shape,
|
...DefaultOptionsSchema.shape,
|
||||||
type: z.literal('select'),
|
type: z.literal('select'),
|
||||||
options: z.array(z.string()).optional(),
|
options: z.union([
|
||||||
|
z.array(z.string()),
|
||||||
|
z.array(z.object({ label: z.string(), value: z.string() }))
|
||||||
|
]).optional(),
|
||||||
value: z.string().optional()
|
value: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
{:else if input.type === 'boolean'}
|
{:else if input.type === 'boolean'}
|
||||||
<InputCheckbox bind:value={value as boolean} {id} />
|
<InputCheckbox bind:value={value as boolean} {id} />
|
||||||
{:else if input.type === 'select'}
|
{:else if input.type === 'select'}
|
||||||
<InputSelect bind:value={value as number} options={input.options} {id} />
|
<InputSelect bind:value={value as number | string} options={input.options} {id} />
|
||||||
{:else if input.type === 'vec3'}
|
{:else if input.type === 'vec3'}
|
||||||
<InputVec3 bind:value={value as [number, number, number]} {id} />
|
<InputVec3 bind:value={value as [number, number, number]} {id} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,17 +1,28 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
type StringOption = string;
|
||||||
|
type LabeledOption = { label: string; value: string };
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
options?: string[];
|
options?: StringOption[] | LabeledOption[];
|
||||||
value?: number;
|
value?: number | string;
|
||||||
id?: string;
|
id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { options = [], value = $bindable(0), id = '' }: Props = $props();
|
let { options = [], value = $bindable<number | string>(0), id = '' }: Props = $props();
|
||||||
|
|
||||||
|
const isLabeled = $derived(options.length > 0 && typeof options[0] === 'object');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<select {id} bind:value class="bg-layer-2 text-text">
|
<select {id} bind:value class="bg-layer-2 text-text">
|
||||||
{#each options as label, i (label)}
|
{#if isLabeled}
|
||||||
<option value={i}>{label}</option>
|
{#each options as opt ((opt as LabeledOption).value)}
|
||||||
|
<option value={(opt as LabeledOption).value}>{(opt as LabeledOption).label}</option>
|
||||||
{/each}
|
{/each}
|
||||||
|
{:else}
|
||||||
|
{#each options as label, i (label)}
|
||||||
|
<option value={i}>{label as string}</option>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export interface PerformanceStore {
|
|||||||
startRun(): void;
|
startRun(): void;
|
||||||
stopRun(): void;
|
stopRun(): void;
|
||||||
addPoint(name: string, value?: number): void;
|
addPoint(name: string, value?: number): void;
|
||||||
|
addToLastRun(name: string, value: number): void;
|
||||||
endPoint(name?: string): void;
|
endPoint(name?: string): void;
|
||||||
mergeData(data: PerformanceData[number]): void;
|
mergeData(data: PerformanceData[number]): void;
|
||||||
get: () => PerformanceData;
|
get: () => PerformanceData;
|
||||||
@@ -63,6 +64,13 @@ export function createPerformanceStore(): PerformanceStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addToLastRun(name: string, value: number) {
|
||||||
|
const last = data[data.length - 1];
|
||||||
|
if (!last) return;
|
||||||
|
last[name] = last[name] || [];
|
||||||
|
last[name].push(value);
|
||||||
|
}
|
||||||
|
|
||||||
function get() {
|
function get() {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -94,6 +102,7 @@ export function createPerformanceStore(): PerformanceStore {
|
|||||||
startRun,
|
startRun,
|
||||||
stopRun,
|
stopRun,
|
||||||
addPoint,
|
addPoint,
|
||||||
|
addToLastRun,
|
||||||
endPoint,
|
endPoint,
|
||||||
mergeData,
|
mergeData,
|
||||||
get
|
get
|
||||||
|
|||||||
Reference in New Issue
Block a user