Shared Contracts & Export Format
Shared schemas and named data contracts live in one place. Simulation implementations are not all identical: the browser preview has a shared TypeScript engine, while the GDevelop extension keeps a separate runtime.
Single source of truth
contracts/contracts.json — particle record layout, SHAPE_ORDER,
easings, blend modes, export schema, and the morph window.
python tools/generate_contracts.py # regenerate the bindings
python tools/generate_contracts.py --check # CI drift gate
Edit the JSON, run the generator, never the generated outputs.
CI fails on stale files or drifted consumers.
Consumers
| Consumer | How it reads the contracts |
|---|---|
Python (editor/particle_studio.py) |
Imports the generated files directly |
C++ core (core/particle_core.cpp) |
Includes the generated header (contracts/gen/contracts.h) |
Browser TypeScript (lempo-runtime/) |
Shared source for browser-preview particle simulation and pure trail math; renderer adapters consume typed-array frames |
| GDevelop extension | Separate simulation implementation that consumes the export format |
Export format v1.2
- Validated against
contracts/gen/schema.jsonon save and load. - Easing values are hyphenated:
linear/ease-in/ease-out/ease-in-out. migrate_effect()heals old files automatically: missing version, v1.0 and v1.1 → v1.2, camelCase easings, missing blend mode / seed.- The effect version is append-only — new fields arrive with defaults (all OFF), so old files always load.
Particle record layout
One flat 22-field record per particle — the exact layout the parity tests compare:
[x, y, vx, vy, age, c0, c1, s0, s1, life, z, vz, shape, tracks,
dx, dy, dz, gx, gy, gz, sizeRatio, speedRatio]
| Fields | Meaning |
|---|---|
x, y, z | position (z/vz used in 3D) |
vx, vy, vz | velocity |
age, life | age vs lifetime (0..1 drives everything over-life) |
c0, c1 | state colors (birth → death) |
s0, s1 | size keyframes |
shape, tracks | morph shape ids + track bits |
dx, dy, dz | direction / alignment axis |
gx, gy, gz | guide/gizmo or per-particle offsets |
sizeRatio, speedRatio | per-particle spawn draws (seeded RNG) |
Determinism
A nonzero seed makes replay deterministic within each runtime. The preview
and GDevelop extension consume random values differently, so equal seeds do not guarantee
identical particle records between them:
| Engine | RNG |
|---|---|
Python (SimEngine) | _rand |
| C++ core | set_seed |
| Browser preview | mulberry32 |
| GDevelop extension | F.srand / F.rrand |
Force fields (v1.2) followed the same recipe: appended with defaults OFF,
so legacy motion stays bit-identical for old files. See
Testing for the gates that enforce this.