Skip to content

feat: Let the length unit be set through initializeForge2D - #120

Merged
spydon merged 5 commits into
mainfrom
feat/length-units-per-meter
Aug 12, 2026
Merged

feat: Let the length unit be set through initializeForge2D#120
spydon merged 5 commits into
mainfrom
feat/length-units-per-meter

Conversation

@spydon

@spydon spydon commented Aug 11, 2026

Copy link
Copy Markdown
Member

Description

Box2D has a handful of tolerances that are absolute lengths rather than fractions of the shapes
they apply to. The most visible one is the speculative distance: manifold.c stops generating
contact points past B2_SPECULATIVE_DISTANCE (4 * B2_LINEAR_SLOP, so 0.02 m), and
contact.c sets touching = pointCount > 0, which means beginContact fires while there is
still a gap of up to 2 cm. A world laid out at a much smaller scale than a meter is dominated by
this: shapes that are only a couple of centimeters across are permanently in contact with their
neighbors.

This came up while migrating flame_forge2d (flame-engine/flame#3952), where a reporter's ball had
a radius of exactly 0.02, and it took a week to track down because the 0.02 is not discoverable
from Dart.

Box2D's answer is b2SetLengthUnitsPerMeter, which scales all of them. Its contract is
@warning This must be modified before any calls to Box2D, which a free-standing setter cannot
enforce, so it is exposed through the initializeForge2D gate that already has to run first:

await initializeForge2D(lengthUnitsPerMeter: 100);
  • Passing the value already in effect is a no-op, so several games that agree on a scale can each
    ask for it. The comparison round-trips through float32, since that is how Box2D stores it and
    values like 0.04 are not representable in either float width.
  • A value that conflicts with the one in effect throws a StateError once a World exists,
    rather than silently corrupting live simulations and the defaults Box2D hands out.
  • Non-positive and non-finite values throw an ArgumentError.

Tolerances exposes the derived constants (lengthUnitsPerMeter, linearSlop,
speculativeDistance, aabbMargin), so the 0.02 becomes a documented number that callers can
reason about and assert against instead of a mystery.

The web backend needs a keepalive wrapper: b2SetLengthUnitsPerMeter and
b2GetLengthUnitsPerMeter are plain B2_API functions, so emcc drops them without one.

The README gains a "Units" section covering the scale to lay a world out at, the absolute
tolerances that bite when you do not, and how the other quantities scale when you rescale a world
(lengths, velocities and gravity by S, masses by , forces and impulses by , torques by
S⁴, with densities, friction, restitution and damping unchanged, which leaves the timing of the
simulation unchanged).

Testing

dart test runs suites as isolates that share one process, and therefore one copy of the native
library, so a suite that changes the length unit would be visible to whichever suites run
alongside it. dart_test.yaml therefore sets concurrency: 1, so suites run one at a time as
part of the normal test run, and the mutating suite puts the length unit back when it is done.
The whole suite takes a couple of seconds either way.

melos test passes and melos analyze is clean.

Checklist

  • The title of my PR starts with a Conventional Commit prefix (fix:, feat:, docs: etc).
  • I have read the Contributor Guide and followed the process outlined for submitting PRs.
  • I have updated/added tests for ALL new/updated/fixed functionality.
  • I have updated/added relevant documentation in docs and added dartdoc comments with ///.
  • [-] I have updated/added relevant examples in examples.

Breaking Change

  • Yes, this is a breaking change.
  • No, this is not a breaking change.

Everything is additive: the new parameter is optional and defaults to leaving the length unit
alone, and Tolerances is a new class.

Related Issues

Needed by flame-engine/flame#3952, which uses it for worlds that cannot be laid out at a
realistic scale, and reports the underlying problem as a debug-mode warning.

spydon added 5 commits August 11, 2026 16:04
Box2D has a handful of tolerances that are absolute lengths rather than
fractions of the shapes they apply to, most visibly the speculative
distance of 0.02 m at which contacts start being reported. A world laid
out at a much smaller scale than a meter is dominated by them.

b2SetLengthUnitsPerMeter scales all of them, but it has to be called
before Box2D is touched at all, so it is exposed through the
initializeForge2D gate that already has to run first rather than as a
free-standing setter. Conflicting values throw instead of silently
corrupting the simulation; repeating the value in effect is a no-op, so
several games that agree on a scale can each ask for it.

Tolerances exposes the derived constants so that the 0.02 is
discoverable rather than mysterious.

The web backend needs a keepalive wrapper, since b2SetLengthUnitsPerMeter
is a plain B2_API function that emcc would otherwise drop.

The suites that change the length unit are tagged out of the normal run,
because dart test shares one process, and therefore one copy of the
native library, between suites.
emcc output is only reproducible per host platform, so a rebuild on
macOS or Windows does not match the Linux build byte for byte even on
the pinned emsdk. Uploading first makes the Linux build downloadable
exactly when the check fails and you need it to commit.
The build-wasm check compares against a Linux build, and emcc output is
only reproducible per host platform, so the macOS rebuild differed from
it despite being the same size and functionally identical.
They were tagged out into their own job so that they could not run
alongside suites that would see the length unit they change. Serializing
the suites achieves the same thing, and the whole suite takes about two
seconds either way, so the tag, the preset, the melos script and the
extra job were not buying anything.
spydon added a commit to flame-engine/flame that referenced this pull request Aug 11, 2026
flame_forge2d now needs initializeForge2D(lengthUnitsPerMeter:) and
Tolerances, which are in flame-engine/forge2d#120 and not in the
published 0.15.0. The constraint is raised to the ^0.16.0 that melos
will cut from that PR, with a git override so that the workspace
resolves in the meantime. Remove the override once 0.16.0 is out.

@ufrshubham ufrshubham left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great feature!

@spydon
spydon merged commit 5696a7b into main Aug 12, 2026
9 checks passed
@spydon
spydon deleted the feat/length-units-per-meter branch August 12, 2026 08:03
spydon added a commit to flame-engine/flame that referenced this pull request Aug 12, 2026
The length unit support in flame-engine/forge2d#120 was released as
0.15.1, so the git dependency override is no longer needed.
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.

2 participants