A held preview is rebased, not paused, and it wears the asset's own rows (#86)
game-maker / suite (push) Successful in 9m42s
game-maker / suite (push) Successful in 9m42s
Three reports plus one found on the way.
Hold only froze the *picture*: the preview computed a fake second while drawing,
the library clock kept counting underneath, so letting go jumped to "how much
would have passed by now". That is the flip side of the formula model — a thing
that is a function of the clock is not stopped, its origin is moved.
ParticleField.rebase() does that; hold(null) calls it with the second stood at.
The Hold label now mirrors the preview's state instead of its own click history,
so dragging the slider (which holds too) reads `Run`, and Restart puts a held
preview back to 0.00 s rather than leaving it on the second it stopped at.
previewProps only read defaultsFor('particles'), so the panel showed the type's
defaults while the inspector showed the asset's rows. AssetBase.props is a full
bag on a graph asset as well — asset.props now lie over the defaults, the
asset's texture is resolved instead of always drawing a white square, and the
slider's span follows its lifetime. The span is re-read in tick(), because the
rows are edited in the inspector and the tab hears nothing about it.
The preview moves from under the graph to beside it, past a draggable splitter:
a graph is read across, so width is what it can spare and height is not.
AssetTabEditor grows a body() hook for that — the base class still stacks header,
strip and canvas, a subclass may put something next to the canvas.
Found by the full run, not reported: the graph canvas's empty-state hint ran
38 px past its frame now that the preview takes 280 px away. It is cut with
truncate() like any other label.
816 checks (809 + 7).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4feb4c2cf6
commit
fbb2d157f0
8 files changed
+284
-66
No files matched your search
+124
-7
@@ -16,7 +16,7 @@ const RATE = 20;
|
||||
|
||||
export async function particles(t) {
|
||||
const { check, sleep, json, evaluate, mutate, click, clickText, locate, typeText, press, shot } = t;
|
||||
const { wheel } = t;
|
||||
const { wheel, drag } = t;
|
||||
const { select } = t;
|
||||
/** Freeze, restart every source's clock, then step the clock to `seconds` and let it redraw. */
|
||||
const at = async (seconds) => {
|
||||
@@ -54,21 +54,24 @@ export async function particles(t) {
|
||||
return { box, p, scrolled };
|
||||
};
|
||||
/** Places a node from the palette. The list scrolls, so the name is searched into view first. */
|
||||
const placeNode = async (label, dx, dy) => {
|
||||
const canvas = await json('__gm.particles.canvasRect()');
|
||||
await click(canvas.x + dx, canvas.y + dy, 'right');
|
||||
const placeNodeAt = async (label, point) => {
|
||||
await click(point.x, point.y, 'right');
|
||||
await sleep(300);
|
||||
await typeText(label);
|
||||
await sleep(200);
|
||||
await clickLast(label);
|
||||
await sleep(300);
|
||||
};
|
||||
const placeNode = async (label, dx, dy) => {
|
||||
const canvas = await json('__gm.particles.canvasRect()');
|
||||
await placeNodeAt(label, { x: canvas.x + dx, y: canvas.y + dy });
|
||||
};
|
||||
const nodeIdOf = (type) =>
|
||||
json(`__gm.state.project.assets.find((a) => a.id === ${JSON.stringify(assetId)})
|
||||
.graph.nodes.find((n) => n.type === ${JSON.stringify(type)})?.id ?? ''`);
|
||||
/**
|
||||
* Adds a node straight to the graph. The palette is proved by the two nodes placed through it
|
||||
* above; the tab's canvas is short (the preview strip takes the lower part), so dropping five
|
||||
* above; the tab's canvas is narrow (the preview panel takes the right of it), so dropping five
|
||||
* more through it would be a test of where they happen to land rather than of what they do.
|
||||
*/
|
||||
const addNode = async (id, type, params = {}) => {
|
||||
@@ -133,7 +136,7 @@ export async function particles(t) {
|
||||
await sleep(200);
|
||||
|
||||
// 31c. One Output node is a whole graph: a particle without rules is one at the source, full size.
|
||||
await placeNode('Output', 460, 60);
|
||||
await placeNode('Output', 80, 60);
|
||||
const withOutput = await json('{ info: __gm.particles.info(), preview: __gm.particles.preview() }');
|
||||
check(
|
||||
'an Output node alone compiles, and the strip below the graph emits',
|
||||
@@ -154,6 +157,118 @@ export async function particles(t) {
|
||||
await shot('31-particles-tab');
|
||||
await evaluate('__gm.particles.hold(null)');
|
||||
|
||||
// 31d-ii. (#86) The controls read the panel rather than their own history: the slider holds it, so
|
||||
// the button has to offer the way back, and Restart on a held panel stands at second 0.
|
||||
const track = await json('__gm.particles.sliderAt()');
|
||||
await click(track.x, track.y);
|
||||
await sleep(200);
|
||||
const dragged = await json('{ c: __gm.particles.controls(), p: __gm.particles.preview() }');
|
||||
check(
|
||||
'dragging the time slider holds the panel, and the button then offers to run it again',
|
||||
dragged.p.holding === true &&
|
||||
dragged.p.time > 0 &&
|
||||
dragged.c.hold === 'Run' &&
|
||||
dragged.c.time === `${dragged.p.time.toFixed(2)} s` &&
|
||||
Math.abs(dragged.c.slider - dragged.p.time) < 0.01,
|
||||
JSON.stringify(dragged),
|
||||
);
|
||||
await clickText('Restart');
|
||||
await sleep(200);
|
||||
const restarted = await json('{ c: __gm.particles.controls(), p: __gm.particles.preview() }');
|
||||
check(
|
||||
'Restart puts a held panel back to second 0, and the readout and slider with it',
|
||||
restarted.p.holding === true &&
|
||||
restarted.p.time === 0 &&
|
||||
restarted.c.time === '0.00 s' &&
|
||||
restarted.c.slider === 0 &&
|
||||
restarted.c.hold === 'Run',
|
||||
JSON.stringify(restarted),
|
||||
);
|
||||
await clickText('Run');
|
||||
await sleep(250);
|
||||
const running = await json('{ c: __gm.particles.controls(), p: __gm.particles.preview() }');
|
||||
check(
|
||||
'the same button lets it run again and says "Hold" once it does',
|
||||
running.p.holding === false && running.c.hold === 'Hold',
|
||||
JSON.stringify(running),
|
||||
);
|
||||
|
||||
// 31d-iii. (#86) The panel stands beside the graph, not under it, and the divider between them is
|
||||
// the one that moves – a graph is read across, so the width is what it cannot spare.
|
||||
const beside = await json(`{
|
||||
canvas: __gm.particles.canvasRect(),
|
||||
preview: __gm.particles.previewRect(),
|
||||
split: __gm.particles.splitterAt(),
|
||||
}`);
|
||||
check(
|
||||
'the preview stands right of the graph, with the divider between the two',
|
||||
beside.preview.x >= beside.canvas.x + beside.canvas.w - 1 &&
|
||||
beside.split.x > beside.canvas.x + beside.canvas.w - 1 &&
|
||||
beside.split.x < beside.preview.x + 1 &&
|
||||
beside.preview.y + beside.preview.h > beside.canvas.y + beside.canvas.h / 2,
|
||||
JSON.stringify(beside),
|
||||
);
|
||||
await drag(beside.split.x, beside.split.y, beside.split.x - 80, beside.split.y, { steps: 8 });
|
||||
await sleep(200);
|
||||
const widened = await json(`{
|
||||
canvas: __gm.particles.canvasRect(),
|
||||
preview: __gm.particles.previewRect(),
|
||||
}`);
|
||||
check(
|
||||
'dragging that divider left widens the preview and narrows the graph by the same 80 px',
|
||||
widened.preview.w === beside.preview.w + 80 && widened.canvas.w === beside.canvas.w - 80,
|
||||
JSON.stringify({ before: beside.preview.w, after: widened.preview.w, canvas: widened.canvas.w }),
|
||||
);
|
||||
await drag(beside.split.x - 80, beside.split.y, beside.split.x, beside.split.y, { steps: 8 });
|
||||
await sleep(200);
|
||||
|
||||
// 31d-iv. (#86) A hold has to stop the clock, not just the picture: the library's second goes on
|
||||
// under the freeze, so letting go must move the panel's origin rather than resume at the
|
||||
// second it would have reached. Measured with the clock nailed down, so 0 means 0.
|
||||
await evaluate('__gm.particleTime(2000)');
|
||||
await sleep(150);
|
||||
await evaluate('__gm.particles.hold(0.5)');
|
||||
await sleep(150);
|
||||
await clickText('Restart');
|
||||
await sleep(200);
|
||||
await evaluate('__gm.particleTime(2003)');
|
||||
await sleep(200);
|
||||
const waited = await json('{ c: __gm.particles.controls(), p: __gm.particles.preview() }');
|
||||
await clickText('Run');
|
||||
await sleep(200);
|
||||
const letGo = await json('{ c: __gm.particles.controls(), p: __gm.particles.preview() }');
|
||||
check(
|
||||
'three seconds spent on hold are not owed: letting it run again carries on from second 0',
|
||||
waited.p.time === 0 && letGo.p.holding === false && letGo.p.time < 0.1 && letGo.c.hold === 'Hold',
|
||||
JSON.stringify({ waited: waited.p, letGo: letGo.p, label: letGo.c.hold }),
|
||||
);
|
||||
|
||||
// 31d-v. (#86) The panel shows the asset's own rows, not the type's defaults: a lifetime of 2 s at
|
||||
// 5 a second is a different field from 1 s at 20, and the slider reaches three lifetimes.
|
||||
await mutate(`
|
||||
const a = __gm.state.project.assets.find((x) => x.id === ${JSON.stringify(assetId)});
|
||||
a.props.lifetime = 2;
|
||||
a.props.rate = 5;
|
||||
`);
|
||||
await sleep(200);
|
||||
await evaluate('__gm.particles.hold(1.5)');
|
||||
await sleep(200);
|
||||
const edited = await json('{ c: __gm.particles.controls(), p: __gm.particles.preview() }');
|
||||
check(
|
||||
"the preview follows the asset's own rate and lifetime, and the slider spans three of them",
|
||||
edited.p.count === 8 && edited.c.span === 6,
|
||||
JSON.stringify({ ...edited.p, span: edited.c.span, defaultsWouldGive: { count: 21, span: 3 } }),
|
||||
);
|
||||
await mutate(`
|
||||
const a = __gm.state.project.assets.find((x) => x.id === ${JSON.stringify(assetId)});
|
||||
delete a.props.lifetime;
|
||||
delete a.props.rate;
|
||||
`);
|
||||
await sleep(200);
|
||||
await evaluate('__gm.particles.hold(null)');
|
||||
await evaluate('__gm.particleTime(null)');
|
||||
await sleep(150);
|
||||
|
||||
// 31e. Placed in the scene, the same asset is a source with its own rows.
|
||||
await mutate(`
|
||||
__gm.state.activeScene.objects.push({ id: ${JSON.stringify(SOURCE)}, assetId: ${JSON.stringify(assetId)},
|
||||
@@ -176,7 +291,9 @@ export async function particles(t) {
|
||||
// old, is exactly 100 px below the source at strength 800.
|
||||
await evaluate("__gm.showTab('particles')");
|
||||
await sleep(250);
|
||||
await placeNode('Gravity', 80, 60);
|
||||
// Not a fixed point: the Output node is already on the canvas, and since the preview took the
|
||||
// right of the tab (#86) there is no corner left that is free by construction.
|
||||
await placeNodeAt('Gravity', await json('__gm.particles.emptySpot()'));
|
||||
const gravityId = await nodeIdOf('ParticleGravity');
|
||||
const outputId = await nodeIdOf('ParticleOutput');
|
||||
await mutate(`
|
||||
|
||||
Reference in new issue
Block a user