Skip to content

feat(dart): physics_basics example (reactphysics3d_dart + thermion) - #239

Open
nmfisher wants to merge 6 commits into
developfrom
asb/dart-physics-sample-clean
Open

feat(dart): physics_basics example (reactphysics3d_dart + thermion)#239
nmfisher wants to merge 6 commits into
developfrom
asb/dart-physics-sample-clean

Conversation

@nmfisher

@nmfisher nmfisher commented Aug 15, 2026

Copy link
Copy Markdown
Owner

Closes ticket the-tvf4.

Adds a physics_basics example: boxes and spheres dropped into a walled pen, simulated with reactphysics3d_dart and rendered with thermion_dart. Registered in the example registry for the headless CLI runner, and as the physics scene in the web gallery.

Supersedes #238.

Web path (this update)

Implements Option C of docs/research/web-physics-scope.md: ReactPhysics3D is linked into the single thermion_dart.wasm, so there is only one Emscripten module and the NativeLibrary.instance singleton conflict dissolves — the module that the gallery's existing initBindings("thermion_dart") call points at exports the _rp3d_* C API alongside the _Thermion_* one. No Dart-side changes, no ffigen_js work, no second loader.

This PR no longer builds ReactPhysics3D itself. reactphysics3d_dart now publishes prebuilt libraries from its own "Build artifacts" workflow (v* tag pushes), so thermion downloads the published artifact instead:

  • scripts/download_reactphysics3d.sh replaces scripts/build_reactphysics3d_web.sh. Pinned to RP3D_DART_TAG=v0.1.0, it stages exactly what the EXTERNAL_PROJECTS hook needs from that release:
    • a reactphysics3d_dart checkout at the tag — this provides native/web/reactphysics3d_dart.cmake (the file EXTERNAL_PROJECTS points at), the C API wrapper that cmake compiles from source, and the headers;
    • libreactphysics3d.a (engine, wasm32) from the reactphysics3d_dart-0.1.0-web-emscripten.zip release asset, dropped next to the cmake file, which imports it as a prebuilt library.
    • The zip's prebuilt wrapper (libreactphysics3d_dart.a) is deliberately not extracted: compiling the wrapper from source keeps the C API matched to the checked-out tag's vendored headers. Checkout and zip come from the same tag, so the bindings and the engine library cannot drift apart.
  • make wasm still configures with -DEXTERNAL_PROJECTS=<that .cmake>; the existing -Wl,--whole-archive group keeps the otherwise-unreferenced rp3d objects alive. EXTERNAL_PROJECTS_CMAKE= (empty) still builds a physics-less WASM.
  • scripts/verify_web_wasm_exports.sh is also removed: that repository's build-artifacts workflow verifies the exports of what it publishes, so thermion's link step only consumes it. The CI cache and paths-filter now key on the download script, so a tag bump in it re-fetches instead of rebuilding.

Verified locally (emsdk 6.0.4, same as CI): against a local mirror serving the genuine web-emscripten artifact from that workflow, the download script staged the tagged checkout + libreactphysics3d.a, was idempotent on re-run, and a clean make wasm produced thermion_dart.wasm (3.99 MB) with 142 _rp3d_* exports; a node smoke test ran the same create → 300 steps → destroy cycle as setupPhysicsBasics against the built WASM. dart analyze clean on thermion_dart and examples_lib.

One prerequisite: v0.1.0 must exist as a tag + release on reactphysics3d_dart (git tag v0.1.0 && git push origin v0.1.0 there). Until it does, make wasm fails with that exact instruction printed by the download script — it cannot fall back to a silent rp3d-less build.

Remaining blocker: reactphysics3d_dart ref bump

The three ffigen_js dependency_overrides stay — they cannot be deleted yet. At the pinned ref (52eec72e) reactphysics3d_dart still pins ffigen_js 0.0.5-pre, and removing the overrides makes dart pub get fail:

Because every version of reactphysics3d_dart from git depends on ffigen_js 0.0.5-pre
and every version of thermion_dart from path depends on ffigen_js ^0.0.14-pre,
reactphysics3d_dart from git is incompatible with thermion_dart from path.

Additionally, dart compile wasm of the web gallery currently fails inside reactphysics3d_dart (0.0.5-era Pointer operator[], missing generated bindings, a direct dart:ffi import) — note this is independent of the gallery registration here, because examples_lib.dart already exports physics_basics.dart, so the import chain from web_gallery pulls those files in either way.

Once reactphysics3d_dart lands the ffigen_js bump + regenerated web bindings, the follow-up on this repo is: bump the git ref in examples_lib/pubspec.yaml, bump RP3D_DART_TAG in scripts/download_reactphysics3d.sh, and delete the three overrides.

What CI must confirm

  • The real GitHub release download works end-to-end (the local verification used a mirror of the genuine artifact): Generate Artifacts → Compile web runs make wasm with rp3d linked — after v0.1.0 is pushed on reactphysics3d_dart.
  • On merge to develop, the R2 upload + web.version bump steps produce an artifact that actually contains rp3d — thermion_dart/native/web/web.version still points at 67bfb19 until then, so anything downloading before that still gets the rp3d-less wasm.
  • Deploy site (release tag) builds the gallery — blocked on the reactphysics3d_dart ref bump above.

Verified

  • headless runner produces output/physics_basics.png, dart analyze clean (from the original PR)
  • combined WASM builds from the downloaded release artifact and exports _rp3d_*; rp3d runs inside it (see above)

🤖 Generated with Claude Code

Scope (a) of the-tvf4: native/headless path only.

- New physics_basics example in examples_lib: boxes and spheres dropped
  into a walled pen, simulated with reactphysics3d_dart, rendered with
  thermion_dart. Fixed-step settle loop (no wall-clock), transforms
  copied to Thermion assets so the headless runner captures one
  deterministic frame.
- Registered in the example registry (headless CLI runner). Not wired
  into the web gallery: reactphysics3d_dart pins ffigen_js 0.0.5-pre,
  which conflicts with thermion_dart's ^0.0.14-pre, so the packages
  only resolve together via dependency_overrides (added to examples_lib,
  headless_runner and web_gallery roots, where pub honours them).
- Ubershader material instances created for body colours are destroyed
  on viewer dispose; leaving them alive deadlocks FilamentApp.destroy().
- Ignore headless_runner output PNGs.

Co-Authored-By: Claude <noreply@anthropic.com>
@nmfisher
nmfisher force-pushed the asb/dart-physics-sample-clean branch from 866eab8 to 470e44c Compare August 19, 2026 04:59
nmfisher and others added 5 commits August 19, 2026 14:07
Research notes for PR #239's blocked web path: exact ffigen_js version
conflict (0.0.5-pre exact pin vs ^0.0.14-pre, with the 0.0.5-generated
bindings being runtime-incompatible with the 0.0.14 runtime), the single
NativeLibrary.instance global in ffigen_js that prevents two WASM modules
per page, the unfinished reactphysics3d_dart web build, and thermion's
existing EXTERNAL_PROJECTS whole-archive mechanism as the recommended
path to one combined WASM.

Co-Authored-By: Claude <noreply@anthropic.com>
Wire the existing EXTERNAL_PROJECTS hook in thermion_dart/native/web/
CMakeLists.txt into `make wasm`, so reactphysics3d is linked into the single
thermion_dart WASM instead of being a second module.

- scripts/build_reactphysics3d_web.sh stages everything the hook needs: a
  reactphysics3d_dart checkout at a pinned ref (52eec72e, the same ref
  examples_lib depends on) and an Emscripten build of libreactphysics3d.a
  dropped next to reactphysics3d_dart.cmake, which imports it as a prebuilt
  library. The engine sources are not vendored by reactphysics3d_dart (only
  its headers are), so they are fetched from upstream v0.10.2 -- verified
  byte-identical to that tag's include/ tree, which is what the C API wrapper
  compiles against. Cached under native/web/lib/external/, which survives
  `make wasm-clean` and is gitignored/pubignored like the Filament web libs.
- scripts/verify_web_wasm_exports.sh gates the build: the link runs with
  -sERROR_ON_UNDEFINED_SYMBOLS=0 and pulls external projects in through
  -Wl,--whole-archive, so a misconfigured build still links and only fails in
  the browser. It checks the JS glue's Module["_name"]=wasmExports["..."]
  assignments (Emscripten minifies the wasm export names themselves).
- `make wasm` now stages rp3d, configures with -DEXTERNAL_PROJECTS=..., and
  verifies the exports afterwards. Set EXTERNAL_PROJECTS_CMAKE= to build
  without physics.
- analysis_options: the staged third-party sources under
  native/web/lib/external/ do not resolve as a package; exclude them.

One module means NativeLibrary.initBindings("thermion_dart") serves both
packages on the web, and the _rp3d_* C API is exported from the same Module
object as the _Thermion_* one -- no Dart-side changes and no ffigen_js work.
Design: docs/research/web-physics-scope.md (Option C).

Verified locally with emsdk 6.0.4: 142 _rp3d_* exports on the Module object
(rp3d_c_api.cpp has 142 EMSCRIPTEN_KEEPALIVE functions), and a
create-world/step/destroy cycle runs against the built wasm in node.
thermion_dart.wasm grows 3.43MB -> 3.99MB.

Co-Authored-By: Claude <noreply@anthropic.com>
The web job already runs `make wasm`, which now links ReactPhysics3D in and
verifies the _rp3d_* exports. Two adjustments so that actually happens on this
change set:

- paths-filter: the Makefile and the two scripts it calls decide what goes
  into the shipped thermion_dart.wasm, so they must trigger the web build the
  same way thermion_dart/native/** does.
- Cache the staged ReactPhysics3D sources and the Emscripten build of
  libreactphysics3d.a (native/web/lib/external/), keyed on the staging script
  so a ref bump in it rebuilds.

The zip this job uploads to R2 (and the web.version bump that points the
download tool at it) therefore carries rp3d from the next merge to develop
onwards.

Co-Authored-By: Claude <noreply@anthropic.com>
Register the scene as 'physics' in galleryScenes. With ReactPhysics3D linked
into the single thermion_dart WASM, the module that the gallery's existing
NativeLibrary.initBindings("thermion_dart") call points at exports the
_rp3d_* C API alongside the _Thermion_* one, so no loader or init changes are
needed -- the gallery's scene list is generated from galleryScenes.keys.

The ffigen_js dependency_overrides in the three example pubspecs stay for now:
reactphysics3d_dart at the pinned ref (52eec72e) still pins ffigen_js
0.0.5-pre, and removing them makes `dart pub get` fail with

  Because every version of reactphysics3d_dart from git depends on
  ffigen_js 0.0.5-pre and every version of thermion_dart from path depends on
  ffigen_js ^0.0.14-pre, reactphysics3d_dart from git is incompatible ...

The comments on them are updated to say what actually removes them: bump the
git ref once reactphysics3d_dart regenerates its web bindings under 0.0.14,
then delete the three overrides.

Known limitation while that ref is pinned: `dart compile wasm` of the gallery
fails inside reactphysics3d_dart itself (0.0.5-era Pointer operator[] /
missing bindings / a direct dart:ffi import), so the physics scene is not
servable until the ref bump. This is not caused by the gallery registration --
examples_lib already exports physics_basics.dart, so the import chain from
web_gallery pulls those files in regardless.

Co-Authored-By: Claude <noreply@anthropic.com>
reactphysics3d_dart now publishes prebuilt libraries from its own
"Build artifacts" workflow (v* tag pushes), so thermion no longer needs
to compile ReactPhysics3D itself.

Replace scripts/build_reactphysics3d_web.sh with
scripts/download_reactphysics3d.sh, which stages what the
EXTERNAL_PROJECTS hook needs from the pinned release (v0.1.0):

- a reactphysics3d_dart checkout at that tag, providing
  native/web/reactphysics3d_dart.cmake, the C API wrapper it compiles
  from source, and the headers, and
- libreactphysics3d.a (engine, wasm32) from the release zip, which the
  cmake hook imports as a prebuilt library. The prebuilt wrapper in the
  zip is not extracted -- compiling the wrapper from source keeps the
  C API matched to the checked-out tag's headers.

Both fetch and download come from the same tag, so the bindings and the
engine library cannot drift apart.

Also remove scripts/verify_web_wasm_exports.sh and its Makefile wiring:
that repository's build-artifacts workflow verifies the exports of what
it publishes, so thermion's link step only needs to consume it. The
Makefile keeps the EXTERNAL_PROJECTS_CMAKE escape hatch for a
physics-less WASM.

The CI cache and paths-filter now key on the download script, so a tag
bump in it re-fetches instead of rebuilding.

reactphysics3d_dart publishes on v* tag pushes only; until that
repository pushes v0.1.0, `make wasm` fails with instructions to cut
the release there. Verified locally against a mirror of the genuine
workflow artifact: clean `make wasm` produced thermion_dart.wasm with
142 _rp3d_* exports, and a node smoke test ran the same 300-step
simulation as the physics example.

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant