Keep an output's name readable beside a long inline value (#306)
An input and an output in the same port row were cut to fit each on its own. The input text could reach x = 123 and the output label could start at x = 83, so a long inline value ran under the output's name. At Animation playing a long animation name covered "progress". - The output label is placed first and keeps its room. The input text is cut with an ellipsis 6 px before it, as decided in the issue. - The rule is a pure function in src/editor/graph/portRow.ts. A row without an output keeps the input's old limit of 108 px. - The value a run shows beside an output uses the same rule. Its left edge can no longer lie right of its right edge. - New test hook __gm.graph.rowSpans(nodeId) gives each row's input and output text as cut and placed, in node units. - NODE_WIDTH stays as it is, as decided in the issue. Tests: - Gate: 230 files, 3520 tests (6 new in port-row.test.ts). - e2e 6: all passed, 149 checks (148 + 1). - Counter-check: with the rule switched off, the input text reaches x = 121.4 under "progress" at x = 114.1, and the new check fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a40e629fde
commit
2aa635f453
7 files changed
+105
-24
No files matched your search
@@ -20,6 +20,8 @@ export function graphHooks({ state, editor }: HookContext) {
|
||||
nodeInputs: (nodeId: string) => graphEditor.canvas.nodeInputs(nodeId),
|
||||
/** Each output row as drawn: while playing, the value it last carried in front of its name. */
|
||||
nodeOutputs: (nodeId: string) => graphEditor.canvas.nodeOutputs(nodeId),
|
||||
/** Each row's input and output text as cut and placed, in node units: two that overlap collide (#306). */
|
||||
rowSpans: (nodeId: string) => graphEditor.canvas.nodeRowSpans(nodeId),
|
||||
/** Beads in flight on exec edges as drawn, in graph coordinates; `progress` 0 = source, 1 = target. */
|
||||
beads: () => graphEditor.canvas.beadsDrawn(),
|
||||
/** Drawn rectangles on screen: what "fit" has to keep inside `canvasRect`. */
|
||||
|
||||
@@ -1140,6 +1140,10 @@ export class GraphCanvas extends Widget {
|
||||
return this.views.get(nodeId)?.outputLines ?? null;
|
||||
}
|
||||
|
||||
nodeRowSpans(nodeId: string): NodeView['rowSpans'] | null {
|
||||
return this.views.get(nodeId)?.rowSpans ?? null;
|
||||
}
|
||||
|
||||
/** Global screen position of a node's title bar centre. */
|
||||
nodeGlobal(nodeId: string): { x: number; y: number } | null {
|
||||
const view = this.views.get(nodeId);
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
} from '../../ui';
|
||||
import { keyLabel, referenceLabel } from '../inspector/fieldFactory';
|
||||
import { collapsedNodeWidth } from './folding';
|
||||
import { inputTextRoom, valueSpan } from './portRow';
|
||||
|
||||
/** Node geometry in graph units; exported because edge anchors must land exactly on these ports. */
|
||||
export const NODE_WIDTH = 176;
|
||||
@@ -100,6 +101,12 @@ function rowStyle(port: PortDef): TextStyleToken {
|
||||
return port.type === 'exec' ? 'body' : 'muted';
|
||||
}
|
||||
|
||||
export interface TextSpan {
|
||||
text: string;
|
||||
left: number;
|
||||
right: number;
|
||||
}
|
||||
|
||||
export interface PortHit {
|
||||
port: PortDef;
|
||||
isOutput: boolean;
|
||||
@@ -110,6 +117,8 @@ export class NodeView extends Widget {
|
||||
private readonly box = new Graphics();
|
||||
private readonly ports = new Graphics();
|
||||
private readonly texts: Text[] = [];
|
||||
/** The input and output text of each port row, among `texts`. */
|
||||
private readonly rowTexts: { input: Text | null; output: Text | null }[] = [];
|
||||
private paramLines: string[] = [];
|
||||
/** Why this node can do nothing; see `callProblem`. */
|
||||
private problem: string | null = null;
|
||||
@@ -312,6 +321,12 @@ export class NodeView extends Widget {
|
||||
return this.def.inputs.map((input) => this.inputText(input));
|
||||
}
|
||||
|
||||
/** Where each port row's input and output text really landed, in node units, as cut to fit. */
|
||||
get rowSpans(): { input: TextSpan | null; output: TextSpan | null }[] {
|
||||
const span = (t: Text | null): TextSpan | null => t && { text: t.text, left: t.x, right: t.x + t.width };
|
||||
return this.rowTexts.map((row) => ({ input: span(row.input), output: span(row.output) }));
|
||||
}
|
||||
|
||||
get outputLines(): string[] {
|
||||
return this.def.outputs.map((output) => {
|
||||
const value = this.valueTexts.get(output.key)?.text;
|
||||
@@ -395,6 +410,7 @@ export class NodeView extends Widget {
|
||||
private rebuildTexts(): void {
|
||||
for (const t of this.texts) t.destroy();
|
||||
this.texts.length = 0;
|
||||
this.rowTexts.length = 0;
|
||||
this.valueSlots = new Map();
|
||||
const add = (
|
||||
text: string,
|
||||
@@ -431,33 +447,29 @@ export class NodeView extends Widget {
|
||||
add(this.problem, 'warning', PAD + 2, y + 2);
|
||||
y += ROW_HEIGHT;
|
||||
}
|
||||
const inputX = PAD + PORT_RADIUS + 4;
|
||||
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;
|
||||
let inputEnd = PAD;
|
||||
if (input) {
|
||||
const text = add(
|
||||
this.inputText(input),
|
||||
rowStyle(input),
|
||||
PAD + PORT_RADIUS + 4,
|
||||
rowTop,
|
||||
'left',
|
||||
NODE_WIDTH / 2 + 20,
|
||||
);
|
||||
inputEnd = text.x + text.width;
|
||||
}
|
||||
if (output) {
|
||||
const text = add(
|
||||
output.label,
|
||||
rowStyle(output),
|
||||
this.w - PAD - PORT_RADIUS - 4,
|
||||
rowTop,
|
||||
'right',
|
||||
NODE_WIDTH / 2 - 10,
|
||||
);
|
||||
if (output.type !== 'exec' && !output.ghost)
|
||||
this.valueSlots.set(output.key, { left: inputEnd + 6, right: text.x - 6, top: rowTop + 1 });
|
||||
const outputText = output
|
||||
? add(
|
||||
output.label,
|
||||
rowStyle(output),
|
||||
this.w - PAD - PORT_RADIUS - 4,
|
||||
rowTop,
|
||||
'right',
|
||||
NODE_WIDTH / 2 - 10,
|
||||
)
|
||||
: null;
|
||||
const room = inputTextRoom(inputX, NODE_WIDTH / 2 + 20, outputText ? outputText.x : null);
|
||||
const inputText = input
|
||||
? add(this.inputText(input), rowStyle(input), inputX, rowTop, 'left', room)
|
||||
: null;
|
||||
this.rowTexts.push({ input: inputText, output: outputText });
|
||||
if (output && outputText && output.type !== 'exec' && !output.ghost) {
|
||||
const inputEnd = inputText ? inputText.x + inputText.width : PAD;
|
||||
this.valueSlots.set(output.key, { ...valueSpan(inputEnd, outputText.x), top: rowTop + 1 });
|
||||
}
|
||||
}
|
||||
this.placeValues();
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/** Space kept between the texts that share one port row. */
|
||||
export const ROW_GAP = 6;
|
||||
|
||||
/**
|
||||
* How wide the input text of a port row may grow. The output label keeps its room and the input
|
||||
* gives way: a link is drawn from the output's name, and the input's value is in the inspector too.
|
||||
* `outputLeft` is null on a row without an output, which leaves the input its own limit.
|
||||
*/
|
||||
export function inputTextRoom(inputX: number, ownMax: number, outputLeft: number | null): number {
|
||||
if (outputLeft === null) return ownMax;
|
||||
return Math.max(0, Math.min(ownMax, outputLeft - ROW_GAP - inputX));
|
||||
}
|
||||
|
||||
/** Where a running game shows an output's value: between the input text and the output label. */
|
||||
export function valueSpan(inputEnd: number, outputLeft: number): { left: number; right: number } {
|
||||
const right = outputLeft - ROW_GAP;
|
||||
return { left: Math.min(inputEnd + ROW_GAP, right), right };
|
||||
}
|
||||
Reference in new issue
Block a user