Files
feuerwerk/src/editor/inspector/portSection.ts
T
root 2cf788eedd Let a value be a bundle of named fields
PropertyValue was number | string | boolean | list. A struct joins it, which
costs two things the typechecker found rather than I did.

An object on a data edge used to mean a live entity, so asEntity would have
taken a struct for one and asString would have read .name off it. EntityHandle
now carries a symbol brand – a symbol so no field name can impersonate it, and
it never serializes, which suits a thing that exists only while the game runs.
It is built in exactly one place.

String(value) would have written a struct as [object Object] at 39 call sites.
They go through textOf(), which is identical for primitives.

normalizeGraph dropped everything but primitives from node params, so the List
node lost its value on every save ([3,1,4] round-tripped to undefined).
Variables were spared: they have a schema, params have none.
2026-09-10 08:14:18 +02:00

90 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
AddPortCommand,
nextPortName,
RemovePortCommand,
RenamePortCommand,
} from '../../core/commands/portCommands';
import { BEHAVIOR_START, DECLARED_PORT_KINDS, type DeclaredPortKind, type GraphNode } from '../../core/graph';
import { Button, Dropdown, HStack, Label, Menu, Spacer, TextField, Theme } from '../../ui';
import { addNote, type InspectorHost } from './host';
const KIND_LABELS: Record<DeclaredPortKind, string> = {
number: 'Number',
boolean: 'Boolean',
text: 'Text',
object: 'Object',
list: 'List',
struct: 'Struct',
};
const KIND_MARKS: Record<DeclaredPortKind, string> = {
number: '#',
boolean: '✓',
text: 'Aa',
object: '◇',
list: '≡',
struct: '⊞',
};
/**
* The ports a start or end node declares – the signature of the behavior seen as a node. Named from
* the caller's side: what the start node hands into the graph is an *input* of the behavior.
*/
export function buildPortSection(host: InspectorHost, assetId: string, node: GraphNode): void {
const onStart = node.type === BEHAVIOR_START;
const noun = onStart ? 'Input' : 'Output';
const header = new HStack({ gap: Theme.gap, align: 'center' });
header.add(new Label(`${noun.toUpperCase()}S`, { style: 'title' }), 1);
const add = new Dropdown(`+ ${noun}`, { variant: 'ghost' });
add.onClick = () => {
const items = DECLARED_PORT_KINDS.map((kind) => ({
label: KIND_LABELS[kind],
onSelect: () =>
host.state.execute(
new AddPortCommand(assetId, node.type, {
key: nextPortName(node, onStart ? 'in' : 'out'),
kind,
}),
),
}));
new Menu(items).openBelow(add);
};
header.add(add);
host.body.add(header);
const ports = node.ports ?? [];
if (ports.length === 0)
addNote(
host,
onStart
? 'No inputs yet. Every input added here becomes a port on the node that runs this behavior.'
: 'No outputs yet. Every output added here becomes a result of running this behavior.',
);
for (const port of ports) {
const row = new HStack({ gap: Theme.gap, align: 'center' });
row.minHeight = Theme.rowHeight;
const mark = new Label(KIND_MARKS[port.kind], { style: 'small', align: 'center' });
mark.minWidth = 18;
row.add(mark);
row.add(
new TextField(port.key, {
width: 90,
onChange: (next) =>
host.state.execute(new RenamePortCommand(assetId, node.type, port.key, next.trim())),
}),
1,
);
row.add(new Spacer());
row.add(
new Button('×', {
variant: 'ghost',
width: Theme.controlHeight,
onClick: () => host.state.execute(new RemovePortCommand(assetId, node.type, port.key)),
}),
);
host.body.add(row);
}
}