Particles: the preview stands beside the graph and wears the asset's own rows (#86)

A hold moves the field's origin instead of pausing a counter, which is the other
side of "a particle is a function of its age" and now stated in Architecture.
Development gains the rule that a step opens a tab with openTab(), not by
clicking its name, and both counters go to 816.
root committed 2026-09-14 07:04:10 +02:00
1 parent 1f2527386e
commit 02c74bba02
3 files changed
+29 -14

No files matched your search

+6 -3
@@ -79,8 +79,8 @@ src/editor/texture/ TextureEditor (header + picker), TextureCanvas (picture +
ImagePicker + imageGrid (the "+ → Texture" dialog), checker (the transparency board)
src/editor/animation/ AnimationEditor (header + picker), AnimationCanvas (preview + frame strip), strip (its geometry)
src/editor/statemachine/ StateMachineEditor (header + picker, on the graph canvas)
src/editor/particles/ ParticlesEditor (graph canvas above a preview strip, splitter between them),
ParticlePreview (the strip: the asset's own particles at the type's defaults)
src/editor/particles/ ParticlesEditor (graph canvas left of a preview panel, splitter between them),
ParticlePreview (the panel: the asset's own particles, wearing its own rows)
src/editor/list/ ListEditor (a row per item), slot (which list is being edited, and how it is written back)
src/editor/console/ log (ring buffer, argument formatting, the console.* capture), ConsolePanel (the tail under the tabs)
api/ PHP backend: index.php (routing), lib/{Store,TextureStore,FontStore,OriginalName,Response}.php
@@ -102,7 +102,10 @@ outline, a name plate or a checkerboard left outside it is drawn over the panel
per frame** — `core/particles/emission.ts` says which particles are alive at second *t* and
`core/particles/compile.ts` turns a graph into what one of them looks like at its age, so a field
of 500 particles is reproducible, holds still for a check, and needs no state beyond where its
source stood when each one was born. Both are pure and tested without a browser.
source stood when each one was born. Both are pure and tested without a browser. The other side of
that choice: **freezing such a thing means moving its origin, not pausing a counter** — the clock
belongs to the library and keeps running, so a preview let go after a minute on hold has to be
rebased to the second it was stopped at, or it resumes a minute further on.
Comments state contracts
and reasons, not what the code does: a `/** */` on every export whose name does not say it all,
`//` only for a decision or gotcha.
+7 -2
@@ -12,7 +12,7 @@ npm run check # typecheck + lint + format:check + tests, the gate CI runs
# code is lost (tail's is reported), so a red run reads as green
npm run build # writes dist/ and regenerates index.html
./build.sh # the same under `set -o pipefail`, for callers that pipe the output
node tools/seed-e2e.mjs && node tools/e2e.mjs # 809 editor checks, screenshots in /tmp/gm-e2e-*.png
node tools/seed-e2e.mjs && node tools/e2e.mjs # 816 editor checks, screenshots in /tmp/gm-e2e-*.png
GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 — a subset while working on one step
./e2e.sh start [GM_STEPS] # the same run detached, see below
./e2e.sh status # live progress, or the last run's result
@@ -68,6 +68,11 @@ scene state and share the CDP driver in `tools/lib/harness.mjs` — `press`, `un
`object(id)`, `world()`, `mutate(js)` and friends, so a check reads like the interaction it
exercises.
A step that needs a tab opens it with `openTab(t, 'container')` from `tools/e2e/lib/scaffold.mjs`,
not by clicking its name: since the tenth tab the row hides some of them at the test window's width,
and the helper goes through the `»` menu when it has to. Clicking the label directly took the
whole suite down the day the Particles tab was added.
`GM_STEPS=20`, `GM_STEPS=1-12` or `GM_STEPS=6,20` runs a subset — seconds instead of minutes
while one step is being written. Because a step lives off what the earlier ones left behind, a
subset that skips a prerequisite fails on purpose; take `1-N` when in doubt.
@@ -151,4 +156,4 @@ widths to find click points. The **render device is not in this repo** — `/dev
and a 1 GB `/dev/shm` come from `container.options` in the runner's own `config.yaml`, which
applies to every job on that runner. Without them chromium falls back to llvmpipe, which gets
WebGL 1.0 only. Measured in the job container: `ANGLE (Intel, Vulkan 1.3.230, HD Graphics 630)`,
WebGL 2.0, and the same 809 checks as here.
WebGL 2.0, and the same 816 checks as here.
+16 -9
@@ -9,7 +9,7 @@ the model was chosen:
- **The same second always looks the same.** Two runs at the same frame rate, or a different one,
draw the same picture — the field is a function of the clock, not of how many frames have gone by.
- **Any second can be shown at once.** The preview strip in the tab has a slider: drag it to 2 s and
- **Any second can be shown at once.** The preview panel in the tab has a slider: drag it to 2 s and
the particles stand where they are at 2 s, exactly, with nothing simulated on the way there.
- **Collision is out, for good.** "Sparks bounce off the ground", wind, drag mid-flight — none of
these can be written as a function of age, and no later addition will change that.
@@ -17,7 +17,7 @@ the model was chosen:
## The Particles tab
`+` in the **Assets** panel makes a particle asset and opens the **Particles** tab on it (a
double-click on an existing one does the same; a click only selects). The upper part is the same
double-click on an existing one does the same; a click only selects). The left part is the same
canvas the Behavior and Shader tabs have: right-click places a node, drag moves, drag from a port
links, `Snap` snaps to the 24 px grid, `Fit` frames the graph, undo takes every step back. The
readout on the right says `N parameters` while the graph compiles, otherwise why it does not
@@ -25,11 +25,18 @@ readout on the right says `N parameters` while the graph compiles, otherwise why
compiling shows the reason on its body and a red frame; a source whose graph does not compile emits
nothing.
Below it, the **preview** strip emits the asset's own particles from its middle, with the type's
default rows (20 a second, one second each, a plain 16 px square). `Hold` stops the clock where it
is and turns into `Run`; the slider next to it jumps to any second in the first three lifetimes;
`Restart` begins the stream again at particle 0. The strip deliberately draws plain squares: which
picture a source throws belongs to the placed object, not to the asset the tab is showing.
Right of it, past a divider that drags, the **preview** panel emits the asset's own particles from
its middle, wearing **the rows the asset itself carries** — edit `Rate` or `Lifetime` on the asset
and the panel changes with them, including how far the slider reaches (three lifetimes). Rows the
asset leaves alone fall back to the type's defaults: 20 a second, one second each, a plain 16 px
square. It stands beside the graph rather than under it because a graph is read across: width is
what it can spare, height is not. `Hold` stops the clock where it is and turns into `Run`; the
slider jumps to any second in the first three lifetimes — and holds the panel too, so the button
reads `Run` after a drag as well; `Restart` begins the stream again at particle 0, which a held
panel shows as 0.00 s rather than keeping the second it stood on. **A hold owes nothing when it
ends**: letting the panel run again carries on from the second it was stopped at, however long it
stood there — the field's origin is moved, rather than the library clock being allowed to run on
underneath the freeze.
## The nodes
@@ -105,8 +112,8 @@ to a cold one over `Age`.
- **A source is culled by nothing.** Its particles may be anywhere on screen, so the source itself is
never skipped for being out of frame — with many sources in one scene that costs.
- **Particle time is the editor's clock, not the game's.** Pausing the game does not freeze a spray.
- **The preview shows the asset, not an instance**: the type's default rows and a plain square, never
the texture or the rate a particular placed source was given.
- **The preview shows the asset, not an instance**: the rows the asset carries, never the ones a
particular placed source was given — every one of them may be overridden where it is placed.
- **A source with `Emitting` off draws nothing at all** once the last particle has died, so it
disappears from the scene view; it is still in the hierarchy panel, and selecting it there shows
its handle.