A selected vector shape has its own inspector rows (#129)
game-maker / gate (push) Successful in 2m12s
game-maker / suite (push) Successful in 7m45s

Stage 4 of #129. Until now the inspector showed the rows of the asset
when a shape was selected, so there was no way to give a shape a colour,
an outline or a place in the drawing order without editing the project
file. The rows for a shape now sit where every other selection puts its
rows.

- SHAPES lists every shape of the asset, topmost row drawn last, the
  same way a layer list reads. Selecting the asset itself shows the same
  list below its own rows, with no shape selected.
- Each row paints its shape with the commands the rasterizer uses
  (shapeDrawing plus paintDrawing) into a 22 px swatch, the whole canvas
  scaled in, so the row also says where on the canvas the shape sits.
- The four order buttons from the issue (forward, to front, backward, to
  back) sit in two rows of two, and a row can be dragged to another
  place with a marker line. They are disabled when there is nothing to
  move.
- VECTOR SHAPE carries name, fill, opacity and an outline checkbox, the
  checkbox adding and removing the stroke instead of hiding it; line
  colour and width appear with it. A dragged colour slider is one undo
  step, the style rows going through previewingHandlers like every other
  row.
- A line width of 0 is no stroke, which the command rejects, so the row
  clamps at 0.1.

Findings:

- locate('Outline') aims beside the control, because it gives the centre
  of the label text and a wider label pushes a fixed offset out of the
  field. __gm.inspectorField(label) knows the centre of the control.
- A click into a text field sets the cursor and selects nothing, so a
  typed hex landed appended to the old one and parseHex returned null
  without anything failing - typeText only checks that the text is
  contained. A Ctrl+A goes first now.
- Four buttons side by side cut "Backward" to "Backw...", which only the
  screenshot showed. Two rows of two now, as in the tilemap panel.

Tests: ./check.sh 128 files, 1897 tests (was 127/1883). Full e2e suite
locally instead of only CI because the change sits in the inspector and
touches every selection: ALL PASSED - 1179 checks (was 1172), module 47
at 24 checks (was 17). Three counter-checks hold: not reversing the list
fails 1 unit test and 2 e2e checks, dropping the gap correction when
dropping a row fails 2 unit tests and the drag check, and a constant
signature kills module 47 because the outline rows never appear.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-20 22:29:40 +02:00
1 parent 3a68067079
commit 78595ec80f
9 files changed
+958 -8

No files matched your search

+108 -2
View File
@@ -69,8 +69,23 @@ const SHAPES = [
];
export async function vectorArt(t) {
const { check, click, doubleClick, drag, clickText, json, evaluate, mutate, settle, sleep, shot, showTab } =
t;
const {
check,
click,
doubleClick,
drag,
clickText,
json,
evaluate,
locate,
mutate,
press,
settle,
sleep,
shot,
showTab,
typeText,
} = t;
const Y = LANES.vectorArt;
// 47a. A drawing on a sprite: what the rasteriser put on the GPU, read off the screen.
@@ -235,4 +250,95 @@ export async function vectorArt(t) {
Boolean(undrawn) && undrawn.file !== after?.file && undrawn.kept,
JSON.stringify({ was: after?.file, now: undrawn?.file }),
);
// 47e. The inspector (stage 4): the shapes are listed, picked, coloured and reordered there.
const shapeIds = async () => (await json("__gm.vectorArt.shapes('va-art')")).map((s) => s.id);
const shapeOf = async (id) =>
(await json("__gm.vectorArt.shapes('va-art')")).find((shape) => shape.id === id);
const rows = await json('__gm.vectorArt.shapeRows()');
const model = await shapeIds();
check(
'the inspector lists the shapes, the one drawn last on top',
Array.isArray(rows) && rows.length === model.length && rows.join() === [...model].reverse().join(),
JSON.stringify({ rows, model }),
);
// A row is picked by where it sits: its name also stands in the tab, so text would be ambiguous.
const fieldRow = await json("__gm.vectorArt.shapeRowAt('va-field')");
await click(fieldRow.x + fieldRow.w / 2, fieldRow.y + fieldRow.h / 2);
await settle();
check(
'clicking a row selects that shape',
(await json('__gm.vectorArt.selected()')) === 'va-field',
JSON.stringify({ selected: await json('__gm.vectorArt.selected()'), row: fieldRow }),
);
// The control of a row, not an offset from its label: a wider label pushes its control right.
const fillField = await json("__gm.inspectorField('Fill')");
await click(fillField.x, fillField.y);
// The click puts a caret in the hex text rather than selecting it, so the old value goes first.
await press('a', { ctrl: true });
await typeText('20c0e8');
await press('Enter');
await settle();
const recoloured = await shapeOf('va-field');
// Read off the tab as well: a row that only writes the model would show nothing to the eye.
const spot = await json('__gm.vectorArt.pointAt(48, 200)');
const painted = await json(`__gm.pixelAt(${Math.round(spot.x)}, ${Math.round(spot.y)})`);
check(
'the Fill row recolours the shape, and the tab shows it',
recoloured?.fill === 0x20c0e8 && colourDistance(painted, '#20c0e8') < 14,
JSON.stringify({ fill: recoloured?.fill?.toString(16), painted }),
);
const outlineField = await json("__gm.inspectorField('Outline')");
await click(outlineField.x, outlineField.y);
await settle();
const outlined = await shapeOf('va-field');
check(
'the Outline switch gives the shape a line, and its rows appear with it',
Boolean(outlined?.stroke) && outlined.stroke.width === 2 && Boolean(await locate('Line width')),
JSON.stringify(outlined?.stroke),
);
// The badge was drawn over the field; "To front" puts the field over the badge instead.
const covered = await json('__gm.vectorArt.pointAt(204, 60)');
const wasBadge = await json(`__gm.pixelAt(${Math.round(covered.x)}, ${Math.round(covered.y)})`);
await clickText('To front');
await settle();
const nowField = await json(`__gm.pixelAt(${Math.round(covered.x)}, ${Math.round(covered.y)})`);
check(
'To front draws the shape over the one that covered it',
(await shapeIds()).at(-1) === 'va-field' &&
colourDistance(wasBadge, BADGE) < 14 &&
colourDistance(nowField, '#20c0e8') < 14,
JSON.stringify({ order: await shapeIds(), wasBadge, nowField }),
);
await shot('47-shape-rows');
// Dragging the top row to the bottom of the list is the same move the other way round.
const top = await json("__gm.vectorArt.shapeRowAt('va-field')");
await drag(
top.x + top.w / 2,
top.y + top.h / 2,
top.x + top.w / 2,
top.y + top.h * (await shapeIds()).length,
);
await settle();
check(
'dragging a row to the bottom draws that shape first',
(await shapeIds())[0] === 'va-field',
JSON.stringify(await shapeIds()),
);
const before47e = (await shapeIds()).length;
await clickText('Add shape');
await settle();
const added = await shapeIds();
check(
'Add shape puts an empty shape on top and selects it',
added.length === before47e + 1 && (await json('__gm.vectorArt.selected()')) === added.at(-1),
JSON.stringify({ added: added.length, selected: await json('__gm.vectorArt.selected()') }),
);
}