Refactoring: one file per node, consistent comments, shared helpers, split large modules
- Graph: nodes.ts split into nodes/<category>/<Type>.ts (62 files) with shared.ts factories and index.ts registry; NodeContext/values/NodeRegistry separated, writeTyped() replaces duplicated typed writes - Comments: one style (JSDoc on non-obvious exports, // for decisions and traps), wrong/stale comments fixed or removed, ids.ts now real 10 base-36 chars - Core: chainOf, shared worldPosition/editorPosition core, commands/require.ts, commands/naming.ts, util/list.ts, setBagValue/toPosition/propDef; dead code removed (isBehaviorAsset, SCENE_OBJECT_SCHEMA, CommandStack.subscribe, Input.axis, setMode, titleText, World.worldPosition, unused ctor params) - Editor: Inspector split into inspector/ sections, App.ts into Editor.ts + debugHook.ts + shortcuts.ts, Viewport into items.ts + symbols.ts, shared "Add collider" action, unified type labels/swatches - UI toolkit: Widget.trackHover/onPointerRelease, drawControlBox, math helpers, Popup.openBelow, Menu/Dialog/ColorPicker/ListRow in own files, named Theme sizes - Tools: seed-demo/smoke-demo and demo-check project removed, tools/lib (harness, projects, builders), e2e.mjs as runner over tools/e2e/*.mjs sections - Tests: tests/helpers.ts replaces copied fixtures and NodeContext doubles Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
3234eb480f
commit
1eb28314cf
189 files changed
+6231
-5543
No files matched your search
@@ -3,9 +3,10 @@ import { edgesInto, type Graph, type GraphNode, inlineInputs, inlineValue, type
|
||||
import type { BehaviorAsset } from '../../core/model/Asset';
|
||||
import { findAsset, type Project } from '../../core/model/Project';
|
||||
import type { PropertyDef } from '../../core/properties/PropertySchema';
|
||||
import { Theme, textStyle, Widget } from '../../ui';
|
||||
import { Theme, textStyle, type TextStyleToken, Widget } from '../../ui';
|
||||
import { keyLabel } from '../inspector/fieldFactory';
|
||||
|
||||
/** Node geometry in graph units; the editor relies on it for edge anchors and hit tests. */
|
||||
export const NODE_WIDTH = 176;
|
||||
export const TITLE_HEIGHT = 24;
|
||||
export const ROW_HEIGHT = 20;
|
||||
@@ -54,7 +55,7 @@ export class NodeView extends Widget {
|
||||
private paramLines: string[] = [];
|
||||
selected = false;
|
||||
hot = false;
|
||||
/** Whether a data input currently has a connection (inline value hidden). */
|
||||
/** Data inputs that currently have a connection (their inline value is hidden). */
|
||||
private connected = new Set<string>();
|
||||
|
||||
constructor(readonly node: GraphNode, readonly def: NodeDef) {
|
||||
@@ -64,22 +65,17 @@ export class NodeView extends Widget {
|
||||
this.sync(node, { nodes: [node], edges: [] }, null, null);
|
||||
}
|
||||
|
||||
get titleText(): string {
|
||||
return this.def.label;
|
||||
}
|
||||
|
||||
/** Ports sit on the left/right edges; widen the hit area so they can be grabbed. */
|
||||
override resize(w: number, h: number): void {
|
||||
super.resize(w, h);
|
||||
this.hitArea = new Rectangle(-PORT_HIT_RADIUS, 0, w + PORT_HIT_RADIUS * 2, h);
|
||||
}
|
||||
|
||||
/** Re-reads params and connections; rebuilds the text lines. `project` resolves asset references (Spawn). */
|
||||
/** Re-reads params and connections; rebuilds the text lines. `project` resolves asset references (Spawn, Is asset). */
|
||||
sync(node: GraphNode, graph: Graph, asset: BehaviorAsset | null, project: Project | null): void {
|
||||
this.paramLines = this.def.params.map((p) => `${p.label}: ${formatParam(p, node.params[p.key] ?? p.default, asset, project)}`);
|
||||
this.connected = new Set(inlineInputs(this.def).filter((p) => edgesInto(graph, node.id, p.key).length > 0).map((p) => p.key));
|
||||
const rows = this.rowCount();
|
||||
const height = TITLE_HEIGHT + PAD + this.paramLines.length * ROW_HEIGHT + rows * ROW_HEIGHT + PAD;
|
||||
const height = TITLE_HEIGHT + PAD + this.paramLines.length * ROW_HEIGHT + this.rowCount() * ROW_HEIGHT + PAD;
|
||||
this.rebuildTexts();
|
||||
this.resize(NODE_WIDTH, height);
|
||||
this.position.set(node.x, node.y);
|
||||
@@ -101,13 +97,17 @@ export class NodeView extends Widget {
|
||||
return TITLE_HEIGHT + PAD + this.paramLines.length * ROW_HEIGHT + index * ROW_HEIGHT + ROW_HEIGHT / 2;
|
||||
}
|
||||
|
||||
/** Every port with its local centre – inputs on the left edge, outputs on the right. */
|
||||
private *portCentres(): Iterable<{ port: PortDef; isOutput: boolean; x: number; y: number }> {
|
||||
for (const [list, isOutput] of [[this.def.inputs, false], [this.def.outputs, true]] as const) {
|
||||
for (let i = 0; i < list.length; i++) yield { port: list[i]!, isOutput, x: isOutput ? this.w : 0, y: this.rowY(i) };
|
||||
}
|
||||
}
|
||||
|
||||
/** Which port (if any) sits under a local point. */
|
||||
portAt(localX: number, localY: number): PortHit | null {
|
||||
for (const [list, isOutput] of [[this.def.inputs, false], [this.def.outputs, true]] as const) {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const px = isOutput ? this.w : 0;
|
||||
if (Math.hypot(localX - px, localY - this.rowY(i)) <= PORT_HIT_RADIUS) return { port: list[i]!, isOutput };
|
||||
}
|
||||
for (const { port, isOutput, x, y } of this.portCentres()) {
|
||||
if (Math.hypot(localX - x, localY - y) <= PORT_HIT_RADIUS) return { port, isOutput };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -115,12 +115,11 @@ export class NodeView extends Widget {
|
||||
private rebuildTexts(): void {
|
||||
for (const t of this.texts) t.destroy();
|
||||
this.texts.length = 0;
|
||||
const add = (text: string, style: Parameters<typeof textStyle>[0], x: number, y: number, align: 'left' | 'right' = 'left', maxWidth = NODE_WIDTH - PAD * 2) => {
|
||||
const add = (text: string, style: TextStyleToken, x: number, y: number, align: 'left' | 'right' = 'left', maxWidth = NODE_WIDTH - PAD * 2) => {
|
||||
const node = new Text({ text: fit(text, style, maxWidth), style: textStyle(style) });
|
||||
node.position.set(align === 'left' ? x : x - node.width, y);
|
||||
this.addChild(node);
|
||||
this.texts.push(node);
|
||||
return node;
|
||||
};
|
||||
add(this.def.label, 'accent', PAD + 2, (TITLE_HEIGHT - 16) / 2);
|
||||
let y = TITLE_HEIGHT + PAD;
|
||||
@@ -128,8 +127,7 @@ export class NodeView extends Widget {
|
||||
add(line, 'small', PAD + 2, y + 2);
|
||||
y += ROW_HEIGHT;
|
||||
}
|
||||
const rows = this.rowCount();
|
||||
for (let i = 0; i < rows; i++) {
|
||||
for (let i = 0; i < this.rowCount(); i++) {
|
||||
const input = this.def.inputs[i];
|
||||
const output = this.def.outputs[i];
|
||||
const rowTop = y + i * ROW_HEIGHT + 2;
|
||||
@@ -139,9 +137,9 @@ export class NodeView extends Widget {
|
||||
const value = inlineValue(this.node, input);
|
||||
if (value !== null && value !== '') label += ` ${formatValue(value)}`;
|
||||
}
|
||||
add(label || '', input.type === 'exec' ? 'body' : 'muted', PAD + PORT_RADIUS + 4, rowTop, 'left', NODE_WIDTH / 2 + 20);
|
||||
add(label, input.type === 'exec' ? 'body' : 'muted', PAD + PORT_RADIUS + 4, rowTop, 'left', NODE_WIDTH / 2 + 20);
|
||||
}
|
||||
if (output) add(output.label || '', output.type === 'exec' ? 'body' : 'muted', this.w - PAD - PORT_RADIUS - 4, rowTop, 'right', NODE_WIDTH / 2 - 10);
|
||||
if (output) add(output.label, output.type === 'exec' ? 'body' : 'muted', this.w - PAD - PORT_RADIUS - 4, rowTop, 'right', NODE_WIDTH / 2 - 10);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,21 +154,20 @@ export class NodeView extends Widget {
|
||||
this.box.roundRect(0.5, 0.5, this.w - 1, this.h - 1, Theme.radius).stroke({ width: this.selected || this.hot ? 2 : 1, color: border });
|
||||
|
||||
this.ports.clear();
|
||||
for (const [list, isOutput] of [[this.def.inputs, false], [this.def.outputs, true]] as const) {
|
||||
list.forEach((port, i) => {
|
||||
const x = isOutput ? this.w : 0;
|
||||
const y = this.rowY(i);
|
||||
if (port.type === 'exec') {
|
||||
this.ports.moveTo(x - 5, y - 5).lineTo(x + 1, y - 5).lineTo(x + 6, y).lineTo(x + 1, y + 5).lineTo(x - 5, y + 5).closePath().fill(portColor('exec'));
|
||||
} else {
|
||||
this.ports.circle(x, y, PORT_RADIUS).fill(portColor(port.type));
|
||||
this.ports.circle(x, y, PORT_RADIUS).stroke({ width: 1, color: c.node });
|
||||
}
|
||||
});
|
||||
const r = PORT_RADIUS;
|
||||
for (const { port, x, y } of this.portCentres()) {
|
||||
if (port.type === 'exec') {
|
||||
// A pentagon pointing right: the Blueprint-style exec pin.
|
||||
this.ports.moveTo(x - r, y - r).lineTo(x + 1, y - r).lineTo(x + r + 1, y).lineTo(x + 1, y + r).lineTo(x - r, y + r).closePath().fill(portColor('exec'));
|
||||
} else {
|
||||
this.ports.circle(x, y, r).fill(portColor(port.type));
|
||||
this.ports.circle(x, y, r).stroke({ width: 1, color: c.node });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The value of a setting as shown on the node; references that no longer resolve get a "?". */
|
||||
function formatParam(param: PropertyDef, value: unknown, asset: BehaviorAsset | null, project: Project | null): string {
|
||||
if (value === undefined || value === '') return '—';
|
||||
if (param.kind === 'key') return keyLabel(String(value));
|
||||
@@ -178,7 +175,7 @@ function formatParam(param: PropertyDef, value: unknown, asset: BehaviorAsset |
|
||||
const target = project ? findAsset(project, String(value)) : undefined;
|
||||
return target ? target.name : `${value}?`;
|
||||
}
|
||||
if (param.kind === 'enum' && param.key === 'variable' && asset && !asset.variables.some((v) => v.key === value) && asset.variables.length > 0 && String(value).length > 3) return `${value}?`;
|
||||
if (param.kind === 'enum' && param.key === 'variable' && asset && !asset.variables.some((v) => v.key === value)) return `${value}?`;
|
||||
return formatValue(value);
|
||||
}
|
||||
|
||||
@@ -188,7 +185,8 @@ function formatValue(value: unknown): string {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function fit(text: string, style: Parameters<typeof textStyle>[0], maxWidth: number): string {
|
||||
/** Truncates with an ellipsis so the text fits `maxWidth`. */
|
||||
function fit(text: string, style: TextStyleToken, maxWidth: number): string {
|
||||
const s = textStyle(style);
|
||||
if (CanvasTextMetrics.measureText(text, s).width <= maxWidth) return text;
|
||||
let cut = text;
|
||||
|
||||
Reference in new issue
Block a user