Snap to the grid from the start in every graph tab (#391)
game-maker / gate (push) Successful in 4m12s
game-maker / suite (push) Successful in 15m55s

Laying out a graph by hand mostly means lining nodes up, so the
magnet was switched on in nearly every session. Every graph tab now
starts with it lit: Behavior, State machine, Shader, Particles,
Sound FX, Instrument, the song's Routing and the Routing tab.

- SnapToggle takes the state it starts in and lights its button to
  match. GRAPH_SNAPS_AT_FIRST sets it for the toggles and for
  GraphCanvas alike, so button and canvas cannot disagree.
- Curve and Vector Art are no graphs and still start without it.
- New hook __gm.graphSnapping(on?) reads every graph tab's magnet,
  or "mixed" where button and canvas differ, and switches them all.
- The e2e harness reads the magnets after each navigate() and then
  switches them off, as it already sets the zoom transition to 0.
  Most checks drag a node to an exact spot. e2e 45 does the same
  itself, because the editor comes up there through the sign-in.
- e2e 06 checks that all eight graph tabs started with the magnet on.
  The checks in 06, 26 and 65 that read it off now say they read
  what the harness left.

Tests:
- Unit snap-toggle: 4 tests, 1 new (lit at first, nothing asked of
  the target before a click).
- e2e 06 (melektaus-atx): 150 checks, 1 new. e2e 34: 134 checks.
- Gate (melektaus-atx, together with #390): 287 files, 4397 tests.
- Pictures looked at: none, the state comes from the hook.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-10 17:37:13 +02:00
1 parent e70954115c
commit fe4e0d9f1a
18 files changed
+104 -25

No files matched your search

+11 -1
View File
@@ -27,6 +27,7 @@ export async function graph(t) {
asset,
api,
navigate,
graphSnapAtFirst,
projectId,
showTab,
} = t;
@@ -447,7 +448,16 @@ export async function graph(t) {
// current zoom – the canvas is not wide enough here for a zoom of 1.
const GRID = 24;
const onGrid = (n) => n.x % GRID === 0 && n.y % GRID === 0;
check('snapping is off until it is asked for', (await json('__gm.graph.snapping()')) === false);
// Every graph tab comes up snapping, magnet lit (#391); the harness has switched them all off since.
const atFirst = graphSnapAtFirst();
check(
'every graph tab starts with snapping on and its magnet lit',
atFirst !== null &&
Object.keys(atFirst).length === 8 &&
Object.values(atFirst).every((on) => on === true),
JSON.stringify(atFirst),
);
check('snapping is off as the harness left it', (await json('__gm.graph.snapping()')) === false);
// The switch is a magnet (#194): a square icon button that stays lit while snapping is on.
const magnet = await json('__gm.graph.snapButton()');
await click(magnet.x, magnet.y);
+4 -1
View File
@@ -110,7 +110,10 @@ export async function stateMachine(t) {
// 26b2. The header's magnet is the graph's: on, a dragged state lands on the 24 px grid.
const GRID = 24;
const stateXY = async (id) => (await states()).find((s) => s.id === id) ?? { x: NaN, y: NaN };
check('snapping is off until it is asked for', (await json('__gm.stateMachine.snapping()')) === false);
check(
'snapping is off as the harness left it (#391)',
(await json('__gm.stateMachine.snapping()')) === false,
);
const magnet = await json('__gm.stateMachine.snapButton()');
const preSnap = JSON.stringify(await states());
await click(magnet.x, magnet.y);
+3
View File
@@ -285,4 +285,7 @@ export async function login(t) {
restored.dialog === null && restored.notice.startsWith('Password changed'),
restored,
);
// The editor came up through the sign-in screen, not `navigate()`: its graphs snap (#391), and
// the steps after this one drag nodes to exact spots.
await evaluate('__gm.graphSnapping(false)');
}
+2 -1
View File
@@ -954,7 +954,8 @@ export async function song(t) {
p = await json("__gm.song.viewAt('routing')");
await click(p.x, p.y);
await sleep(300);
// The magnet is the routing's, as in every graph tab: off at first, greyed out on the timeline.
// The magnet is the routing's, as in every graph tab: off as the harness left it (#391), greyed
// out on the timeline.
const magnet = await json('__gm.song.routing.snapButton()');
const snapOff = await json('__gm.song.routing.snapping()');
await click(magnet.x, magnet.y);
+8
View File
@@ -491,7 +491,14 @@ function makeTools({ cdp, projectId, baseUrl, consoleLog, check, shotPrefix }) {
check('debug hook present', Boolean(await evaluate('Boolean(window.__gm?.state)')));
// The zoom is shown as a transition (#309); a check reads the step it lands on, not the way.
await evaluate('__gm.zoomTransition?.(0)');
// Every graph snaps from the start (#391); the checks drag nodes to exact spots, so the magnets
// go off here. What they started at stays readable for the check that wants it.
snapAtFirst = (await json('__gm.graphSnapping?.() ?? null')) ?? null;
await evaluate('__gm.graphSnapping?.(false)');
};
/** Each graph tab's magnet as the last load left it, before the harness switched it off (#391). */
let snapAtFirst = null;
const graphSnapAtFirst = () => snapAtFirst;
return {
cdp,
@@ -545,6 +552,7 @@ function makeTools({ cdp, projectId, baseUrl, consoleLog, check, shotPrefix }) {
signIn,
sceneOf,
navigate,
graphSnapAtFirst,
};
}