A box drawn around a part of the graph is part of it, so a fit frames it and a band catches it (#95)
game-maker / suite (push) Successful in 6m22s
game-maker / suite (push) Successful in 6m22s
Fit measured the nodes alone: a group drawn around them was cut off, and a graph holding nothing but a comment framed the empty origin. It now frames both sorts together. The rubber band ignored boxes entirely. It catches them now, into the *same* selection as the nodes — `nodeIds` and `annotationIds` side by side — so one drag moves all of it, one Delete removes all of it, and one undo takes it back. A box counts only when the band holds all of it, where a node counts on touching: a group wraps the nodes it holds and therefore overlaps every band drawn over any of them, and since it drags those nodes along, catching it by accident would be felt at once. A node inside a selected group is moved once, not twice — `travellers()` collects everything one gesture carries and lets each thing appear in it a single time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
be8f2331de
commit
36ccc7626f
9 files changed
+554
-46
No files matched your search
@@ -228,4 +228,105 @@ export async function graphGroups(t) {
|
||||
(saved.graph.groups ?? []).length === 0,
|
||||
JSON.stringify({ notes: saved.graph.notes, groups: saved.graph.groups }),
|
||||
);
|
||||
// 36j. Fit frames the boxes too (#95). A comment far from every node is the case that tells the
|
||||
// two answers apart: framing the nodes alone leaves it off screen, and nothing in the
|
||||
// picture says why. The graph itself never moves – only the view does.
|
||||
const away = await one('note');
|
||||
// Far enough that framing the nodes alone leaves it off screen, near enough that the fit can
|
||||
// still show everything: the view stops zooming out at 0.25, and past that no fit can succeed.
|
||||
await mutate(`
|
||||
const g = __gm.state.project.assets.find((a) => a.id === 'gg-logic').graph;
|
||||
const note = g.notes.find((n) => n.id === ${JSON.stringify(away.id)});
|
||||
note.x = -700; note.y = -500;`);
|
||||
// The button, not a hook: "click Fit" is the gesture the issue is about.
|
||||
const fitButton = await locate('Fit');
|
||||
await click(fitButton.x, fitButton.y);
|
||||
const canvas = await json('__gm.graph.canvasRect()');
|
||||
const noteRect = await json(`__gm.graph.annotationRect(${JSON.stringify(away.id)})`);
|
||||
const startRect = await json('__gm.graph.nodeRect("gg-start")');
|
||||
const inside = (r) =>
|
||||
r !== null &&
|
||||
r.x >= canvas.x - 1 &&
|
||||
r.y >= canvas.y - 1 &&
|
||||
r.x + r.w <= canvas.x + canvas.w + 1 &&
|
||||
r.y + r.h <= canvas.y + canvas.h + 1;
|
||||
check(
|
||||
'fit brings a far-off comment into view without losing the nodes',
|
||||
inside(noteRect) && inside(startRect),
|
||||
JSON.stringify({ canvas, note: noteRect, node: startRect }),
|
||||
);
|
||||
await shot('36-fit-with-comment');
|
||||
|
||||
// 36k. The rubber band catches boxes as well (#95). Everything is on screen after the fit, so a
|
||||
// band across the whole canvas must come back holding both sorts at once – and it stays a
|
||||
// node selection, which is what keeps the inspector showing what it showed before.
|
||||
await evaluate('__gm.state.select(null)');
|
||||
const band = await json(`__gm.graph.annotationRect(${JSON.stringify(away.id)})`);
|
||||
await drag(canvas.x + 3, canvas.y + 3, canvas.x + canvas.w - 3, canvas.y + canvas.h - 3);
|
||||
const caught = await json('__gm.state.selection');
|
||||
check(
|
||||
'a band over the whole graph catches the comment along with the nodes',
|
||||
caught?.kind === 'node' &&
|
||||
(caught.annotationIds ?? []).includes(away.id) &&
|
||||
caught.nodeIds.includes('gg-start'),
|
||||
JSON.stringify(caught),
|
||||
);
|
||||
|
||||
// 36l. But a band that merely crosses a box leaves it alone – the rule that keeps a group, which
|
||||
// wraps the nodes it holds, from joining every band drawn over any of them. It starts to the
|
||||
// left of the comment on empty canvas and stops halfway across it.
|
||||
await evaluate('__gm.state.select(null)');
|
||||
await drag(band.x - 8, band.y + band.h / 2, band.x + band.w / 2, band.y + band.h / 2 + 4);
|
||||
const grazed = await json('__gm.state.selectedAnnotationIds ?? []');
|
||||
const stillThere = await one('note');
|
||||
check(
|
||||
'a band that only crosses a box does not catch it, and does not move it either',
|
||||
!grazed.includes(away.id) && stillThere.x === -700 && stillThere.y === -500,
|
||||
JSON.stringify({ grazed, note: { x: stillThere.x, y: stillThere.y } }),
|
||||
);
|
||||
|
||||
// 36m. What one band caught, one drag moves – nodes and boxes by the same offset. The group here
|
||||
// is the trap: it carries the nodes inside it, and those are selected too, so a naive
|
||||
// implementation offsets them twice. Equal deltas are what says it did not.
|
||||
await mutate(`
|
||||
const g = __gm.state.project.assets.find((a) => a.id === 'gg-logic').graph;
|
||||
g.groups = [{ id: 'gg-box', x: -20, y: -44, w: 216, h: 320, text: 'Held' }];`);
|
||||
const fitAgain = await locate('Fit');
|
||||
await click(fitAgain.x, fitAgain.y);
|
||||
const sheet = await json('__gm.graph.canvasRect()');
|
||||
await evaluate('__gm.state.select(null)');
|
||||
await drag(sheet.x + 3, sheet.y + 3, sheet.x + sheet.w - 3, sheet.y + sheet.h - 3);
|
||||
const before = {
|
||||
start: await nodeAt('gg-start'),
|
||||
far: await nodeAt('gg-far'),
|
||||
group: (await boxes()).find((a) => a.id === 'gg-box'),
|
||||
note: await one('note'),
|
||||
};
|
||||
const grip = await json('__gm.graph.nodeAt("gg-far")');
|
||||
await drag(grip.x, grip.y, grip.x + 70, grip.y + 50);
|
||||
const after = {
|
||||
start: await nodeAt('gg-start'),
|
||||
far: await nodeAt('gg-far'),
|
||||
group: (await boxes()).find((a) => a.id === 'gg-box'),
|
||||
note: await one('note'),
|
||||
};
|
||||
const delta = (key) => ({ x: after[key].x - before[key].x, y: after[key].y - before[key].y });
|
||||
const moved = delta('far');
|
||||
check(
|
||||
'dragging one node moves every caught box and node by the same step, none of them twice',
|
||||
moved.x > 0 &&
|
||||
moved.y > 0 &&
|
||||
['start', 'group', 'note'].every((key) => delta(key).x === moved.x && delta(key).y === moved.y),
|
||||
JSON.stringify({ far: moved, start: delta('start'), group: delta('group'), note: delta('note') }),
|
||||
);
|
||||
|
||||
// And one undo takes the whole mixed gesture back, boxes included.
|
||||
await t.undo();
|
||||
check(
|
||||
'one undo returns the nodes and the boxes together',
|
||||
JSON.stringify(await nodeAt('gg-start')) === JSON.stringify(before.start) &&
|
||||
(await boxes()).find((a) => a.id === 'gg-box').x === before.group.x &&
|
||||
(await one('note')).x === before.note.x,
|
||||
JSON.stringify({ start: await nodeAt('gg-start'), note: (await one('note')).x }),
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user