Symbol names in the scene view were plain grey text. On a background of a similar colour they could not be read, and names of symbols close together were drawn over each other. - Each name stands on a rounded, nearly black plate at 70 % opacity, 3 px padding left and right, 1 px top and bottom. - The text is light (viewportLabel) instead of muted grey. - The plate of a selected object gets an accent border. - New rule stackLabels in src/viewport/labelRows.ts. A name that overlaps another name or an icon moves one row down, then one up, then two down and so on, until a row is free. - Names are placed top to bottom, so the upper one keeps its row. A name only moves vertically and stays on the side of its icon. - Names outside the view still claim their row. Otherwise the visible names would jump while panning. - New hook __gm.sceneLabels() for the e2e check. The frame names in the texture editor already had a plate, so they are unchanged. Not covered by tests: the accent border on selection. Readability on a light background was only judged from a screenshot on a dark one. Tests: - 5 unit tests in tests/viewport/label-rows.test.ts: free names stay, the lower of two colliding names moves, the stacking order, names avoid icons, 30 crowded names never overlap. - e2e 5c-ii: 8 symbols on one spot give 8 plates, no overlap with each other or with icons, each name on the side of its icon. - Counter-checks: without the overlap test in the rule 4 of 5 unit tests fail. With the scene view ignoring the rows exactly 5c-ii fails. - ./check.sh: 161 files, 2369 tests, CHECK EXIT 0. - e2e 1-5: ALL PASSED, 140 checks. The e2e sum is now 1382 (computed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
51 lines
1.8 KiB
TypeScript
51 lines
1.8 KiB
TypeScript
/**
|
||
* Where the scene view's symbol names stand. Each wants the row beside its icon; a name that would
|
||
* cover one already placed, or another icon, moves a row down, up, two down and so on, until it
|
||
* finds a free row. Names are placed top to bottom, so of two that collide the upper one keeps its
|
||
* row, and a name only ever moves up or down: it stays on its icon's side.
|
||
*/
|
||
export interface LabelBox {
|
||
x: number;
|
||
y: number;
|
||
w: number;
|
||
h: number;
|
||
}
|
||
|
||
export interface WantedLabel extends LabelBox {
|
||
id: string;
|
||
}
|
||
|
||
/** Tries per name before it gives up and keeps its own row – far more rows than a scene fills. */
|
||
const MAX_TRIES = 4096;
|
||
|
||
function overlaps(a: LabelBox, b: LabelBox): boolean {
|
||
// A hair of slack: boxes that just touch are fine, and rounding must not push one away.
|
||
const e = 1e-6;
|
||
return a.x < b.x + b.w - e && b.x < a.x + a.w - e && a.y < b.y + b.h - e && b.y < a.y + a.h - e;
|
||
}
|
||
|
||
/** The top of every label, `step` apart from its wanted row: 0, +1, −1, +2, −2 … rows. */
|
||
export function stackLabels(
|
||
labels: readonly WantedLabel[],
|
||
obstacles: readonly LabelBox[],
|
||
step: number,
|
||
): Map<string, number> {
|
||
const order = labels.map((label, k) => ({ label, k }));
|
||
order.sort((a, b) => a.label.y - b.label.y || a.label.x - b.label.x || a.k - b.k);
|
||
const placed: LabelBox[] = [...obstacles];
|
||
const tops = new Map<string, number>();
|
||
for (const { label } of order) {
|
||
let top = label.y;
|
||
for (let k = 0; k < MAX_TRIES; k++) {
|
||
const rows = k % 2 === 0 ? -k / 2 : (k + 1) / 2;
|
||
const box = { ...label, y: label.y + rows * step };
|
||
if (placed.some((other) => overlaps(box, other))) continue;
|
||
top = box.y;
|
||
break;
|
||
}
|
||
tops.set(label.id, top);
|
||
placed.push({ x: label.x, y: top, w: label.w, h: label.h });
|
||
}
|
||
return tops;
|
||
}
|