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.
90 lines
2.7 KiB
TypeScript
90 lines
2.7 KiB
TypeScript
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);
|
||
}
|
||
}
|