Add ESLint and Prettier, and close the node test gaps
Tooling: - Prettier as the single formatting authority (110 columns, single quotes, measured against the existing style); Markdown excluded because table-cell padding turns the README glossary into 1000-character lines. - Type-aware ESLint ending in eslint-config-prettier, so a rule always means "this is a bug", never "this looks different". - .editorconfig and an `npm run check` that runs typecheck, lint, format and tests in one go. Findings fixed on the first run: three more duplicate imports, four String(unknown) calls (parameter types narrowed instead of casts), two dead initialisers, two redundant casts, a console.log for a real error. Two warnings were false here and are disabled with a reason in place. Tests 132 -> 269: - node-catalog.test.ts covers every node with a run(), with a guard that fails when a new node has no row. - behavior-events.test.ts covers the ten event nodes through a running World, checking timing rather than existence. - Input's state machine (press/release/releaseAll) is reachable without DOM events now; the DOM handlers are thin adapters. Compare fell back to == without a configured operator although the schema default is <; aligned. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f140c2fd77
commit
d54a620ebd
113 files changed
+5049
-666
No files matched your search
@@ -1,10 +1,22 @@
|
||||
import { Container, type FederatedPointerEvent, type FederatedWheelEvent, Graphics, Text } from 'pixi.js';
|
||||
import { AddNodeCommand, ConnectCommand, DisconnectCommand, MoveNodeCommand, RemoveNodeCommand } from '../../core/commands/graphCommands';
|
||||
import { edgesInto, edgesOutOf, type GraphEdge, type NodeDef, nodeDef, portsCompatible } from '../../core/graph';
|
||||
import {
|
||||
AddNodeCommand,
|
||||
ConnectCommand,
|
||||
DisconnectCommand,
|
||||
MoveNodeCommand,
|
||||
RemoveNodeCommand,
|
||||
} from '../../core/commands/graphCommands';
|
||||
import {
|
||||
edgesInto,
|
||||
edgesOutOf,
|
||||
type GraphEdge,
|
||||
type NodeDef,
|
||||
nodeDef,
|
||||
portsCompatible,
|
||||
} from '../../core/graph';
|
||||
import type { BehaviorAsset } from '../../core/model/Asset';
|
||||
import { findAsset } from '../../core/model/Project';
|
||||
import type { Trace } from '../../runtime/systems/BehaviorSystem';
|
||||
import { TRACE_FADE } from '../../runtime/systems/BehaviorSystem';
|
||||
import { type Trace, TRACE_FADE } from '../../runtime/systems/BehaviorSystem';
|
||||
import { clamp, drawGrid, Menu, type Size, Theme, textStyle, Widget, zoomFactor } from '../../ui';
|
||||
import type { EditorState } from '../state';
|
||||
import { NodePalette } from './NodePalette';
|
||||
@@ -47,7 +59,12 @@ export class GraphEditor extends Widget {
|
||||
private pan = { x: 0, y: 0 };
|
||||
private zoom = 1;
|
||||
private panning: { startX: number; startY: number; panX: number; panY: number } | null = null;
|
||||
private dragging: { nodeId: string; start: { x: number; y: number }; pointer: { x: number; y: number }; moved: boolean } | null = null;
|
||||
private dragging: {
|
||||
nodeId: string;
|
||||
start: { x: number; y: number };
|
||||
pointer: { x: number; y: number };
|
||||
moved: boolean;
|
||||
} | null = null;
|
||||
private connecting: { from: PortAnchor; pointer: { x: number; y: number } } | null = null;
|
||||
/** Fit the view once the editor has a real size (it may be hidden behind the Scene tab). */
|
||||
private needsFit = true;
|
||||
@@ -78,7 +95,10 @@ export class GraphEditor extends Widget {
|
||||
});
|
||||
|
||||
state.on('selection', () => this.followSelection());
|
||||
state.on('load', () => { this.assetId = null; this.followSelection(); });
|
||||
state.on('load', () => {
|
||||
this.assetId = null;
|
||||
this.followSelection();
|
||||
});
|
||||
state.on('project', () => this.sync());
|
||||
state.on('mode', () => this.redraw());
|
||||
}
|
||||
@@ -183,11 +203,13 @@ export class GraphEditor extends Widget {
|
||||
if (e.target !== this) return;
|
||||
const local = this.toLocal(e.global);
|
||||
if (e.button === 2) {
|
||||
if (this.assetId && this.state.mode === 'edit') this.openPalette(e.global.x, e.global.y, this.screenToGraph(local.x, local.y));
|
||||
if (this.assetId && this.state.mode === 'edit')
|
||||
this.openPalette(e.global.x, e.global.y, this.screenToGraph(local.x, local.y));
|
||||
return;
|
||||
}
|
||||
if (e.button === 0 || e.button === 1) {
|
||||
if (e.button === 0 && this.assetId && this.state.mode === 'edit') this.state.select({ kind: 'asset', assetId: this.assetId });
|
||||
if (e.button === 0 && this.assetId && this.state.mode === 'edit')
|
||||
this.state.select({ kind: 'asset', assetId: this.assetId });
|
||||
this.panning = { startX: e.global.x, startY: e.global.y, panX: this.pan.x, panY: this.pan.y };
|
||||
}
|
||||
}
|
||||
@@ -205,13 +227,21 @@ export class GraphEditor extends Widget {
|
||||
if (e.button !== 0) return;
|
||||
if (hit) {
|
||||
const p = this.toLocal(e.global);
|
||||
this.connecting = { from: { nodeId: view.node.id, port: hit.port.key, isOutput: hit.isOutput }, pointer: this.screenToGraph(p.x, p.y) };
|
||||
this.connecting = {
|
||||
from: { nodeId: view.node.id, port: hit.port.key, isOutput: hit.isOutput },
|
||||
pointer: this.screenToGraph(p.x, p.y),
|
||||
};
|
||||
this.redraw();
|
||||
return;
|
||||
}
|
||||
this.state.select({ kind: 'node', assetId: this.assetId, nodeId: view.node.id });
|
||||
const p = this.toLocal(e.global);
|
||||
this.dragging = { nodeId: view.node.id, start: { x: view.node.x, y: view.node.y }, pointer: this.screenToGraph(p.x, p.y), moved: false };
|
||||
this.dragging = {
|
||||
nodeId: view.node.id,
|
||||
start: { x: view.node.x, y: view.node.y },
|
||||
pointer: this.screenToGraph(p.x, p.y),
|
||||
moved: false,
|
||||
};
|
||||
this.nodeLayer.addChild(view); // raise
|
||||
}
|
||||
|
||||
@@ -261,7 +291,13 @@ export class GraphEditor extends Widget {
|
||||
if (target && target.isOutput !== connecting.from.isOutput) {
|
||||
const out = connecting.from.isOutput ? connecting.from : target;
|
||||
const inp = connecting.from.isOutput ? target : connecting.from;
|
||||
this.state.execute(new ConnectCommand(this.assetId, { node: out.nodeId, port: out.port }, { node: inp.nodeId, port: inp.port }));
|
||||
this.state.execute(
|
||||
new ConnectCommand(
|
||||
this.assetId,
|
||||
{ node: out.nodeId, port: out.port },
|
||||
{ node: inp.nodeId, port: inp.port },
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
this.redraw();
|
||||
@@ -282,7 +318,8 @@ export class GraphEditor extends Widget {
|
||||
new NodePalette((def: NodeDef) => {
|
||||
if (!this.assetId) return;
|
||||
const command = new AddNodeCommand(this.assetId, def.type, at.x, at.y);
|
||||
if (this.state.execute(command)) this.state.select({ kind: 'node', assetId: this.assetId, nodeId: command.node.id });
|
||||
if (this.state.execute(command))
|
||||
this.state.select({ kind: 'node', assetId: this.assetId, nodeId: command.node.id });
|
||||
}).open(globalX, globalY);
|
||||
}
|
||||
|
||||
@@ -290,15 +327,37 @@ export class GraphEditor extends Widget {
|
||||
const assetId = this.assetId;
|
||||
if (!assetId) return;
|
||||
this.state.select({ kind: 'node', assetId, nodeId: view.node.id });
|
||||
new Menu([{ label: 'Delete node', danger: true, onSelect: () => this.state.execute(new RemoveNodeCommand(assetId, view.node.id)) }]).open(globalX, globalY);
|
||||
new Menu([
|
||||
{
|
||||
label: 'Delete node',
|
||||
danger: true,
|
||||
onSelect: () => this.state.execute(new RemoveNodeCommand(assetId, view.node.id)),
|
||||
},
|
||||
]).open(globalX, globalY);
|
||||
}
|
||||
|
||||
private openPortMenu(view: NodeView, port: string, isOutput: boolean, globalX: number, globalY: number): void {
|
||||
private openPortMenu(
|
||||
view: NodeView,
|
||||
port: string,
|
||||
isOutput: boolean,
|
||||
globalX: number,
|
||||
globalY: number,
|
||||
): void {
|
||||
const asset = this.asset();
|
||||
if (!asset) return;
|
||||
const edges = isOutput ? edgesOutOf(asset.graph, view.node.id, port) : edgesInto(asset.graph, view.node.id, port);
|
||||
const edges = isOutput
|
||||
? edgesOutOf(asset.graph, view.node.id, port)
|
||||
: edgesInto(asset.graph, view.node.id, port);
|
||||
if (edges.length === 0) return;
|
||||
new Menu([{ label: edges.length === 1 ? 'Disconnect' : `Disconnect ${edges.length} links`, danger: true, onSelect: () => { for (const edge of edges) this.state.execute(new DisconnectCommand(asset.id, edge.id)); } }]).open(globalX, globalY);
|
||||
new Menu([
|
||||
{
|
||||
label: edges.length === 1 ? 'Disconnect' : `Disconnect ${edges.length} links`,
|
||||
danger: true,
|
||||
onSelect: () => {
|
||||
for (const edge of edges) this.state.execute(new DisconnectCommand(asset.id, edge.id));
|
||||
},
|
||||
},
|
||||
]).open(globalX, globalY);
|
||||
}
|
||||
|
||||
// --- drawing -----------------------------------------------------------------------------
|
||||
@@ -320,7 +379,10 @@ export class GraphEditor extends Widget {
|
||||
return;
|
||||
}
|
||||
const playing = this.state.mode === 'play';
|
||||
this.hint.text = asset.graph.nodes.length === 0 ? `${asset.name} · right-click to add a node` : `${asset.name}${playing ? ' · live' : ''}`;
|
||||
this.hint.text =
|
||||
asset.graph.nodes.length === 0
|
||||
? `${asset.name} · right-click to add a node`
|
||||
: `${asset.name}${playing ? ' · live' : ''}`;
|
||||
|
||||
const trace = playing ? this.traceProvider(asset.id) : null;
|
||||
const now = this.timeProvider();
|
||||
@@ -362,7 +424,11 @@ export class GraphEditor extends Widget {
|
||||
const inType = to.def.inputs.find((p) => p.key === edge.to.port)?.type ?? 'any';
|
||||
const valid = portsCompatible(type, inType);
|
||||
bezier(this.edges, from.x + a.x, from.y + a.y, to.x + b.x, to.y + b.y);
|
||||
this.edges.stroke({ width: hot ? 4 : type === 'exec' ? 2.5 : 2, color: hot ? Theme.colors.hot : valid ? portColor(type) : Theme.colors.danger, alpha: type === 'exec' || hot ? 1 : 0.85 });
|
||||
this.edges.stroke({
|
||||
width: hot ? 4 : type === 'exec' ? 2.5 : 2,
|
||||
color: hot ? Theme.colors.hot : valid ? portColor(type) : Theme.colors.danger,
|
||||
alpha: type === 'exec' || hot ? 1 : 0.85,
|
||||
});
|
||||
}
|
||||
|
||||
private syncGrid(): void {
|
||||
@@ -371,7 +437,14 @@ export class GraphEditor extends Widget {
|
||||
if (step < MIN_GRID_STEP) return;
|
||||
// `pan` is where the graph origin sits on screen – but the graph has no origin worth
|
||||
// marking, so no axis lines.
|
||||
drawGrid(this.grid, { originX: this.pan.x, originY: this.pan.y, step, w: this.w, h: this.h, alpha: 0.04 });
|
||||
drawGrid(this.grid, {
|
||||
originX: this.pan.x,
|
||||
originY: this.pan.y,
|
||||
step,
|
||||
w: this.w,
|
||||
h: this.h,
|
||||
alpha: 0.04,
|
||||
});
|
||||
}
|
||||
|
||||
// --- test hooks -----------------------------------------------------------------------------
|
||||
|
||||
@@ -47,8 +47,18 @@ export class NodePalette extends Popup {
|
||||
private refresh(): void {
|
||||
const query = this.search.value.trim().toLowerCase();
|
||||
this.filtered = allNodeDefs()
|
||||
.filter((d) => !query || d.label.toLowerCase().includes(query) || d.type.toLowerCase().includes(query) || d.description.toLowerCase().includes(query))
|
||||
.sort((a, b) => CATEGORY_ORDER.indexOf(a.category) - CATEGORY_ORDER.indexOf(b.category) || a.label.localeCompare(b.label));
|
||||
.filter(
|
||||
(d) =>
|
||||
!query ||
|
||||
d.label.toLowerCase().includes(query) ||
|
||||
d.type.toLowerCase().includes(query) ||
|
||||
d.description.toLowerCase().includes(query),
|
||||
)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
CATEGORY_ORDER.indexOf(a.category) - CATEGORY_ORDER.indexOf(b.category) ||
|
||||
a.label.localeCompare(b.label),
|
||||
);
|
||||
this.list.setItems(this.filtered.map((d) => ({ id: d.type, label: d.label, hint: d.category })));
|
||||
this.scroll.relayoutContent();
|
||||
}
|
||||
|
||||
+112
-29
@@ -1,7 +1,20 @@
|
||||
import { CanvasTextMetrics, Graphics, Rectangle, Text } from 'pixi.js';
|
||||
import { edgesInto, type Graph, type GraphNode, inlineInputs, inlineValue, type NodeCategory, type NodeDef, type PortDef, type PortType } from '../../core/graph';
|
||||
import {
|
||||
asString,
|
||||
edgesInto,
|
||||
type Graph,
|
||||
type GraphNode,
|
||||
inlineInputs,
|
||||
inlineValue,
|
||||
type NodeCategory,
|
||||
type NodeDef,
|
||||
type PortDef,
|
||||
type PortType,
|
||||
type Value,
|
||||
} from '../../core/graph';
|
||||
import type { BehaviorAsset } from '../../core/model/Asset';
|
||||
import { findAsset, type Project } from '../../core/model/Project';
|
||||
import type { PropertyValue } from '../../core/model/Properties';
|
||||
import type { PropertyDef } from '../../core/properties/PropertySchema';
|
||||
import { Theme, textStyle, type TextStyleToken, Widget } from '../../ui';
|
||||
import { keyLabel } from '../inspector/fieldFactory';
|
||||
@@ -22,23 +35,34 @@ const PAD = 6;
|
||||
export function categoryColor(category: NodeCategory): number {
|
||||
const c = Theme.colors;
|
||||
switch (category) {
|
||||
case 'event': return c.categoryEvent;
|
||||
case 'flow': return c.categoryFlow;
|
||||
case 'data': return c.categoryData;
|
||||
case 'variable': return c.categoryVariable;
|
||||
case 'action': return c.categoryAction;
|
||||
case 'event':
|
||||
return c.categoryEvent;
|
||||
case 'flow':
|
||||
return c.categoryFlow;
|
||||
case 'data':
|
||||
return c.categoryData;
|
||||
case 'variable':
|
||||
return c.categoryVariable;
|
||||
case 'action':
|
||||
return c.categoryAction;
|
||||
}
|
||||
}
|
||||
|
||||
export function portColor(type: PortType): number {
|
||||
const c = Theme.colors;
|
||||
switch (type) {
|
||||
case 'exec': return c.portExec;
|
||||
case 'number': return c.portNumber;
|
||||
case 'boolean': return c.portBoolean;
|
||||
case 'string': return c.portString;
|
||||
case 'object': return c.portObject;
|
||||
case 'any': return c.portAny;
|
||||
case 'exec':
|
||||
return c.portExec;
|
||||
case 'number':
|
||||
return c.portNumber;
|
||||
case 'boolean':
|
||||
return c.portBoolean;
|
||||
case 'string':
|
||||
return c.portString;
|
||||
case 'object':
|
||||
return c.portObject;
|
||||
case 'any':
|
||||
return c.portAny;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +86,10 @@ export class NodeView extends Widget {
|
||||
/** Data inputs that currently have a connection (their inline value is hidden). */
|
||||
private connected = new Set<string>();
|
||||
|
||||
constructor(readonly node: GraphNode, readonly def: NodeDef) {
|
||||
constructor(
|
||||
readonly node: GraphNode,
|
||||
readonly def: NodeDef,
|
||||
) {
|
||||
super();
|
||||
this.cursor = 'grab';
|
||||
this.addChild(this.box, this.ports);
|
||||
@@ -77,9 +104,16 @@ export class NodeView extends Widget {
|
||||
|
||||
/** 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 height = TITLE_HEIGHT + PAD + this.paramLines.length * ROW_HEIGHT + this.rowCount() * ROW_HEIGHT + PAD;
|
||||
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 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);
|
||||
@@ -103,8 +137,12 @@ export class NodeView extends Widget {
|
||||
|
||||
/** 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) };
|
||||
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) };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +157,14 @@ 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: TextStyleToken, 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);
|
||||
@@ -141,9 +186,24 @@ 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,16 +213,27 @@ export class NodeView extends Widget {
|
||||
this.box.roundRect(2, 3, this.w, this.h, Theme.radius).fill({ color: 0x000000, alpha: 0.35 });
|
||||
this.box.roundRect(0, 0, this.w, this.h, Theme.radius).fill(c.node);
|
||||
this.box.roundRect(0, 0, this.w, TITLE_HEIGHT, Theme.radius).fill(categoryColor(this.def.category));
|
||||
this.box.rect(0, TITLE_HEIGHT - Theme.radius, this.w, Theme.radius).fill(categoryColor(this.def.category));
|
||||
this.box
|
||||
.rect(0, TITLE_HEIGHT - Theme.radius, this.w, Theme.radius)
|
||||
.fill(categoryColor(this.def.category));
|
||||
const border = this.hot ? c.hot : this.selected ? c.nodeSelected : c.nodeBorder;
|
||||
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.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();
|
||||
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'));
|
||||
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 });
|
||||
@@ -172,21 +243,32 @@ export class NodeView extends Widget {
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
function formatParam(
|
||||
param: PropertyDef,
|
||||
value: PropertyValue | undefined,
|
||||
asset: BehaviorAsset | null,
|
||||
project: Project | null,
|
||||
): string {
|
||||
if (value === undefined || value === '') return '—';
|
||||
if (param.kind === 'key') return keyLabel(String(value));
|
||||
if (param.kind === 'enum' && param.key === 'asset') {
|
||||
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)) return `${value}?`;
|
||||
if (
|
||||
param.kind === 'enum' &&
|
||||
param.key === 'variable' &&
|
||||
asset &&
|
||||
!asset.variables.some((v) => v.key === value)
|
||||
)
|
||||
return `${value}?`;
|
||||
return formatValue(value);
|
||||
}
|
||||
|
||||
function formatValue(value: unknown): string {
|
||||
function formatValue(value: Value): string {
|
||||
if (typeof value === 'number') return Number.isInteger(value) ? String(value) : value.toFixed(2);
|
||||
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
||||
return String(value);
|
||||
return asString(value);
|
||||
}
|
||||
|
||||
/** Truncates with an ellipsis so the text fits `maxWidth`. */
|
||||
@@ -194,6 +276,7 @@ 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;
|
||||
while (cut.length > 1 && CanvasTextMetrics.measureText(cut + '…', s).width > maxWidth) cut = cut.slice(0, -1);
|
||||
while (cut.length > 1 && CanvasTextMetrics.measureText(cut + '…', s).width > maxWidth)
|
||||
cut = cut.slice(0, -1);
|
||||
return cut + '…';
|
||||
}
|
||||
Reference in new issue
Block a user