Trim needless comments and fix the picker width

Remove comments that only restate the code, keeping the section banners
and the notes that carry a reason, a measured fact or an external
contract.

ButtonOptions.width claimed to be a fixed width but only set minWidth,
so the Graph and Texture pickers grew with the name they showed. It is
now what it promises, with a separate minWidth for the buttons that have
two forms. Narrow buttons shrink their padding, otherwise a 24px icon
button had 8px left for its glyph and truncate dropped it entirely.

__gm.labelsDropped() guards that class: the overflow guard only saw text
that was too wide, never a label truncated to nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-08 15:52:05 +02:00
1 parent f16801aef3
commit 4d84231b9f
115 files changed
+707 -1564

No files matched your search

+3 -11
View File
@@ -1,9 +1,6 @@
import type { PropertyBag } from '../model/Properties';
/**
* Port data types. `exec` carries control flow (Blueprint-style white arrows); the others
* carry values. `any` accepts every value type (variables, logging).
*/
/** `exec` carries control flow, the others values; `any` accepts every value type. */
export type PortType = 'exec' | 'number' | 'boolean' | 'string' | 'object' | 'any';
export interface PortRef {
@@ -12,9 +9,8 @@ export interface PortRef {
}
/**
* One node of a behavior graph. `type` names a NodeDef in the registry; `params` holds
* the node's own settings (e.g. which key) and inline values of unconnected data inputs,
* both keyed by the param / port key.
* `type` names a NodeDef in the registry; `params` holds both the node's own settings and the
* inline values of unconnected data inputs, keyed by param or port key.
*/
export interface GraphNode {
id: string;
@@ -24,7 +20,6 @@ export interface GraphNode {
params: PropertyBag;
}
/** A connection from an output port to an input port. */
export interface GraphEdge {
id: string;
from: PortRef;
@@ -49,17 +44,14 @@ export function edgesInto(graph: Graph, nodeId: string, port: string): GraphEdge
return graph.edges.filter((e) => e.to.node === nodeId && e.to.port === port);
}
/** Edges leaving one output port. */
export function edgesOutOf(graph: Graph, nodeId: string, port: string): GraphEdge[] {
return graph.edges.filter((e) => e.from.node === nodeId && e.from.port === port);
}
/** Every edge touching a node (used when removing it). */
export function edgesOfNode(graph: Graph, nodeId: string): GraphEdge[] {
return graph.edges.filter((e) => e.from.node === nodeId || e.to.node === nodeId);
}
/** Whether a value may flow from an output of type `from` into an input of type `to`. */
export function portsCompatible(from: PortType, to: PortType): boolean {
if (from === 'exec' || to === 'exec') return from === to;
return from === to || from === 'any' || to === 'any';
+5 -14
View File
@@ -10,21 +10,17 @@ import type { Value } from './values';
const MAX_STEPS_PER_FIRE = 1000;
export interface TraceListener {
/** An exec edge was followed. */
onEdge(edgeId: string): void;
/** A node ran (exec) or was evaluated (data). */
onNode(nodeId: string): void;
}
/**
* Runs one behavior graph for one instance. Blueprint semantics: an event node starts a
* *pulse*; the interpreter follows exec edges, and whenever a node needs a data input it
* pulls the value from the connected output – evaluating pure data nodes lazily and
* reading exec nodes' outputs from the pulse cache. Per-node state (timers) lives here,
* so every instance has its own.
* Runs one behavior graph for one instance. An event node starts a *pulse*; the interpreter follows
* exec edges and pulls data inputs from the connected output, evaluating pure data nodes lazily and
* reading exec nodes' outputs from the pulse cache.
*/
export class GraphInterpreter {
/** Per-node persistent state across pulses (timer accumulators etc.). */
/** Per-node state that persists across pulses (timer accumulators etc.). */
readonly nodeState = new Map<string, Record<string, number>>();
private outputCache = new Map<string, Record<string, Value>>();
private steps = 0;
@@ -32,15 +28,11 @@ export class GraphInterpreter {
constructor(readonly graph: Graph) {}
/** Nodes of one event type, in graph order. */
eventNodes(type: string): GraphNode[] {
return this.graph.nodes.filter((n) => n.type === type);
}
/**
* Fires every event node of `type` whose params satisfy `filter`, seeding its outputs with
* `outputs` (e.g. `{ dt }`). Returns how many event nodes fired.
*/
/** Fires every event node of `type` passing `filter`, seeding its outputs with `outputs`. */
fire(
type: string,
ctx: NodeContext,
@@ -60,7 +52,6 @@ export class GraphInterpreter {
return fired;
}
/** Follows the exec edges leaving `port` of `node`, running each target in order. */
private follow(node: GraphNode, port: string, ctx: NodeContext): void {
for (const edge of edgesOutOf(this.graph, node.id, port)) {
const target = this.node(edge.to.node);
+14 -27
View File
@@ -3,12 +3,11 @@ import { sumAlongChain } from '../model/walk';
import { enabledAlongChain } from '../properties/enabled';
/**
* What a graph sees of a running entity. The runtime's Entity satisfies this shape; unit
* tests use plain objects. Mutating `x`/`y`/`props` is how actions take effect.
* What a graph sees of a running entity; the runtime's Entity satisfies this shape. Mutating
* `x`/`y`/`props` is how actions take effect.
*/
export interface EntityHandle {
id: string;
/** Asset this entity was instantiated from (what `Is asset` compares against). */
assetId: string;
name: string;
/** Position local to `parent` (world position for top-level entities). */
@@ -16,11 +15,9 @@ export interface EntityHandle {
y: number;
z: number;
props: PropertyBag;
/** The entity this one is parented to; children move with their parent. */
parent: EntityHandle | null;
}
/** World position of a handle: its local position plus those of all ancestors. */
export function worldPositionOf(handle: EntityHandle): { x: number; y: number; z: number } {
return sumAlongChain(
handle,
@@ -29,7 +26,7 @@ export function worldPositionOf(handle: EntityHandle): { x: number; y: number; z
);
}
/** Whether a handle and every ancestor of it are switched on (see `properties/enabled.ts`). */
/** Whether a handle and every ancestor of it are switched on. */
export function isHandleEnabled(handle: EntityHandle): boolean {
return enabledAlongChain(
handle,
@@ -38,11 +35,10 @@ export function isHandleEnabled(handle: EntityHandle): boolean {
);
}
/** Where the mouse is (world units) and what it did this frame. */
/** Position in world units. */
export interface PointerState {
x: number;
y: number;
/** Whether the pointer is over the game view at all. */
inside: boolean;
down: boolean;
/** Went down / up during this frame. */
@@ -51,45 +47,36 @@ export interface PointerState {
}
/**
* Services a graph may use while running. Implemented by the runtime (BehaviorSystem) and
* by test doubles. Everything the graph does to the world goes through here, so the core
* stays free of runtime imports.
* Everything the graph does to the world goes through here, so the core stays free of runtime
* imports. Implemented by the runtime (BehaviorSystem) and by test doubles.
*/
export interface NodeContext {
/** The behavior entity itself. */
self: EntityHandle;
/** The behavior's parent object (the one the graph acts on), if any. */
/** The behavior's parent object – the one the graph acts on. */
owner: EntityHandle | null;
/** Mutable variable state of this behavior instance. */
variables: PropertyBag;
/** Seconds since the world started and since the last tick. */
time: number;
dt: number;
findByName(name: string): EntityHandle | null;
isKeyDown(key: string): boolean;
/** Mouse state of this frame, in world units. */
pointer: PointerState;
/**
* Whether a world point lies on `target` – on its colliders when it has any, otherwise on
* its own footprint or that of anything parented below it (so a button made of a container
* with a sprite and a label is hit as a whole).
* Whether a world point lies on `target`: on its colliders when it has any, otherwise on its own
* footprint or that of anything parented below it.
*/
hitTest(target: EntityHandle, x: number, y: number): boolean;
/**
* Whether any collider of `a` overlaps any collider of `b` right now. A collider entity
* stands for itself; any other entity stands for its direct collider children.
*/
/** A collider entity stands for itself; any other entity for its direct collider children. */
isTouching(a: EntityHandle, b: EntityHandle): boolean;
/**
* Moves `target` by (dx, dy) in local units, stopping its solid colliders at other solid
* colliders axis by axis (x first, then y) so it slides along walls. Reports which axes hit.
* Moves `target` by (dx, dy) in local units, stopping solid colliders at other solid ones axis by
* axis (x first, then y) so it slides along walls.
*/
moveAndSlide(target: EntityHandle, dx: number, dy: number): { blockedX: boolean; blockedY: boolean };
log(message: string): void;
/**
* Places a new instance of an asset into the running scene and returns it. The position is
* local to `parent` (world when none). Returns null when the asset does not exist or cannot
* be placed (a scene). Spawned behaviors run their `On start` on the next tick.
* The position is local to `parent` (world when none); null when the asset does not exist or
* cannot be placed. A spawned behavior runs its `On start` on the next tick.
*/
spawn(
assetId: string,
+5 -12
View File
@@ -15,31 +15,25 @@ export interface PortDef {
export type NodeCategory = 'event' | 'flow' | 'data' | 'variable' | 'action';
export interface RunResult {
/** Values for the node's data outputs (cached for the rest of the pulse). */
/** Cached for the rest of the pulse. */
outputs?: Record<string, Value>;
/** Exec output(s) to continue along; omitted = the node ends the flow. */
next?: string | string[];
}
/**
* Describes one node type: its ports, its inline settings and how it runs. The editor
* palette, port rendering, connection validation and the inspector fields are all derived
* from this – adding a node type means adding one definition, no UI code.
*/
/** Palette, port rendering, connection validation and inspector fields are all derived from this. */
export interface NodeDef {
type: string;
label: string;
category: NodeCategory;
/** Shown in the palette and the inspector. */
description: string;
inputs: PortDef[];
outputs: PortDef[];
/** Node settings that are not ports (key name, operator, variable). */
params: PropertyDef[];
/**
* Executes the node. Pure data nodes (no exec pins) are evaluated on demand whenever an
* input downstream needs their value; exec nodes run when the flow reaches them.
* Event nodes have no `run`; the interpreter seeds their outputs when it fires them.
* Pure data nodes are evaluated on demand when a downstream input needs their value; exec nodes
* run when the flow reaches them. Event nodes have no `run` – the interpreter seeds their outputs.
*/
run?(ctx: NodeContext, inputs: Record<string, Value>, params: PropertyBag): RunResult;
}
@@ -74,12 +68,11 @@ export function hasExecInput(def: NodeDef): boolean {
return def.inputs.some((p) => p.type === 'exec');
}
/** Data inputs that may carry an inline value when unconnected (numbers, strings, booleans, any). */
/** Data inputs that may carry an inline value while unconnected. */
export function inlineInputs(def: NodeDef): PortDef[] {
return def.inputs.filter((p) => p.type !== 'exec' && p.type !== 'object');
}
/** A PropertyDef for editing the inline value of a port. */
export function inlineInputDef(port: PortDef): PropertyDef {
switch (port.type) {
case 'boolean':
+2 -2
View File
@@ -28,8 +28,8 @@ export const Compare: NodeDef = {
},
],
run: (_ctx, inputs, params) => {
// The fallback matches the param's declared default, so a node whose setting is missing
// (an old document, a hand-written graph) behaves like a freshly created one.
// The fallback matches the param's declared default, so a node with no setting behaves
// like a freshly created one.
const compare = OPERATORS[String(params.op)] ?? OPERATORS['<']!;
return { outputs: { result: compare(asNumber(inputs.a), asNumber(inputs.b)) } };
},
+2 -2
View File
@@ -68,8 +68,8 @@ import { GetVariable } from './variable/GetVariable';
import { SetVariable } from './variable/SetVariable';
/**
* The built-in node set, one definition per file. Importing this module registers them;
* the palette sorts by category and label, so the order here is documentation only.
* Importing this module registers the built-in nodes. The palette sorts by category and label, so
* the order here is documentation only.
*/
export const BUILT_IN_NODES: readonly NodeDef[] = [
// events
+1 -8
View File
@@ -12,7 +12,6 @@ export const str = (key: string, label = key): PortDef => ({ key, label, type: '
export const obj = (key: string, label = key): PortDef => ({ key, label, type: 'object' });
export const any = (key: string, label = key): PortDef => ({ key, label, type: 'any' });
/** The same port with an inline value it carries while unconnected. */
export const withDefault = (port: PortDef, value: PropertyValue): PortDef => ({ ...port, default: value });
/** Outputs of the three collision events: the other body, its collider and the own collider. */
@@ -36,15 +35,11 @@ export const variableParam: PropertyDef = {
options: [],
};
/**
* A reference to an asset the node acts on (spawns, compares against). No `assetTypes`: any
* placeable asset is a valid answer, and restricting them would be guessing for the user.
*/
/** No `assetTypes`: any placeable asset is a valid answer here. */
export const assetParam: PropertyDef = { key: 'asset', label: 'Asset', kind: 'asset', default: '' };
// --- node factories --------------------------------------------------------------------------
/** A data node computing one number from `a` and `b`. */
export function binaryMath(type: string, description: string, fn: (a: number, b: number) => number): NodeDef {
return {
type,
@@ -58,7 +53,6 @@ export function binaryMath(type: string, description: string, fn: (a: number, b:
};
}
/** A data node computing one number from `value`. */
export function unaryMath(type: string, description: string, fn: (value: number) => number): NodeDef {
return {
type,
@@ -72,7 +66,6 @@ export function unaryMath(type: string, description: string, fn: (value: number)
};
}
/** A data node combining the booleans `a` and `b`. */
export function binaryLogic(
type: string,
description: string,