diff --git a/.github/workflows/build-wasm.yml b/.github/workflows/build-wasm.yml new file mode 100644 index 00000000..d6139c6a --- /dev/null +++ b/.github/workflows/build-wasm.yml @@ -0,0 +1,34 @@ +name: build-wasm + +on: + pull_request: + paths: + - "packages/forge2d/native/wasm/**" + - "packages/forge2d/third_party/box2d/**" + - "packages/forge2d/tool/build_wasm.sh" + - ".github/workflows/build-wasm.yml" + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: mymindstorm/setup-emsdk@v14 + with: + # Keep in sync with the version documented in tool/build_wasm.sh; + # the artifact check below relies on reproducible output. + version: 4.0.15 + - name: Rebuild box2d.wasm + run: packages/forge2d/tool/build_wasm.sh + - name: Verify the committed artifact is up to date + run: | + if ! git diff --exit-code --stat -- packages/forge2d/lib/src/backend/wasm/box2d.wasm; then + echo "The committed box2d.wasm differs from a fresh build." + echo "Rebuild it with tool/build_wasm.sh using emsdk 4.0.15." + exit 1 + fi + - uses: actions/upload-artifact@v4 + with: + name: box2d-wasm + path: packages/forge2d/lib/src/backend/wasm/box2d.wasm diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index a7c987e9..fd0947c6 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -68,4 +68,19 @@ jobs: - uses: subosito/flutter-action@v2 - uses: bluefireteam/melos-action@v3 - run: melos test + + test-web: + needs: [format, analyze] + strategy: + fail-fast: false + matrix: + compiler: [dart2js, dart2wasm] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + - uses: bluefireteam/melos-action@v3 + - name: Test the WebAssembly backend + working-directory: packages/forge2d + run: dart test -p chrome -c ${{ matrix.compiler }} # END TESTING STAGE diff --git a/README.md b/README.md index 5f5509e2..ff7f3375 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,16 @@ You can use it independently in Dart or in your - Dart 3.12+ (Flutter 3.38+), which builds the bundled C library automatically through build hooks. -- A C toolchain: Xcode on macOS/iOS, Visual Studio Build Tools on Windows, - clang or gcc on Linux, and the NDK on Android. -- Supported platforms: Android, iOS, macOS, Windows, and Linux. Web support - is planned via a WebAssembly build of Box2D behind the same API. +- A C toolchain on native platforms: Xcode on macOS/iOS, Visual Studio + Build Tools on Windows, clang or gcc on Linux, and the NDK on Android. +- Supported platforms: Android, iOS, macOS, Windows, Linux, and the web. + +On the web the same API runs against a bundled WebAssembly build of Box2D +(about 220 KB). No setup is needed: `initializeForge2D()` finds the module +at the package asset path in Dart web apps, and at the bundled package +asset in Flutter web apps. For custom hosting setups the location can be +overridden with `initializeForge2D(wasmUri: ...)`, and +`dart run forge2d:setup_web` copies the module into a `web/` directory. ## Getting started @@ -109,6 +115,12 @@ concepts map as follows: removed; stay on forge2d 0.14 if you depend on it. - Worlds default to `subStepCount: 4` in `step` instead of velocity and position iterations. +- A bare `World()` now has the Box2D default gravity of `(0, -10)`; the old + API defaulted to zero gravity. Top-down games should pass + `World(gravity: Vector2.zero())`. +- Destroying bodies, shapes, chains, or joints while the world is stepping + (from a collision callback) is deferred until the step ends; creating + them mid-step throws a `StateError` instead of the old silent queueing. ## Timeline diff --git a/packages/forge2d/bin/setup_web.dart b/packages/forge2d/bin/setup_web.dart new file mode 100644 index 00000000..bd928e38 --- /dev/null +++ b/packages/forge2d/bin/setup_web.dart @@ -0,0 +1,32 @@ +// Copies the bundled box2d.wasm into a web app's web/ directory, where +// initializeForge2D() finds it at runtime. +// +// Run from the root of your app: +// dart run forge2d:setup_web +// ignore_for_file: avoid_print +import 'dart:io'; +import 'dart:isolate'; + +Future main() async { + final packageUri = await Isolate.resolvePackageUri( + Uri.parse('package:forge2d/src/backend/wasm/box2d.wasm'), + ); + if (packageUri == null) { + stderr.writeln('Could not resolve the forge2d package.'); + exitCode = 1; + return; + } + + final webDirectory = Directory('web'); + if (!webDirectory.existsSync()) { + stderr.writeln( + 'No web/ directory found. Run this from the root of a web app.', + ); + exitCode = 1; + return; + } + + final target = File('web/box2d.wasm'); + File.fromUri(packageUri).copySync(target.path); + print('Copied box2d.wasm to ${target.path}'); +} diff --git a/packages/forge2d/dart_test.yaml b/packages/forge2d/dart_test.yaml new file mode 100644 index 00000000..f4134dec --- /dev/null +++ b/packages/forge2d/dart_test.yaml @@ -0,0 +1,5 @@ +# The default run exercises the FFI backend on the VM. Run the same suite +# against the WebAssembly backend with: +# dart test -p chrome (dart2js) +# dart test -p chrome -c dart2wasm +platforms: [vm] diff --git a/packages/forge2d/lib/src/api/body.dart b/packages/forge2d/lib/src/api/body.dart index a67bba9f..23f6a11e 100644 --- a/packages/forge2d/lib/src/api/body.dart +++ b/packages/forge2d/lib/src/api/body.dart @@ -336,8 +336,23 @@ class Body { } /// Creates a shape with the given [geometry] and attaches it to the body. + /// + /// Cannot be called while the world is stepping. Shape createShape(ShapeGeometry geometry, [ShapeDef? definition]) { + world.checkCanMutate('create a shape'); final shapeDefinition = definition ?? ShapeDef(); + assert( + shapeDefinition.density >= 0, + 'ShapeDef.density must not be negative', + ); + assert( + switch (geometry) { + Circle(:final radius) => radius > 0 && radius.isFinite, + Capsule(:final radius) => radius > 0 && radius.isFinite, + _ => true, + }, + 'The geometry radius must be positive and finite', + ); final rawDefinition = _rawShapeDef(shapeDefinition); final (shapeIndex1, shapeWorldAndGeneration) = switch (geometry) { Circle(:final center, :final radius) => rawBox2D.createCircleShape( @@ -407,7 +422,22 @@ class Body { } /// Creates a chain of one-sided segments and attaches it to the body. + /// + /// Chains need at least four points: open chains use the first and last + /// point as invisible ghost anchors that smooth collisions at the ends. + /// + /// Cannot be called while the world is stepping. Chain createChain(ChainDef definition) { + world.checkCanMutate('create a chain'); + if (definition.points.length < 4) { + throw ArgumentError('A chain needs at least four points'); + } + final materialCount = definition.materials.length; + if (materialCount != 1 && materialCount != definition.points.length) { + throw ArgumentError( + 'A chain needs one material, or one material per point', + ); + } final (chainIndex1, chainWorldAndGeneration) = rawBox2D.createChain( index1, worldAndGeneration, @@ -434,6 +464,10 @@ class Body { world.chainUserData[(chainIndex1, chainWorldAndGeneration)] = definition.userData; } + world.chainOwners[(chainIndex1, chainWorldAndGeneration)] = ( + index1, + worldAndGeneration, + ); return Chain.internal(world, chainIndex1, chainWorldAndGeneration); } @@ -455,14 +489,33 @@ class Body { ]; } - /// Destroys this body and all its shapes and joints. + /// Destroys this body and all its shapes, chains, and joints. + /// + /// Safe to call while the world is stepping (from a callback or while + /// processing events): the destruction is deferred until the step ends, + /// and the body stays valid until then. void destroy() { + if (world.locked) { + world.deferredActions.add(destroy); + return; + } + if (!isValid) { + // Also covers a deferred destroy that was requested more than once. + return; + } for (final shape in shapes) { world.shapeUserData.remove((shape.index1, shape.worldAndGeneration)); } for (final joint in joints) { world.jointUserData.remove((joint.index1, joint.worldAndGeneration)); } + world.chainOwners.removeWhere((chainId, ownerId) { + if (ownerId == (index1, worldAndGeneration)) { + world.chainUserData.remove(chainId); + return true; + } + return false; + }); world.bodyUserData.remove((index1, worldAndGeneration)); rawBox2D.destroyBody(index1, worldAndGeneration); } diff --git a/packages/forge2d/lib/src/api/chain.dart b/packages/forge2d/lib/src/api/chain.dart index c6161ff6..8b08db1a 100644 --- a/packages/forge2d/lib/src/api/chain.dart +++ b/packages/forge2d/lib/src/api/chain.dart @@ -63,8 +63,19 @@ class Chain { } /// Destroys this chain and its segment shapes. + /// + /// Safe to call while the world is stepping: the destruction is deferred + /// until the step ends, and the chain stays valid until then. void destroy() { + if (world.locked) { + world.deferredActions.add(destroy); + return; + } + if (!isValid) { + return; + } world.chainUserData.remove((index1, worldAndGeneration)); + world.chainOwners.remove((index1, worldAndGeneration)); rawBox2D.destroyChain(index1, worldAndGeneration); } diff --git a/packages/forge2d/lib/src/api/defs.dart b/packages/forge2d/lib/src/api/defs.dart index 9fe59283..cc087e08 100644 --- a/packages/forge2d/lib/src/api/defs.dart +++ b/packages/forge2d/lib/src/api/defs.dart @@ -180,7 +180,8 @@ class BodyDef { /// The speed below which the body may fall asleep, in meters per second. double sleepThreshold; - /// An optional name for debugging. + /// An optional name for debugging, truncated to 31 bytes by the native + /// library. String? name; /// An arbitrary object associated with the body. diff --git a/packages/forge2d/lib/src/api/joints/joint.dart b/packages/forge2d/lib/src/api/joints/joint.dart index e23ca227..97113c98 100644 --- a/packages/forge2d/lib/src/api/joints/joint.dart +++ b/packages/forge2d/lib/src/api/joints/joint.dart @@ -164,7 +164,17 @@ abstract base class Joint { } /// Destroys this joint. + /// + /// Safe to call while the world is stepping: the destruction is deferred + /// until the step ends, and the joint stays valid until then. void destroy() { + if (world.locked) { + world.deferredActions.add(destroy); + return; + } + if (!isValid) { + return; + } world.jointUserData.remove((index1, worldAndGeneration)); rawBox2D.destroyJoint(index1, worldAndGeneration); } diff --git a/packages/forge2d/lib/src/api/shape.dart b/packages/forge2d/lib/src/api/shape.dart index 2cf70ba2..5fc66b64 100644 --- a/packages/forge2d/lib/src/api/shape.dart +++ b/packages/forge2d/lib/src/api/shape.dart @@ -1,6 +1,7 @@ import 'package:forge2d/src/api/body.dart'; import 'package:forge2d/src/api/defs.dart'; import 'package:forge2d/src/api/enums.dart'; +import 'package:forge2d/src/api/geometry.dart'; import 'package:forge2d/src/api/math.dart'; import 'package:forge2d/src/api/world.dart'; import 'package:forge2d/src/initialize.dart'; @@ -149,6 +150,56 @@ class Shape { bool testPoint(Vector2 point) => rawBox2D.shapeTestPoint(index1, worldAndGeneration, point.x, point.y); + /// The geometry of the shape in the body's local coordinates, read back + /// from the simulation. + /// + /// Polygons return their convex hull, so a [Polygon.box] comes back as a + /// four-vertex [Polygon]. Chain segments come back as the [Segment] they + /// collide with. + ShapeGeometry get geometry { + switch (type) { + case ShapeType.circle: + final (centerX, centerY, radius) = rawBox2D.shapeGetCircle( + index1, + worldAndGeneration, + ); + return Circle(center: Vector2(centerX, centerY), radius: radius); + case ShapeType.capsule: + final (center1X, center1Y, center2X, center2Y, radius) = rawBox2D + .shapeGetCapsule(index1, worldAndGeneration); + return Capsule( + center1: Vector2(center1X, center1Y), + center2: Vector2(center2X, center2Y), + radius: radius, + ); + case ShapeType.segment: + final (point1X, point1Y, point2X, point2Y) = rawBox2D.shapeGetSegment( + index1, + worldAndGeneration, + ); + return Segment( + point1: Vector2(point1X, point1Y), + point2: Vector2(point2X, point2Y), + ); + case ShapeType.chainSegment: + final (point1X, point1Y, point2X, point2Y) = rawBox2D + .shapeGetChainSegment(index1, worldAndGeneration); + return Segment( + point1: Vector2(point1X, point1Y), + point2: Vector2(point2X, point2Y), + ); + case ShapeType.polygon: + final polygon = rawBox2D.shapeGetPolygon(index1, worldAndGeneration); + return Polygon( + [ + for (var i = 0; i < polygon.points.length; i += 2) + Vector2(polygon.points[i], polygon.points[i + 1]), + ], + radius: polygon.radius, + ); + } + } + /// The current world axis-aligned bounding box of the shape. Aabb get aabb { final (lowerX, lowerY, upperX, upperY) = rawBox2D.shapeGetAabb( @@ -173,7 +224,17 @@ class Shape { /// /// When [updateBodyMass] is false, call [Body.applyMassFromShapes] /// afterwards. + /// + /// Safe to call while the world is stepping: the destruction is deferred + /// until the step ends, and the shape stays valid until then. void destroy({bool updateBodyMass = true}) { + if (world.locked) { + world.deferredActions.add(() => destroy(updateBodyMass: updateBodyMass)); + return; + } + if (!isValid) { + return; + } world.shapeUserData.remove((index1, worldAndGeneration)); rawBox2D.destroyShape( index1, diff --git a/packages/forge2d/lib/src/api/world.dart b/packages/forge2d/lib/src/api/world.dart index 052e1e32..998f8e58 100644 --- a/packages/forge2d/lib/src/api/world.dart +++ b/packages/forge2d/lib/src/api/world.dart @@ -68,6 +68,33 @@ class World { @internal final Map<(int, int), Object?> jointUserData = {}; + /// The owning body of every chain, keyed by chain id, so that destroying + /// a body can release its chains' user data. + @internal + final Map<(int, int), (int, int)> chainOwners = {}; + + /// Whether the world is currently inside [step]. Mutating operations are + /// deferred or rejected while stepping. + @internal + bool locked = false; + + /// Destroy operations requested during [step], flushed when it returns. + @internal + final List deferredActions = []; + + /// Throws when the world is stepping: [operation] cannot run from inside + /// a callback. Destroy operations are deferred automatically instead. + @internal + void checkCanMutate(String operation) { + if (locked) { + throw StateError( + 'Cannot $operation while the world is stepping. Collision callbacks ' + 'run inside step(); create after step() returns instead. Destroy ' + 'operations are deferred automatically.', + ); + } + } + /// Whether this world has not been destroyed. bool get isValid => rawBox2D.worldIsValid(id); @@ -77,7 +104,19 @@ class World { /// for accuracy; 4 is a good default. void step(double timeStep, {int subStepCount = 4}) { assert(isValid, 'World has been destroyed'); - rawBox2D.worldStep(id, timeStep, subStepCount); + locked = true; + try { + rawBox2D.worldStep(id, timeStep, subStepCount); + } finally { + locked = false; + if (deferredActions.isNotEmpty) { + final actions = List.of(deferredActions); + deferredActions.clear(); + for (final action in actions) { + action(); + } + } + } } /// The gravity vector, in meters per second squared. @@ -103,9 +142,29 @@ class World { rawBox2D.worldEnableContinuous(id, enabled: value); /// Creates a body in this world. + /// + /// Cannot be called while the world is stepping. Body createBody([BodyDef? definition]) { assert(isValid, 'World has been destroyed'); + checkCanMutate('create a body'); final bodyDefinition = definition ?? BodyDef(); + assert( + bodyDefinition.position.x.isFinite && bodyDefinition.position.y.isFinite, + 'BodyDef.position must be finite', + ); + assert( + bodyDefinition.linearVelocity.x.isFinite && + bodyDefinition.linearVelocity.y.isFinite, + 'BodyDef.linearVelocity must be finite', + ); + assert( + bodyDefinition.angularVelocity.isFinite, + 'BodyDef.angularVelocity must be finite', + ); + assert( + bodyDefinition.linearDamping >= 0 && bodyDefinition.angularDamping >= 0, + 'BodyDef damping must not be negative', + ); final (index1, worldAndGeneration) = rawBox2D.createBody( id, type: bodyDefinition.type.index, @@ -136,6 +195,7 @@ class World { /// Creates a distance joint from [definition]. DistanceJoint createDistanceJoint(DistanceJointDef definition) { + checkCanMutate('create a joint'); final (index1, worldAndGeneration) = rawBox2D.createDistanceJoint( id, bodyA: (definition.bodyA.index1, definition.bodyA.worldAndGeneration), @@ -161,6 +221,7 @@ class World { /// Creates a filter joint from [definition], disabling collision between /// the two bodies. FilterJoint createFilterJoint(FilterJointDef definition) { + checkCanMutate('create a joint'); final (index1, worldAndGeneration) = rawBox2D.createFilterJoint( id, bodyA: (definition.bodyA.index1, definition.bodyA.worldAndGeneration), @@ -172,6 +233,7 @@ class World { /// Creates a motor joint from [definition]. MotorJoint createMotorJoint(MotorJointDef definition) { + checkCanMutate('create a joint'); final (index1, worldAndGeneration) = rawBox2D.createMotorJoint( id, bodyA: (definition.bodyA.index1, definition.bodyA.worldAndGeneration), @@ -189,6 +251,7 @@ class World { /// Creates a mouse joint from [definition]. MouseJoint createMouseJoint(MouseJointDef definition) { + checkCanMutate('create a joint'); final (index1, worldAndGeneration) = rawBox2D.createMouseJoint( id, bodyA: (definition.bodyA.index1, definition.bodyA.worldAndGeneration), @@ -205,6 +268,7 @@ class World { /// Creates a prismatic joint from [definition]. PrismaticJoint createPrismaticJoint(PrismaticJointDef definition) { + checkCanMutate('create a joint'); final (index1, worldAndGeneration) = rawBox2D.createPrismaticJoint( id, bodyA: (definition.bodyA.index1, definition.bodyA.worldAndGeneration), @@ -231,6 +295,7 @@ class World { /// Creates a revolute joint from [definition]. RevoluteJoint createRevoluteJoint(RevoluteJointDef definition) { + checkCanMutate('create a joint'); final (index1, worldAndGeneration) = rawBox2D.createRevoluteJoint( id, bodyA: (definition.bodyA.index1, definition.bodyA.worldAndGeneration), @@ -257,6 +322,7 @@ class World { /// Creates a weld joint from [definition]. WeldJoint createWeldJoint(WeldJointDef definition) { + checkCanMutate('create a joint'); final (index1, worldAndGeneration) = rawBox2D.createWeldJoint( id, bodyA: (definition.bodyA.index1, definition.bodyA.worldAndGeneration), @@ -276,6 +342,7 @@ class World { /// Creates a wheel joint from [definition]. WheelJoint createWheelJoint(WheelJointDef definition) { + checkCanMutate('create a joint'); final (index1, worldAndGeneration) = rawBox2D.createWheelJoint( id, bodyA: (definition.bodyA.index1, definition.bodyA.worldAndGeneration), @@ -685,10 +752,17 @@ class World { /// Destroys this world and everything in it. void destroy() { assert(isValid, 'World has been destroyed'); + checkCanMutate('destroy the world'); + // Unregister simulation callbacks so their closures do not outlive the + // world in the backend registries. + rawBox2D.worldSetCustomFilterCallback(id, null); + rawBox2D.worldSetPreSolveCallback(id, null); rawBox2D.destroyWorld(id); bodyUserData.clear(); shapeUserData.clear(); chainUserData.clear(); jointUserData.clear(); + chainOwners.clear(); + deferredActions.clear(); } } diff --git a/packages/forge2d/lib/src/backend/backend.dart b/packages/forge2d/lib/src/backend/backend.dart index 4cefdd17..6de61967 100644 --- a/packages/forge2d/lib/src/backend/backend.dart +++ b/packages/forge2d/lib/src/backend/backend.dart @@ -1,9 +1,11 @@ /// Compile-time selection of the platform backend. /// -/// Exports `createRawBox2D` and `initializeBackend`. The web backend will be -/// added here as a `dart.library.js_interop` branch. +/// Exports `createRawBox2D` and `initializeBackend`: dart:ffi against the +/// native library on native platforms, dart:js_interop against the bundled +/// box2d.wasm on the web. library; export 'package:forge2d/src/backend/raw_box2d.dart'; export 'package:forge2d/src/backend/raw_box2d_unsupported.dart' - if (dart.library.ffi) 'package:forge2d/src/backend/raw_box2d_ffi.dart'; + if (dart.library.ffi) 'package:forge2d/src/backend/raw_box2d_ffi.dart' + if (dart.library.js_interop) 'package:forge2d/src/backend/raw_box2d_wasm.dart'; diff --git a/packages/forge2d/lib/src/backend/raw_box2d.dart b/packages/forge2d/lib/src/backend/raw_box2d.dart index 7c79ca81..2798f31d 100644 --- a/packages/forge2d/lib/src/backend/raw_box2d.dart +++ b/packages/forge2d/lib/src/backend/raw_box2d.dart @@ -487,6 +487,38 @@ abstract interface class RawBox2D { /// Whether the given world point is inside the shape. bool shapeTestPoint(int index1, int worldAndGeneration, double x, double y); + /// Returns the geometry of a circle shape as (centerX, centerY, radius). + (double, double, double) shapeGetCircle(int index1, int worldAndGeneration); + + /// Returns the geometry of a capsule shape as + /// (center1X, center1Y, center2X, center2Y, radius). + (double, double, double, double, double) shapeGetCapsule( + int index1, + int worldAndGeneration, + ); + + /// Returns the geometry of a segment shape as + /// (point1X, point1Y, point2X, point2Y). + (double, double, double, double) shapeGetSegment( + int index1, + int worldAndGeneration, + ); + + /// Returns the inner segment of a chain segment shape as + /// (point1X, point1Y, point2X, point2Y). + (double, double, double, double) shapeGetChainSegment( + int index1, + int worldAndGeneration, + ); + + /// Returns the geometry of a polygon shape: the convex hull vertices + /// flattened as `[x0, y0, x1, y1, ...]` in local coordinates, and the + /// rounding radius. + ({List points, double radius}) shapeGetPolygon( + int index1, + int worldAndGeneration, + ); + /// Returns the current world axis-aligned bounding box of the shape as /// (lowerX, lowerY, upperX, upperY). (double, double, double, double) shapeGetAabb( diff --git a/packages/forge2d/lib/src/backend/raw_box2d_ffi.dart b/packages/forge2d/lib/src/backend/raw_box2d_ffi.dart index 26a1944b..f10c6838 100644 --- a/packages/forge2d/lib/src/backend/raw_box2d_ffi.dart +++ b/packages/forge2d/lib/src/backend/raw_box2d_ffi.dart @@ -5,8 +5,8 @@ import 'package:forge2d/src/backend/raw_box2d.dart'; import 'package:forge2d/src/ffi/box2d.g.dart' as b2; /// Prepares the FFI backend. Nothing to do: the native library is bundled by -/// the build hook and loaded lazily. -Future initializeBackend() async {} +/// the build hook and loaded lazily. [wasmUri] only applies on the web. +Future initializeBackend({Uri? wasmUri}) async {} /// Creates the FFI backend. RawBox2D createRawBox2D() => RawBox2DFfi(); @@ -871,6 +871,77 @@ final class RawBox2DFfi implements RawBox2D { bool shapeTestPoint(int index1, int worldAndGeneration, double x, double y) => b2.b2Shape_TestPoint(_shape(index1, worldAndGeneration), _vec2(x, y)); + @override + (double, double, double) shapeGetCircle( + int index1, + int worldAndGeneration, + ) { + final circle = b2.b2Shape_GetCircle(_shape(index1, worldAndGeneration)); + return (circle.center.x, circle.center.y, circle.radius); + } + + @override + (double, double, double, double, double) shapeGetCapsule( + int index1, + int worldAndGeneration, + ) { + final capsule = b2.b2Shape_GetCapsule(_shape(index1, worldAndGeneration)); + return ( + capsule.center1.x, + capsule.center1.y, + capsule.center2.x, + capsule.center2.y, + capsule.radius, + ); + } + + @override + (double, double, double, double) shapeGetSegment( + int index1, + int worldAndGeneration, + ) { + final segment = b2.b2Shape_GetSegment(_shape(index1, worldAndGeneration)); + return ( + segment.point1.x, + segment.point1.y, + segment.point2.x, + segment.point2.y, + ); + } + + @override + (double, double, double, double) shapeGetChainSegment( + int index1, + int worldAndGeneration, + ) { + final chainSegment = b2.b2Shape_GetChainSegment( + _shape(index1, worldAndGeneration), + ); + return ( + chainSegment.segment.point1.x, + chainSegment.segment.point1.y, + chainSegment.segment.point2.x, + chainSegment.segment.point2.y, + ); + } + + @override + ({List points, double radius}) shapeGetPolygon( + int index1, + int worldAndGeneration, + ) { + final polygon = b2.b2Shape_GetPolygon(_shape(index1, worldAndGeneration)); + return ( + points: [ + for (var i = 0; i < polygon.count; i++) ...[ + polygon.vertices[i].x, + polygon.vertices[i].y, + ], + ], + radius: polygon.radius, + ); + } + @override (double, double, double, double) shapeGetAabb( int index1, diff --git a/packages/forge2d/lib/src/backend/raw_box2d_unsupported.dart b/packages/forge2d/lib/src/backend/raw_box2d_unsupported.dart index ee0c40b2..5a7d03ff 100644 --- a/packages/forge2d/lib/src/backend/raw_box2d_unsupported.dart +++ b/packages/forge2d/lib/src/backend/raw_box2d_unsupported.dart @@ -1,7 +1,7 @@ import 'package:forge2d/src/backend/raw_box2d.dart'; /// Fails: this platform has no forge2d backend. -Future initializeBackend() async { +Future initializeBackend({Uri? wasmUri}) async { throw UnsupportedError(_message); } diff --git a/packages/forge2d/lib/src/backend/raw_box2d_wasm.dart b/packages/forge2d/lib/src/backend/raw_box2d_wasm.dart new file mode 100644 index 00000000..56d2e790 --- /dev/null +++ b/packages/forge2d/lib/src/backend/raw_box2d_wasm.dart @@ -0,0 +1,2836 @@ +import 'package:forge2d/src/backend/raw_box2d.dart'; +import 'package:forge2d/src/backend/wasm/wasm_runtime.dart'; + +WasmRuntime? _runtime; + +/// Loads and instantiates the Box2D WebAssembly module. +/// +/// Called by `initializeForge2D`. [wasmUri] overrides the default lookup +/// locations: the package asset path used by Dart web tooling, then a +/// `box2d.wasm` next to the page for Flutter web apps. +Future initializeBackend({Uri? wasmUri}) async { + if (_runtime != null) { + return; + } + _runtime = await WasmRuntime.load([ + if (wasmUri != null) + wasmUri + else ...[ + // Dart web tooling serves package files directly. + Uri.parse('packages/forge2d/src/backend/wasm/box2d.wasm'), + Uri.parse('/packages/forge2d/src/backend/wasm/box2d.wasm'), + // Flutter web bundles the module as a package asset automatically. + Uri.parse('assets/packages/forge2d/lib/src/backend/wasm/box2d.wasm'), + // Last resort: a manually hosted copy next to the page. + Uri.parse('box2d.wasm'), + ], + ]); +} + +/// Creates the WebAssembly backend. +RawBox2D createRawBox2D() { + final runtime = _runtime; + if (runtime == null) { + throw StateError( + 'forge2d is not initialized. On the web you must call and await ' + 'initializeForge2D() before creating a World.', + ); + } + return RawBox2DWasm(runtime); +} + +/// The dart:js_interop implementation of [RawBox2D], calling the `f2d_` +/// shim exports of box2d.wasm. See native/wasm/f2d_shim.c for the ABI. +final class RawBox2DWasm implements RawBox2D { + /// Creates the backend over a loaded [WasmRuntime]. + RawBox2DWasm(this._runtime); + + final WasmRuntime _runtime; + + // Scratch arena layout (see WasmRuntime.scratch, 4096 bytes): + // - byte 0: small out-parameters (up to 128 bytes) + // - byte 128: the f2dShapeDef struct (19 * 4 bytes) + // Strings go through the runtime's growable string buffer instead. + static const _outOffset = 0; + static const _shapeDefOffset = 128; + + int get _out => _runtime.scratch + _outOffset; + int get _shapeDefPointer => _runtime.scratch + _shapeDefOffset; + + num _call(String name, List arguments) => _runtime.call(name, arguments); + + double _callF(String name, List arguments) => + _call(name, arguments).toDouble(); + + int _callI(String name, List arguments) => + _call(name, arguments).toInt(); + + bool _callB(String name, List arguments) => _call(name, arguments) != 0; + + static int _b(bool value) => value ? 1 : 0; + + /// Splits a Dart int holding a 64-bit bit field into unsigned 32-bit + /// halves. -1 is the seam's "all bits set" sentinel; other negative + /// values are 64-bit patterns with the top bit set, which only exist + /// where ints are 64-bit (the VM and dart2wasm) and split exactly there. + static (int, int) _splitBits(int value) { + if (value == -1) { + return (0xFFFFFFFF, 0xFFFFFFFF); + } + if (value < 0) { + return (value & 0xFFFFFFFF, (value >> 32) & 0xFFFFFFFF); + } + return (value % 0x100000000, value ~/ 0x100000000); + } + + static int _joinBits(int low, int high) { + final unsignedLow = low < 0 ? low + 0x100000000 : low; + final unsignedHigh = high < 0 ? high + 0x100000000 : high; + if (unsignedLow == 0xFFFFFFFF && unsignedHigh == 0xFFFFFFFF) { + return -1; + } + return unsignedHigh * 0x100000000 + unsignedLow; + } + + (double, double) _outVec2() => + (_runtime.readF32(_out), _runtime.readF32(_out + 4)); + + /// Reinterprets a value read through a signed 32-bit view as unsigned, + /// keeping ids consistent with the unsigned values in event records even + /// once a slot's generation count sets the top bit. + static int _unsigned32(int value) => value < 0 ? value + 0x100000000 : value; + + /// Normalizes the worldAndGeneration halves of a flat id pair list. + static List _idPairs(List raw) => [ + for (var i = 0; i < raw.length; i++) i.isOdd ? _unsigned32(raw[i]) : raw[i], + ]; + + (int, int) _outIdPair() => ( + _runtime.readI32(_out), + _unsigned32(_runtime.readI32(_out + 4)), + ); + + // World. + + @override + int createWorld({ + required double gravityX, + required double gravityY, + required double restitutionThreshold, + required double hitEventThreshold, + required double contactHertz, + required double contactDampingRatio, + required double maxContactPushSpeed, + required double maximumLinearSpeed, + required bool enableSleep, + required bool enableContinuous, + }) => _callI('f2d_create_world', [ + gravityX, + gravityY, + restitutionThreshold, + hitEventThreshold, + contactHertz, + contactDampingRatio, + maxContactPushSpeed, + maximumLinearSpeed, + _b(enableSleep), + _b(enableContinuous), + ]); + + @override + void destroyWorld(int worldId) => _call('f2d_destroy_world', [worldId]); + + @override + bool worldIsValid(int worldId) => _callB('f2d_world_is_valid', [worldId]); + + @override + void worldStep(int worldId, double timeStep, int subStepCount) => + _call('f2d_world_step', [worldId, timeStep, subStepCount]); + + @override + void worldSetGravity(int worldId, double x, double y) => + _call('f2d_world_set_gravity', [worldId, x, y]); + + @override + (double, double) worldGetGravity(int worldId) { + _call('f2d_world_get_gravity', [worldId, _out]); + return _outVec2(); + } + + @override + void worldEnableSleeping(int worldId, {required bool enabled}) => + _call('f2d_world_enable_sleeping', [worldId, _b(enabled)]); + + @override + bool worldIsSleepingEnabled(int worldId) => + _callB('f2d_world_is_sleeping_enabled', [worldId]); + + @override + void worldEnableContinuous(int worldId, {required bool enabled}) => + _call('f2d_world_enable_continuous', [worldId, _b(enabled)]); + + @override + bool worldIsContinuousEnabled(int worldId) => + _callB('f2d_world_is_continuous_enabled', [worldId]); + + // Bodies. + + @override + (int, int) createBody( + int worldId, { + required int type, + required double positionX, + required double positionY, + required double rotationCos, + required double rotationSin, + required double linearVelocityX, + required double linearVelocityY, + required double angularVelocity, + required double linearDamping, + required double angularDamping, + required double gravityScale, + required double sleepThreshold, + required String? name, + required bool enableSleep, + required bool isAwake, + required bool fixedRotation, + required bool isBullet, + required bool isEnabled, + required bool allowFastRotation, + }) { + final namePointer = _runtime.writeCString(name); + _call('f2d_create_body', [ + worldId, + type, + positionX, + positionY, + rotationCos, + rotationSin, + linearVelocityX, + linearVelocityY, + angularVelocity, + linearDamping, + angularDamping, + gravityScale, + sleepThreshold, + namePointer, + _b(enableSleep), + _b(isAwake), + _b(fixedRotation), + _b(isBullet), + _b(isEnabled), + _b(allowFastRotation), + _out, + ]); + return _outIdPair(); + } + + @override + void destroyBody(int index1, int worldAndGeneration) => + _call('f2d_destroy_body', [index1, worldAndGeneration]); + + @override + bool bodyIsValid(int index1, int worldAndGeneration) => + _callB('f2d_body_is_valid', [index1, worldAndGeneration]); + + @override + (double, double) bodyGetPosition(int index1, int worldAndGeneration) { + _call('f2d_body_get_position', [index1, worldAndGeneration, _out]); + return _outVec2(); + } + + @override + (double, double) bodyGetRotation(int index1, int worldAndGeneration) { + _call('f2d_body_get_rotation', [index1, worldAndGeneration, _out]); + return _outVec2(); + } + + @override + void bodySetTransform( + int index1, + int worldAndGeneration, + double positionX, + double positionY, + double rotationCos, + double rotationSin, + ) => _call('f2d_body_set_transform', [ + index1, + worldAndGeneration, + positionX, + positionY, + rotationCos, + rotationSin, + ]); + + @override + (double, double) bodyGetLinearVelocity(int index1, int worldAndGeneration) { + _call('f2d_body_get_linear_velocity', [index1, worldAndGeneration, _out]); + return _outVec2(); + } + + @override + void bodySetLinearVelocity( + int index1, + int worldAndGeneration, + double x, + double y, + ) => + _call('f2d_body_set_linear_velocity', [index1, worldAndGeneration, x, y]); + + @override + double bodyGetAngularVelocity(int index1, int worldAndGeneration) => + _callF('f2d_body_get_angular_velocity', [index1, worldAndGeneration]); + + @override + void bodySetAngularVelocity( + int index1, + int worldAndGeneration, + double value, + ) => _call('f2d_body_set_angular_velocity', [ + index1, + worldAndGeneration, + value, + ]); + + @override + void bodyApplyForce( + int index1, + int worldAndGeneration, + double forceX, + double forceY, + double pointX, + double pointY, { + required bool wake, + }) => _call('f2d_body_apply_force', [ + index1, + worldAndGeneration, + forceX, + forceY, + pointX, + pointY, + _b(wake), + ]); + + @override + void bodyApplyForceToCenter( + int index1, + int worldAndGeneration, + double forceX, + double forceY, { + required bool wake, + }) => _call('f2d_body_apply_force_to_center', [ + index1, + worldAndGeneration, + forceX, + forceY, + _b(wake), + ]); + + @override + void bodyApplyTorque( + int index1, + int worldAndGeneration, + double torque, { + required bool wake, + }) => _call('f2d_body_apply_torque', [ + index1, + worldAndGeneration, + torque, + _b(wake), + ]); + + @override + void bodyApplyLinearImpulse( + int index1, + int worldAndGeneration, + double impulseX, + double impulseY, + double pointX, + double pointY, { + required bool wake, + }) => _call('f2d_body_apply_linear_impulse', [ + index1, + worldAndGeneration, + impulseX, + impulseY, + pointX, + pointY, + _b(wake), + ]); + + @override + void bodyApplyLinearImpulseToCenter( + int index1, + int worldAndGeneration, + double impulseX, + double impulseY, { + required bool wake, + }) => _call('f2d_body_apply_linear_impulse_to_center', [ + index1, + worldAndGeneration, + impulseX, + impulseY, + _b(wake), + ]); + + @override + void bodyApplyAngularImpulse( + int index1, + int worldAndGeneration, + double impulse, { + required bool wake, + }) => _call('f2d_body_apply_angular_impulse', [ + index1, + worldAndGeneration, + impulse, + _b(wake), + ]); + + @override + double bodyGetMass(int index1, int worldAndGeneration) => + _callF('f2d_body_get_mass', [index1, worldAndGeneration]); + + @override + double bodyGetRotationalInertia(int index1, int worldAndGeneration) => + _callF('f2d_body_get_rotational_inertia', [index1, worldAndGeneration]); + + @override + (double, double) bodyGetLocalCenterOfMass( + int index1, + int worldAndGeneration, + ) { + _call('f2d_body_get_local_center', [index1, worldAndGeneration, _out]); + return _outVec2(); + } + + @override + (double, double) bodyGetWorldCenterOfMass( + int index1, + int worldAndGeneration, + ) { + _call('f2d_body_get_world_center', [index1, worldAndGeneration, _out]); + return _outVec2(); + } + + @override + void bodySetMassData( + int index1, + int worldAndGeneration, + double mass, + double rotationalInertia, + double centerX, + double centerY, + ) => _call('f2d_body_set_mass_data', [ + index1, + worldAndGeneration, + mass, + rotationalInertia, + centerX, + centerY, + ]); + + @override + void bodyApplyMassFromShapes(int index1, int worldAndGeneration) => + _call('f2d_body_apply_mass_from_shapes', [index1, worldAndGeneration]); + + @override + int bodyGetType(int index1, int worldAndGeneration) => + _callI('f2d_body_get_type', [index1, worldAndGeneration]); + + @override + void bodySetType(int index1, int worldAndGeneration, int type) => + _call('f2d_body_set_type', [index1, worldAndGeneration, type]); + + @override + String bodyGetName(int index1, int worldAndGeneration) => _runtime + .readCString(_callI('f2d_body_get_name', [index1, worldAndGeneration])); + + @override + void bodySetName(int index1, int worldAndGeneration, String? name) => _call( + 'f2d_body_set_name', + [index1, worldAndGeneration, _runtime.writeCString(name)], + ); + + @override + bool bodyIsAwake(int index1, int worldAndGeneration) => + _callB('f2d_body_is_awake', [index1, worldAndGeneration]); + + @override + void bodySetAwake( + int index1, + int worldAndGeneration, { + required bool awake, + }) => _call('f2d_body_set_awake', [index1, worldAndGeneration, _b(awake)]); + + @override + bool bodyIsSleepEnabled(int index1, int worldAndGeneration) => + _callB('f2d_body_is_sleep_enabled', [index1, worldAndGeneration]); + + @override + void bodyEnableSleep( + int index1, + int worldAndGeneration, { + required bool enabled, + }) => + _call('f2d_body_enable_sleep', [index1, worldAndGeneration, _b(enabled)]); + + @override + double bodyGetSleepThreshold(int index1, int worldAndGeneration) => + _callF('f2d_body_get_sleep_threshold', [index1, worldAndGeneration]); + + @override + void bodySetSleepThreshold( + int index1, + int worldAndGeneration, + double value, + ) => _call('f2d_body_set_sleep_threshold', [ + index1, + worldAndGeneration, + value, + ]); + + @override + bool bodyIsEnabled(int index1, int worldAndGeneration) => + _callB('f2d_body_is_enabled', [index1, worldAndGeneration]); + + @override + void bodyDisable(int index1, int worldAndGeneration) => + _call('f2d_body_disable', [index1, worldAndGeneration]); + + @override + void bodyEnable(int index1, int worldAndGeneration) => + _call('f2d_body_enable', [index1, worldAndGeneration]); + + @override + bool bodyIsFixedRotation(int index1, int worldAndGeneration) => + _callB('f2d_body_is_fixed_rotation', [index1, worldAndGeneration]); + + @override + void bodySetFixedRotation( + int index1, + int worldAndGeneration, { + required bool flag, + }) => _call('f2d_body_set_fixed_rotation', [ + index1, + worldAndGeneration, + _b(flag), + ]); + + @override + bool bodyIsBullet(int index1, int worldAndGeneration) => + _callB('f2d_body_is_bullet', [index1, worldAndGeneration]); + + @override + void bodySetBullet( + int index1, + int worldAndGeneration, { + required bool flag, + }) => _call('f2d_body_set_bullet', [index1, worldAndGeneration, _b(flag)]); + + @override + double bodyGetGravityScale(int index1, int worldAndGeneration) => + _callF('f2d_body_get_gravity_scale', [index1, worldAndGeneration]); + + @override + void bodySetGravityScale(int index1, int worldAndGeneration, double scale) => + _call('f2d_body_set_gravity_scale', [index1, worldAndGeneration, scale]); + + @override + double bodyGetLinearDamping(int index1, int worldAndGeneration) => + _callF('f2d_body_get_linear_damping', [index1, worldAndGeneration]); + + @override + void bodySetLinearDamping( + int index1, + int worldAndGeneration, + double damping, + ) => _call('f2d_body_set_linear_damping', [ + index1, + worldAndGeneration, + damping, + ]); + + @override + double bodyGetAngularDamping(int index1, int worldAndGeneration) => + _callF('f2d_body_get_angular_damping', [index1, worldAndGeneration]); + + @override + void bodySetAngularDamping( + int index1, + int worldAndGeneration, + double damping, + ) => _call('f2d_body_set_angular_damping', [ + index1, + worldAndGeneration, + damping, + ]); + + @override + (double, double) bodyGetWorldPoint( + int index1, + int worldAndGeneration, + double x, + double y, + ) { + _call('f2d_body_get_world_point', [index1, worldAndGeneration, x, y, _out]); + return _outVec2(); + } + + @override + (double, double) bodyGetLocalPoint( + int index1, + int worldAndGeneration, + double x, + double y, + ) { + _call('f2d_body_get_local_point', [index1, worldAndGeneration, x, y, _out]); + return _outVec2(); + } + + @override + List bodyGetShapes(int index1, int worldAndGeneration) { + final count = _callI('f2d_body_get_shape_count', [ + index1, + worldAndGeneration, + ]); + if (count == 0) { + return const []; + } + final buffer = _runtime.bulk(count * 8); + final written = _callI('f2d_body_get_shapes', [ + index1, + worldAndGeneration, + buffer, + count, + ]); + return _idPairs(_runtime.readI32List(buffer, written * 2)); + } + + @override + List bodyGetJoints(int index1, int worldAndGeneration) { + final count = _callI('f2d_body_get_joint_count', [ + index1, + worldAndGeneration, + ]); + if (count == 0) { + return const []; + } + final buffer = _runtime.bulk(count * 8); + final written = _callI('f2d_body_get_joints', [ + index1, + worldAndGeneration, + buffer, + count, + ]); + return _idPairs(_runtime.readI32List(buffer, written * 2)); + } + + // Shapes. + + void _writeShapeDef(RawShapeDef definition) { + final pointer = _shapeDefPointer; + final (categoryLow, categoryHigh) = _splitBits(definition.categoryBits); + final (maskLow, maskHigh) = _splitBits(definition.maskBits); + _runtime + ..writeF32(pointer, definition.friction) + ..writeF32(pointer + 4, definition.restitution) + ..writeF32(pointer + 8, definition.rollingResistance) + ..writeF32(pointer + 12, definition.tangentSpeed) + ..writeI32(pointer + 16, definition.userMaterialId) + ..writeI32(pointer + 20, definition.customColor) + ..writeF32(pointer + 24, definition.density) + ..writeI32(pointer + 28, categoryLow) + ..writeI32(pointer + 32, categoryHigh) + ..writeI32(pointer + 36, maskLow) + ..writeI32(pointer + 40, maskHigh) + ..writeI32(pointer + 44, definition.groupIndex) + ..writeI32(pointer + 48, _b(definition.isSensor)) + ..writeI32(pointer + 52, _b(definition.enableSensorEvents)) + ..writeI32(pointer + 56, _b(definition.enableContactEvents)) + ..writeI32(pointer + 60, _b(definition.enableHitEvents)) + ..writeI32(pointer + 64, _b(definition.enablePreSolveEvents)) + ..writeI32(pointer + 68, _b(definition.invokeContactCreation)) + ..writeI32(pointer + 72, _b(definition.updateBodyMass)); + } + + @override + (int, int) createCircleShape( + int bodyIndex1, + int bodyWorldAndGeneration, { + required double centerX, + required double centerY, + required double radius, + required RawShapeDef definition, + }) { + _writeShapeDef(definition); + _call('f2d_create_circle_shape', [ + bodyIndex1, + bodyWorldAndGeneration, + _shapeDefPointer, + centerX, + centerY, + radius, + _out, + ]); + return _outIdPair(); + } + + @override + (int, int) createCapsuleShape( + int bodyIndex1, + int bodyWorldAndGeneration, { + required double center1X, + required double center1Y, + required double center2X, + required double center2Y, + required double radius, + required RawShapeDef definition, + }) { + _writeShapeDef(definition); + _call('f2d_create_capsule_shape', [ + bodyIndex1, + bodyWorldAndGeneration, + _shapeDefPointer, + center1X, + center1Y, + center2X, + center2Y, + radius, + _out, + ]); + return _outIdPair(); + } + + @override + (int, int) createSegmentShape( + int bodyIndex1, + int bodyWorldAndGeneration, { + required double point1X, + required double point1Y, + required double point2X, + required double point2Y, + required RawShapeDef definition, + }) { + _writeShapeDef(definition); + _call('f2d_create_segment_shape', [ + bodyIndex1, + bodyWorldAndGeneration, + _shapeDefPointer, + point1X, + point1Y, + point2X, + point2Y, + _out, + ]); + return _outIdPair(); + } + + @override + (int, int) createBoxShape( + int bodyIndex1, + int bodyWorldAndGeneration, { + required double halfWidth, + required double halfHeight, + required double centerX, + required double centerY, + required double rotationCos, + required double rotationSin, + required double radius, + required RawShapeDef definition, + }) { + _writeShapeDef(definition); + _call('f2d_create_box_shape', [ + bodyIndex1, + bodyWorldAndGeneration, + _shapeDefPointer, + halfWidth, + halfHeight, + centerX, + centerY, + rotationCos, + rotationSin, + radius, + _out, + ]); + return _outIdPair(); + } + + @override + (int, int) createPolygonShape( + int bodyIndex1, + int bodyWorldAndGeneration, { + required List points, + required double radius, + required RawShapeDef definition, + }) { + _writeShapeDef(definition); + final buffer = _runtime.bulk(points.length * 4); + _runtime.writeF32List(buffer, points); + final ok = _callB('f2d_create_polygon_shape', [ + bodyIndex1, + bodyWorldAndGeneration, + _shapeDefPointer, + buffer, + points.length ~/ 2, + radius, + _out, + ]); + if (!ok) { + throw ArgumentError( + 'The points do not form a valid convex hull: at least three ' + 'distinct, non-collinear points are required', + ); + } + return _outIdPair(); + } + + @override + void destroyShape( + int index1, + int worldAndGeneration, { + required bool updateBodyMass, + }) => _call('f2d_destroy_shape', [ + index1, + worldAndGeneration, + _b(updateBodyMass), + ]); + + @override + bool shapeIsValid(int index1, int worldAndGeneration) => + _callB('f2d_shape_is_valid', [index1, worldAndGeneration]); + + @override + int shapeGetType(int index1, int worldAndGeneration) => + _callI('f2d_shape_get_type', [index1, worldAndGeneration]); + + @override + (int, int) shapeGetBody(int index1, int worldAndGeneration) { + _call('f2d_shape_get_body', [index1, worldAndGeneration, _out]); + return _outIdPair(); + } + + @override + bool shapeIsSensor(int index1, int worldAndGeneration) => + _callB('f2d_shape_is_sensor', [index1, worldAndGeneration]); + + @override + double shapeGetDensity(int index1, int worldAndGeneration) => + _callF('f2d_shape_get_density', [index1, worldAndGeneration]); + + @override + void shapeSetDensity( + int index1, + int worldAndGeneration, + double density, { + required bool updateBodyMass, + }) => _call('f2d_shape_set_density', [ + index1, + worldAndGeneration, + density, + _b(updateBodyMass), + ]); + + @override + double shapeGetFriction(int index1, int worldAndGeneration) => + _callF('f2d_shape_get_friction', [index1, worldAndGeneration]); + + @override + void shapeSetFriction(int index1, int worldAndGeneration, double friction) => + _call('f2d_shape_set_friction', [index1, worldAndGeneration, friction]); + + @override + double shapeGetRestitution(int index1, int worldAndGeneration) => + _callF('f2d_shape_get_restitution', [index1, worldAndGeneration]); + + @override + void shapeSetRestitution( + int index1, + int worldAndGeneration, + double restitution, + ) => _call('f2d_shape_set_restitution', [ + index1, + worldAndGeneration, + restitution, + ]); + + @override + (int, int, int) shapeGetFilter(int index1, int worldAndGeneration) { + _call('f2d_shape_get_filter', [index1, worldAndGeneration, _out]); + return ( + _joinBits(_runtime.readI32(_out), _runtime.readI32(_out + 4)), + _joinBits(_runtime.readI32(_out + 8), _runtime.readI32(_out + 12)), + _runtime.readI32(_out + 16), + ); + } + + @override + void shapeSetFilter( + int index1, + int worldAndGeneration, + int categoryBits, + int maskBits, + int groupIndex, + ) { + final (categoryLow, categoryHigh) = _splitBits(categoryBits); + final (maskLow, maskHigh) = _splitBits(maskBits); + _call('f2d_shape_set_filter', [ + index1, + worldAndGeneration, + categoryLow, + categoryHigh, + maskLow, + maskHigh, + groupIndex, + ]); + } + + @override + bool shapeAreSensorEventsEnabled(int index1, int worldAndGeneration) => + _callB('f2d_shape_are_sensor_events_enabled', [ + index1, + worldAndGeneration, + ]); + + @override + void shapeEnableSensorEvents( + int index1, + int worldAndGeneration, { + required bool enabled, + }) => _call('f2d_shape_enable_sensor_events', [ + index1, + worldAndGeneration, + _b(enabled), + ]); + + @override + bool shapeAreContactEventsEnabled(int index1, int worldAndGeneration) => + _callB('f2d_shape_are_contact_events_enabled', [ + index1, + worldAndGeneration, + ]); + + @override + void shapeEnableContactEvents( + int index1, + int worldAndGeneration, { + required bool enabled, + }) => _call('f2d_shape_enable_contact_events', [ + index1, + worldAndGeneration, + _b(enabled), + ]); + + @override + bool shapeAreHitEventsEnabled(int index1, int worldAndGeneration) => + _callB('f2d_shape_are_hit_events_enabled', [index1, worldAndGeneration]); + + @override + void shapeEnableHitEvents( + int index1, + int worldAndGeneration, { + required bool enabled, + }) => _call('f2d_shape_enable_hit_events', [ + index1, + worldAndGeneration, + _b(enabled), + ]); + + @override + bool shapeArePreSolveEventsEnabled(int index1, int worldAndGeneration) => + _callB('f2d_shape_are_pre_solve_events_enabled', [ + index1, + worldAndGeneration, + ]); + + @override + void shapeEnablePreSolveEvents( + int index1, + int worldAndGeneration, { + required bool enabled, + }) => _call('f2d_shape_enable_pre_solve_events', [ + index1, + worldAndGeneration, + _b(enabled), + ]); + + @override + bool shapeTestPoint(int index1, int worldAndGeneration, double x, double y) => + _callB('f2d_shape_test_point', [index1, worldAndGeneration, x, y]); + + @override + (double, double, double) shapeGetCircle(int index1, int worldAndGeneration) { + _call('f2d_shape_get_circle', [index1, worldAndGeneration, _out]); + return ( + _runtime.readF32(_out), + _runtime.readF32(_out + 4), + _runtime.readF32(_out + 8), + ); + } + + @override + (double, double, double, double, double) shapeGetCapsule( + int index1, + int worldAndGeneration, + ) { + _call('f2d_shape_get_capsule', [index1, worldAndGeneration, _out]); + return ( + _runtime.readF32(_out), + _runtime.readF32(_out + 4), + _runtime.readF32(_out + 8), + _runtime.readF32(_out + 12), + _runtime.readF32(_out + 16), + ); + } + + @override + (double, double, double, double) shapeGetSegment( + int index1, + int worldAndGeneration, + ) { + _call('f2d_shape_get_segment', [index1, worldAndGeneration, _out]); + return ( + _runtime.readF32(_out), + _runtime.readF32(_out + 4), + _runtime.readF32(_out + 8), + _runtime.readF32(_out + 12), + ); + } + + @override + (double, double, double, double) shapeGetChainSegment( + int index1, + int worldAndGeneration, + ) { + _call('f2d_shape_get_chain_segment', [index1, worldAndGeneration, _out]); + return ( + _runtime.readF32(_out), + _runtime.readF32(_out + 4), + _runtime.readF32(_out + 8), + _runtime.readF32(_out + 12), + ); + } + + @override + ({List points, double radius}) shapeGetPolygon( + int index1, + int worldAndGeneration, + ) { + final count = _callI('f2d_shape_get_polygon', [ + index1, + worldAndGeneration, + _out, + ]); + return ( + points: _runtime.readF32List(_out + 4, count * 2), + radius: _runtime.readF32(_out), + ); + } + + @override + (double, double, double, double) shapeGetAabb( + int index1, + int worldAndGeneration, + ) { + _call('f2d_shape_get_aabb', [index1, worldAndGeneration, _out]); + return ( + _runtime.readF32(_out), + _runtime.readF32(_out + 4), + _runtime.readF32(_out + 8), + _runtime.readF32(_out + 12), + ); + } + + // Chains. + + @override + (int, int) createChain( + int bodyIndex1, + int bodyWorldAndGeneration, { + required List points, + required List materials, + required int categoryBits, + required int maskBits, + required int groupIndex, + required bool isLoop, + required bool enableSensorEvents, + }) { + final pointBytes = points.length * 4; + final buffer = _runtime.bulk(pointBytes + materials.length * 4); + _runtime + ..writeF32List(buffer, points) + ..writeF32List(buffer + pointBytes, materials); + final (categoryLow, categoryHigh) = _splitBits(categoryBits); + final (maskLow, maskHigh) = _splitBits(maskBits); + _call('f2d_create_chain', [ + bodyIndex1, + bodyWorldAndGeneration, + buffer, + points.length ~/ 2, + buffer + pointBytes, + materials.length ~/ 6, + categoryLow, + categoryHigh, + maskLow, + maskHigh, + groupIndex, + _b(isLoop), + _b(enableSensorEvents), + _out, + ]); + return _outIdPair(); + } + + @override + void destroyChain(int index1, int worldAndGeneration) => + _call('f2d_destroy_chain', [index1, worldAndGeneration]); + + @override + bool chainIsValid(int index1, int worldAndGeneration) => + _callB('f2d_chain_is_valid', [index1, worldAndGeneration]); + + @override + void chainSetFriction(int index1, int worldAndGeneration, double friction) => + _call('f2d_chain_set_friction', [index1, worldAndGeneration, friction]); + + @override + double chainGetFriction(int index1, int worldAndGeneration) => + _callF('f2d_chain_get_friction', [index1, worldAndGeneration]); + + @override + void chainSetRestitution( + int index1, + int worldAndGeneration, + double restitution, + ) => _call('f2d_chain_set_restitution', [ + index1, + worldAndGeneration, + restitution, + ]); + + @override + double chainGetRestitution(int index1, int worldAndGeneration) => + _callF('f2d_chain_get_restitution', [index1, worldAndGeneration]); + + @override + List chainGetSegments(int index1, int worldAndGeneration) { + final count = _callI('f2d_chain_get_segment_count', [ + index1, + worldAndGeneration, + ]); + if (count == 0) { + return const []; + } + final buffer = _runtime.bulk(count * 8); + final written = _callI('f2d_chain_get_segments', [ + index1, + worldAndGeneration, + buffer, + count, + ]); + return _idPairs(_runtime.readI32List(buffer, written * 2)); + } + + // Joints. + + @override + (int, int) createDistanceJoint( + int worldId, { + required (int, int) bodyA, + required (int, int) bodyB, + required (double, double) localAnchorA, + required (double, double) localAnchorB, + required double length, + required bool enableSpring, + required double hertz, + required double dampingRatio, + required bool enableLimit, + required double minLength, + required double maxLength, + required bool enableMotor, + required double maxMotorForce, + required double motorSpeed, + required bool collideConnected, + }) { + _call('f2d_create_distance_joint', [ + worldId, + bodyA.$1, + bodyA.$2, + bodyB.$1, + bodyB.$2, + localAnchorA.$1, + localAnchorA.$2, + localAnchorB.$1, + localAnchorB.$2, + length, + _b(enableSpring), + hertz, + dampingRatio, + _b(enableLimit), + minLength, + maxLength, + _b(enableMotor), + maxMotorForce, + motorSpeed, + _b(collideConnected), + _out, + ]); + return _outIdPair(); + } + + @override + (int, int) createFilterJoint( + int worldId, { + required (int, int) bodyA, + required (int, int) bodyB, + }) { + _call('f2d_create_filter_joint', [ + worldId, + bodyA.$1, + bodyA.$2, + bodyB.$1, + bodyB.$2, + _out, + ]); + return _outIdPair(); + } + + @override + (int, int) createMotorJoint( + int worldId, { + required (int, int) bodyA, + required (int, int) bodyB, + required (double, double) linearOffset, + required double angularOffset, + required double maxForce, + required double maxTorque, + required double correctionFactor, + required bool collideConnected, + }) { + _call('f2d_create_motor_joint', [ + worldId, + bodyA.$1, + bodyA.$2, + bodyB.$1, + bodyB.$2, + linearOffset.$1, + linearOffset.$2, + angularOffset, + maxForce, + maxTorque, + correctionFactor, + _b(collideConnected), + _out, + ]); + return _outIdPair(); + } + + @override + (int, int) createMouseJoint( + int worldId, { + required (int, int) bodyA, + required (int, int) bodyB, + required (double, double) target, + required double hertz, + required double dampingRatio, + required double maxForce, + required bool collideConnected, + }) { + _call('f2d_create_mouse_joint', [ + worldId, + bodyA.$1, + bodyA.$2, + bodyB.$1, + bodyB.$2, + target.$1, + target.$2, + hertz, + dampingRatio, + maxForce, + _b(collideConnected), + _out, + ]); + return _outIdPair(); + } + + @override + (int, int) createPrismaticJoint( + int worldId, { + required (int, int) bodyA, + required (int, int) bodyB, + required (double, double) localAnchorA, + required (double, double) localAnchorB, + required (double, double) localAxisA, + required double referenceAngle, + required double targetTranslation, + required bool enableSpring, + required double hertz, + required double dampingRatio, + required bool enableLimit, + required double lowerTranslation, + required double upperTranslation, + required bool enableMotor, + required double maxMotorForce, + required double motorSpeed, + required bool collideConnected, + }) { + _call('f2d_create_prismatic_joint', [ + worldId, + bodyA.$1, + bodyA.$2, + bodyB.$1, + bodyB.$2, + localAnchorA.$1, + localAnchorA.$2, + localAnchorB.$1, + localAnchorB.$2, + localAxisA.$1, + localAxisA.$2, + referenceAngle, + targetTranslation, + _b(enableSpring), + hertz, + dampingRatio, + _b(enableLimit), + lowerTranslation, + upperTranslation, + _b(enableMotor), + maxMotorForce, + motorSpeed, + _b(collideConnected), + _out, + ]); + return _outIdPair(); + } + + @override + (int, int) createRevoluteJoint( + int worldId, { + required (int, int) bodyA, + required (int, int) bodyB, + required (double, double) localAnchorA, + required (double, double) localAnchorB, + required double referenceAngle, + required double targetAngle, + required bool enableSpring, + required double hertz, + required double dampingRatio, + required bool enableLimit, + required double lowerAngle, + required double upperAngle, + required bool enableMotor, + required double maxMotorTorque, + required double motorSpeed, + required double drawSize, + required bool collideConnected, + }) { + _call('f2d_create_revolute_joint', [ + worldId, + bodyA.$1, + bodyA.$2, + bodyB.$1, + bodyB.$2, + localAnchorA.$1, + localAnchorA.$2, + localAnchorB.$1, + localAnchorB.$2, + referenceAngle, + targetAngle, + _b(enableSpring), + hertz, + dampingRatio, + _b(enableLimit), + lowerAngle, + upperAngle, + _b(enableMotor), + maxMotorTorque, + motorSpeed, + drawSize, + _b(collideConnected), + _out, + ]); + return _outIdPair(); + } + + @override + (int, int) createWeldJoint( + int worldId, { + required (int, int) bodyA, + required (int, int) bodyB, + required (double, double) localAnchorA, + required (double, double) localAnchorB, + required double referenceAngle, + required double linearHertz, + required double angularHertz, + required double linearDampingRatio, + required double angularDampingRatio, + required bool collideConnected, + }) { + _call('f2d_create_weld_joint', [ + worldId, + bodyA.$1, + bodyA.$2, + bodyB.$1, + bodyB.$2, + localAnchorA.$1, + localAnchorA.$2, + localAnchorB.$1, + localAnchorB.$2, + referenceAngle, + linearHertz, + angularHertz, + linearDampingRatio, + angularDampingRatio, + _b(collideConnected), + _out, + ]); + return _outIdPair(); + } + + @override + (int, int) createWheelJoint( + int worldId, { + required (int, int) bodyA, + required (int, int) bodyB, + required (double, double) localAnchorA, + required (double, double) localAnchorB, + required (double, double) localAxisA, + required bool enableSpring, + required double hertz, + required double dampingRatio, + required bool enableLimit, + required double lowerTranslation, + required double upperTranslation, + required bool enableMotor, + required double maxMotorTorque, + required double motorSpeed, + required bool collideConnected, + }) { + _call('f2d_create_wheel_joint', [ + worldId, + bodyA.$1, + bodyA.$2, + bodyB.$1, + bodyB.$2, + localAnchorA.$1, + localAnchorA.$2, + localAnchorB.$1, + localAnchorB.$2, + localAxisA.$1, + localAxisA.$2, + _b(enableSpring), + hertz, + dampingRatio, + _b(enableLimit), + lowerTranslation, + upperTranslation, + _b(enableMotor), + maxMotorTorque, + motorSpeed, + _b(collideConnected), + _out, + ]); + return _outIdPair(); + } + + @override + void destroyJoint(int index1, int worldAndGeneration) => + _call('f2d_destroy_joint', [index1, worldAndGeneration]); + + @override + bool jointIsValid(int index1, int worldAndGeneration) => + _callB('f2d_joint_is_valid', [index1, worldAndGeneration]); + + @override + int jointGetType(int index1, int worldAndGeneration) => + _callI('f2d_joint_get_type', [index1, worldAndGeneration]); + + @override + (int, int) jointGetBodyA(int index1, int worldAndGeneration) { + _call('f2d_joint_get_body_a', [index1, worldAndGeneration, _out]); + return _outIdPair(); + } + + @override + (int, int) jointGetBodyB(int index1, int worldAndGeneration) { + _call('f2d_joint_get_body_b', [index1, worldAndGeneration, _out]); + return _outIdPair(); + } + + @override + (double, double) jointGetLocalAnchorA(int index1, int worldAndGeneration) { + _call('f2d_joint_get_local_anchor_a', [index1, worldAndGeneration, _out]); + return _outVec2(); + } + + @override + (double, double) jointGetLocalAnchorB(int index1, int worldAndGeneration) { + _call('f2d_joint_get_local_anchor_b', [index1, worldAndGeneration, _out]); + return _outVec2(); + } + + @override + bool jointGetCollideConnected(int index1, int worldAndGeneration) => + _callB('f2d_joint_get_collide_connected', [index1, worldAndGeneration]); + + @override + void jointSetCollideConnected( + int index1, + int worldAndGeneration, { + required bool value, + }) => _call('f2d_joint_set_collide_connected', [ + index1, + worldAndGeneration, + _b(value), + ]); + + @override + void jointWakeBodies(int index1, int worldAndGeneration) => + _call('f2d_joint_wake_bodies', [index1, worldAndGeneration]); + + @override + (double, double) jointGetConstraintForce(int index1, int worldAndGeneration) { + _call('f2d_joint_get_constraint_force', [index1, worldAndGeneration, _out]); + return _outVec2(); + } + + @override + double jointGetConstraintTorque(int index1, int worldAndGeneration) => + _callF('f2d_joint_get_constraint_torque', [index1, worldAndGeneration]); + + // Distance joint. + + @override + double distanceJointGetLength(int index1, int worldAndGeneration) => + _callF('f2d_distance_joint_get_length', [index1, worldAndGeneration]); + + @override + void distanceJointSetLength( + int index1, + int worldAndGeneration, + double length, + ) => _call('f2d_distance_joint_set_length', [ + index1, + worldAndGeneration, + length, + ]); + + @override + double distanceJointGetCurrentLength(int index1, int worldAndGeneration) => + _callF('f2d_distance_joint_get_current_length', [ + index1, + worldAndGeneration, + ]); + + @override + bool distanceJointIsSpringEnabled(int index1, int worldAndGeneration) => + _callB('f2d_distance_joint_is_spring_enabled', [ + index1, + worldAndGeneration, + ]); + + @override + void distanceJointEnableSpring( + int index1, + int worldAndGeneration, { + required bool enabled, + }) => _call('f2d_distance_joint_enable_spring', [ + index1, + worldAndGeneration, + _b(enabled), + ]); + + @override + double distanceJointGetSpringHertz(int index1, int worldAndGeneration) => + _callF('f2d_distance_joint_get_spring_hertz', [ + index1, + worldAndGeneration, + ]); + + @override + void distanceJointSetSpringHertz( + int index1, + int worldAndGeneration, + double hertz, + ) => _call('f2d_distance_joint_set_spring_hertz', [ + index1, + worldAndGeneration, + hertz, + ]); + + @override + double distanceJointGetSpringDampingRatio( + int index1, + int worldAndGeneration, + ) => _callF('f2d_distance_joint_get_spring_damping_ratio', [ + index1, + worldAndGeneration, + ]); + + @override + void distanceJointSetSpringDampingRatio( + int index1, + int worldAndGeneration, + double ratio, + ) => _call('f2d_distance_joint_set_spring_damping_ratio', [ + index1, + worldAndGeneration, + ratio, + ]); + + @override + bool distanceJointIsLimitEnabled(int index1, int worldAndGeneration) => + _callB('f2d_distance_joint_is_limit_enabled', [ + index1, + worldAndGeneration, + ]); + + @override + void distanceJointEnableLimit( + int index1, + int worldAndGeneration, { + required bool enabled, + }) => _call('f2d_distance_joint_enable_limit', [ + index1, + worldAndGeneration, + _b(enabled), + ]); + + @override + double distanceJointGetMinLength(int index1, int worldAndGeneration) => + _callF('f2d_distance_joint_get_min_length', [index1, worldAndGeneration]); + + @override + double distanceJointGetMaxLength(int index1, int worldAndGeneration) => + _callF('f2d_distance_joint_get_max_length', [index1, worldAndGeneration]); + + @override + void distanceJointSetLengthRange( + int index1, + int worldAndGeneration, + double minLength, + double maxLength, + ) => _call('f2d_distance_joint_set_length_range', [ + index1, + worldAndGeneration, + minLength, + maxLength, + ]); + + @override + bool distanceJointIsMotorEnabled(int index1, int worldAndGeneration) => + _callB('f2d_distance_joint_is_motor_enabled', [ + index1, + worldAndGeneration, + ]); + + @override + void distanceJointEnableMotor( + int index1, + int worldAndGeneration, { + required bool enabled, + }) => _call('f2d_distance_joint_enable_motor', [ + index1, + worldAndGeneration, + _b(enabled), + ]); + + @override + double distanceJointGetMotorSpeed(int index1, int worldAndGeneration) => + _callF('f2d_distance_joint_get_motor_speed', [ + index1, + worldAndGeneration, + ]); + + @override + void distanceJointSetMotorSpeed( + int index1, + int worldAndGeneration, + double speed, + ) => _call('f2d_distance_joint_set_motor_speed', [ + index1, + worldAndGeneration, + speed, + ]); + + @override + double distanceJointGetMaxMotorForce(int index1, int worldAndGeneration) => + _callF('f2d_distance_joint_get_max_motor_force', [ + index1, + worldAndGeneration, + ]); + + @override + void distanceJointSetMaxMotorForce( + int index1, + int worldAndGeneration, + double force, + ) => _call('f2d_distance_joint_set_max_motor_force', [ + index1, + worldAndGeneration, + force, + ]); + + @override + double distanceJointGetMotorForce(int index1, int worldAndGeneration) => + _callF('f2d_distance_joint_get_motor_force', [ + index1, + worldAndGeneration, + ]); + + // Motor joint. + + @override + (double, double) motorJointGetLinearOffset( + int index1, + int worldAndGeneration, + ) { + _call('f2d_motor_joint_get_linear_offset', [ + index1, + worldAndGeneration, + _out, + ]); + return _outVec2(); + } + + @override + void motorJointSetLinearOffset( + int index1, + int worldAndGeneration, + double x, + double y, + ) => _call('f2d_motor_joint_set_linear_offset', [ + index1, + worldAndGeneration, + x, + y, + ]); + + @override + double motorJointGetAngularOffset(int index1, int worldAndGeneration) => + _callF('f2d_motor_joint_get_angular_offset', [ + index1, + worldAndGeneration, + ]); + + @override + void motorJointSetAngularOffset( + int index1, + int worldAndGeneration, + double offset, + ) => _call('f2d_motor_joint_set_angular_offset', [ + index1, + worldAndGeneration, + offset, + ]); + + @override + double motorJointGetMaxForce(int index1, int worldAndGeneration) => + _callF('f2d_motor_joint_get_max_force', [index1, worldAndGeneration]); + + @override + void motorJointSetMaxForce( + int index1, + int worldAndGeneration, + double force, + ) => _call('f2d_motor_joint_set_max_force', [ + index1, + worldAndGeneration, + force, + ]); + + @override + double motorJointGetMaxTorque(int index1, int worldAndGeneration) => + _callF('f2d_motor_joint_get_max_torque', [index1, worldAndGeneration]); + + @override + void motorJointSetMaxTorque( + int index1, + int worldAndGeneration, + double torque, + ) => _call('f2d_motor_joint_set_max_torque', [ + index1, + worldAndGeneration, + torque, + ]); + + @override + double motorJointGetCorrectionFactor(int index1, int worldAndGeneration) => + _callF('f2d_motor_joint_get_correction_factor', [ + index1, + worldAndGeneration, + ]); + + @override + void motorJointSetCorrectionFactor( + int index1, + int worldAndGeneration, + double factor, + ) => _call('f2d_motor_joint_set_correction_factor', [ + index1, + worldAndGeneration, + factor, + ]); + + // Mouse joint. + + @override + (double, double) mouseJointGetTarget(int index1, int worldAndGeneration) { + _call('f2d_mouse_joint_get_target', [index1, worldAndGeneration, _out]); + return _outVec2(); + } + + @override + void mouseJointSetTarget( + int index1, + int worldAndGeneration, + double x, + double y, + ) => _call('f2d_mouse_joint_set_target', [index1, worldAndGeneration, x, y]); + + @override + double mouseJointGetSpringHertz(int index1, int worldAndGeneration) => + _callF('f2d_mouse_joint_get_spring_hertz', [index1, worldAndGeneration]); + + @override + void mouseJointSetSpringHertz( + int index1, + int worldAndGeneration, + double hertz, + ) => _call('f2d_mouse_joint_set_spring_hertz', [ + index1, + worldAndGeneration, + hertz, + ]); + + @override + double mouseJointGetSpringDampingRatio(int index1, int worldAndGeneration) => + _callF('f2d_mouse_joint_get_spring_damping_ratio', [ + index1, + worldAndGeneration, + ]); + + @override + void mouseJointSetSpringDampingRatio( + int index1, + int worldAndGeneration, + double ratio, + ) => _call('f2d_mouse_joint_set_spring_damping_ratio', [ + index1, + worldAndGeneration, + ratio, + ]); + + @override + double mouseJointGetMaxForce(int index1, int worldAndGeneration) => + _callF('f2d_mouse_joint_get_max_force', [index1, worldAndGeneration]); + + @override + void mouseJointSetMaxForce( + int index1, + int worldAndGeneration, + double force, + ) => _call('f2d_mouse_joint_set_max_force', [ + index1, + worldAndGeneration, + force, + ]); + + // Prismatic joint. + + @override + bool prismaticJointIsSpringEnabled(int index1, int worldAndGeneration) => + _callB('f2d_prismatic_joint_is_spring_enabled', [ + index1, + worldAndGeneration, + ]); + + @override + void prismaticJointEnableSpring( + int index1, + int worldAndGeneration, { + required bool enabled, + }) => _call('f2d_prismatic_joint_enable_spring', [ + index1, + worldAndGeneration, + _b(enabled), + ]); + + @override + double prismaticJointGetSpringHertz(int index1, int worldAndGeneration) => + _callF('f2d_prismatic_joint_get_spring_hertz', [ + index1, + worldAndGeneration, + ]); + + @override + void prismaticJointSetSpringHertz( + int index1, + int worldAndGeneration, + double hertz, + ) => _call('f2d_prismatic_joint_set_spring_hertz', [ + index1, + worldAndGeneration, + hertz, + ]); + + @override + double prismaticJointGetSpringDampingRatio( + int index1, + int worldAndGeneration, + ) => _callF('f2d_prismatic_joint_get_spring_damping_ratio', [ + index1, + worldAndGeneration, + ]); + + @override + void prismaticJointSetSpringDampingRatio( + int index1, + int worldAndGeneration, + double ratio, + ) => _call('f2d_prismatic_joint_set_spring_damping_ratio', [ + index1, + worldAndGeneration, + ratio, + ]); + + @override + double prismaticJointGetTargetTranslation( + int index1, + int worldAndGeneration, + ) => _callF('f2d_prismatic_joint_get_target_translation', [ + index1, + worldAndGeneration, + ]); + + @override + void prismaticJointSetTargetTranslation( + int index1, + int worldAndGeneration, + double value, + ) => _call('f2d_prismatic_joint_set_target_translation', [ + index1, + worldAndGeneration, + value, + ]); + + @override + bool prismaticJointIsLimitEnabled(int index1, int worldAndGeneration) => + _callB('f2d_prismatic_joint_is_limit_enabled', [ + index1, + worldAndGeneration, + ]); + + @override + void prismaticJointEnableLimit( + int index1, + int worldAndGeneration, { + required bool enabled, + }) => _call('f2d_prismatic_joint_enable_limit', [ + index1, + worldAndGeneration, + _b(enabled), + ]); + + @override + double prismaticJointGetLowerLimit(int index1, int worldAndGeneration) => + _callF('f2d_prismatic_joint_get_lower_limit', [ + index1, + worldAndGeneration, + ]); + + @override + double prismaticJointGetUpperLimit(int index1, int worldAndGeneration) => + _callF('f2d_prismatic_joint_get_upper_limit', [ + index1, + worldAndGeneration, + ]); + + @override + void prismaticJointSetLimits( + int index1, + int worldAndGeneration, + double lower, + double upper, + ) => _call('f2d_prismatic_joint_set_limits', [ + index1, + worldAndGeneration, + lower, + upper, + ]); + + @override + bool prismaticJointIsMotorEnabled(int index1, int worldAndGeneration) => + _callB('f2d_prismatic_joint_is_motor_enabled', [ + index1, + worldAndGeneration, + ]); + + @override + void prismaticJointEnableMotor( + int index1, + int worldAndGeneration, { + required bool enabled, + }) => _call('f2d_prismatic_joint_enable_motor', [ + index1, + worldAndGeneration, + _b(enabled), + ]); + + @override + double prismaticJointGetMotorSpeed(int index1, int worldAndGeneration) => + _callF('f2d_prismatic_joint_get_motor_speed', [ + index1, + worldAndGeneration, + ]); + + @override + void prismaticJointSetMotorSpeed( + int index1, + int worldAndGeneration, + double speed, + ) => _call('f2d_prismatic_joint_set_motor_speed', [ + index1, + worldAndGeneration, + speed, + ]); + + @override + double prismaticJointGetMaxMotorForce(int index1, int worldAndGeneration) => + _callF('f2d_prismatic_joint_get_max_motor_force', [ + index1, + worldAndGeneration, + ]); + + @override + void prismaticJointSetMaxMotorForce( + int index1, + int worldAndGeneration, + double force, + ) => _call('f2d_prismatic_joint_set_max_motor_force', [ + index1, + worldAndGeneration, + force, + ]); + + @override + double prismaticJointGetMotorForce(int index1, int worldAndGeneration) => + _callF('f2d_prismatic_joint_get_motor_force', [ + index1, + worldAndGeneration, + ]); + + @override + double prismaticJointGetTranslation(int index1, int worldAndGeneration) => + _callF('f2d_prismatic_joint_get_translation', [ + index1, + worldAndGeneration, + ]); + + @override + double prismaticJointGetSpeed(int index1, int worldAndGeneration) => + _callF('f2d_prismatic_joint_get_speed', [index1, worldAndGeneration]); + + // Revolute joint. + + @override + bool revoluteJointIsSpringEnabled(int index1, int worldAndGeneration) => + _callB('f2d_revolute_joint_is_spring_enabled', [ + index1, + worldAndGeneration, + ]); + + @override + void revoluteJointEnableSpring( + int index1, + int worldAndGeneration, { + required bool enabled, + }) => _call('f2d_revolute_joint_enable_spring', [ + index1, + worldAndGeneration, + _b(enabled), + ]); + + @override + double revoluteJointGetSpringHertz(int index1, int worldAndGeneration) => + _callF('f2d_revolute_joint_get_spring_hertz', [ + index1, + worldAndGeneration, + ]); + + @override + void revoluteJointSetSpringHertz( + int index1, + int worldAndGeneration, + double hertz, + ) => _call('f2d_revolute_joint_set_spring_hertz', [ + index1, + worldAndGeneration, + hertz, + ]); + + @override + double revoluteJointGetSpringDampingRatio( + int index1, + int worldAndGeneration, + ) => _callF('f2d_revolute_joint_get_spring_damping_ratio', [ + index1, + worldAndGeneration, + ]); + + @override + void revoluteJointSetSpringDampingRatio( + int index1, + int worldAndGeneration, + double ratio, + ) => _call('f2d_revolute_joint_set_spring_damping_ratio', [ + index1, + worldAndGeneration, + ratio, + ]); + + @override + double revoluteJointGetTargetAngle(int index1, int worldAndGeneration) => + _callF('f2d_revolute_joint_get_target_angle', [ + index1, + worldAndGeneration, + ]); + + @override + void revoluteJointSetTargetAngle( + int index1, + int worldAndGeneration, + double angle, + ) => _call('f2d_revolute_joint_set_target_angle', [ + index1, + worldAndGeneration, + angle, + ]); + + @override + double revoluteJointGetAngle(int index1, int worldAndGeneration) => + _callF('f2d_revolute_joint_get_angle', [index1, worldAndGeneration]); + + @override + bool revoluteJointIsLimitEnabled(int index1, int worldAndGeneration) => + _callB('f2d_revolute_joint_is_limit_enabled', [ + index1, + worldAndGeneration, + ]); + + @override + void revoluteJointEnableLimit( + int index1, + int worldAndGeneration, { + required bool enabled, + }) => _call('f2d_revolute_joint_enable_limit', [ + index1, + worldAndGeneration, + _b(enabled), + ]); + + @override + double revoluteJointGetLowerLimit(int index1, int worldAndGeneration) => + _callF('f2d_revolute_joint_get_lower_limit', [ + index1, + worldAndGeneration, + ]); + + @override + double revoluteJointGetUpperLimit(int index1, int worldAndGeneration) => + _callF('f2d_revolute_joint_get_upper_limit', [ + index1, + worldAndGeneration, + ]); + + @override + void revoluteJointSetLimits( + int index1, + int worldAndGeneration, + double lower, + double upper, + ) => _call('f2d_revolute_joint_set_limits', [ + index1, + worldAndGeneration, + lower, + upper, + ]); + + @override + bool revoluteJointIsMotorEnabled(int index1, int worldAndGeneration) => + _callB('f2d_revolute_joint_is_motor_enabled', [ + index1, + worldAndGeneration, + ]); + + @override + void revoluteJointEnableMotor( + int index1, + int worldAndGeneration, { + required bool enabled, + }) => _call('f2d_revolute_joint_enable_motor', [ + index1, + worldAndGeneration, + _b(enabled), + ]); + + @override + double revoluteJointGetMotorSpeed(int index1, int worldAndGeneration) => + _callF('f2d_revolute_joint_get_motor_speed', [ + index1, + worldAndGeneration, + ]); + + @override + void revoluteJointSetMotorSpeed( + int index1, + int worldAndGeneration, + double speed, + ) => _call('f2d_revolute_joint_set_motor_speed', [ + index1, + worldAndGeneration, + speed, + ]); + + @override + double revoluteJointGetMaxMotorTorque(int index1, int worldAndGeneration) => + _callF('f2d_revolute_joint_get_max_motor_torque', [ + index1, + worldAndGeneration, + ]); + + @override + void revoluteJointSetMaxMotorTorque( + int index1, + int worldAndGeneration, + double torque, + ) => _call('f2d_revolute_joint_set_max_motor_torque', [ + index1, + worldAndGeneration, + torque, + ]); + + @override + double revoluteJointGetMotorTorque(int index1, int worldAndGeneration) => + _callF('f2d_revolute_joint_get_motor_torque', [ + index1, + worldAndGeneration, + ]); + + // Weld joint. + + @override + double weldJointGetLinearHertz(int index1, int worldAndGeneration) => + _callF('f2d_weld_joint_get_linear_hertz', [index1, worldAndGeneration]); + + @override + void weldJointSetLinearHertz( + int index1, + int worldAndGeneration, + double hertz, + ) => _call('f2d_weld_joint_set_linear_hertz', [ + index1, + worldAndGeneration, + hertz, + ]); + + @override + double weldJointGetAngularHertz(int index1, int worldAndGeneration) => + _callF('f2d_weld_joint_get_angular_hertz', [index1, worldAndGeneration]); + + @override + void weldJointSetAngularHertz( + int index1, + int worldAndGeneration, + double hertz, + ) => _call('f2d_weld_joint_set_angular_hertz', [ + index1, + worldAndGeneration, + hertz, + ]); + + @override + double weldJointGetLinearDampingRatio(int index1, int worldAndGeneration) => + _callF('f2d_weld_joint_get_linear_damping_ratio', [ + index1, + worldAndGeneration, + ]); + + @override + void weldJointSetLinearDampingRatio( + int index1, + int worldAndGeneration, + double ratio, + ) => _call('f2d_weld_joint_set_linear_damping_ratio', [ + index1, + worldAndGeneration, + ratio, + ]); + + @override + double weldJointGetAngularDampingRatio(int index1, int worldAndGeneration) => + _callF('f2d_weld_joint_get_angular_damping_ratio', [ + index1, + worldAndGeneration, + ]); + + @override + void weldJointSetAngularDampingRatio( + int index1, + int worldAndGeneration, + double ratio, + ) => _call('f2d_weld_joint_set_angular_damping_ratio', [ + index1, + worldAndGeneration, + ratio, + ]); + + // Wheel joint. + + @override + bool wheelJointIsSpringEnabled(int index1, int worldAndGeneration) => + _callB('f2d_wheel_joint_is_spring_enabled', [index1, worldAndGeneration]); + + @override + void wheelJointEnableSpring( + int index1, + int worldAndGeneration, { + required bool enabled, + }) => _call('f2d_wheel_joint_enable_spring', [ + index1, + worldAndGeneration, + _b(enabled), + ]); + + @override + double wheelJointGetSpringHertz(int index1, int worldAndGeneration) => + _callF('f2d_wheel_joint_get_spring_hertz', [index1, worldAndGeneration]); + + @override + void wheelJointSetSpringHertz( + int index1, + int worldAndGeneration, + double hertz, + ) => _call('f2d_wheel_joint_set_spring_hertz', [ + index1, + worldAndGeneration, + hertz, + ]); + + @override + double wheelJointGetSpringDampingRatio(int index1, int worldAndGeneration) => + _callF('f2d_wheel_joint_get_spring_damping_ratio', [ + index1, + worldAndGeneration, + ]); + + @override + void wheelJointSetSpringDampingRatio( + int index1, + int worldAndGeneration, + double ratio, + ) => _call('f2d_wheel_joint_set_spring_damping_ratio', [ + index1, + worldAndGeneration, + ratio, + ]); + + @override + bool wheelJointIsLimitEnabled(int index1, int worldAndGeneration) => + _callB('f2d_wheel_joint_is_limit_enabled', [index1, worldAndGeneration]); + + @override + void wheelJointEnableLimit( + int index1, + int worldAndGeneration, { + required bool enabled, + }) => _call('f2d_wheel_joint_enable_limit', [ + index1, + worldAndGeneration, + _b(enabled), + ]); + + @override + double wheelJointGetLowerLimit(int index1, int worldAndGeneration) => + _callF('f2d_wheel_joint_get_lower_limit', [index1, worldAndGeneration]); + + @override + double wheelJointGetUpperLimit(int index1, int worldAndGeneration) => + _callF('f2d_wheel_joint_get_upper_limit', [index1, worldAndGeneration]); + + @override + void wheelJointSetLimits( + int index1, + int worldAndGeneration, + double lower, + double upper, + ) => _call('f2d_wheel_joint_set_limits', [ + index1, + worldAndGeneration, + lower, + upper, + ]); + + @override + bool wheelJointIsMotorEnabled(int index1, int worldAndGeneration) => + _callB('f2d_wheel_joint_is_motor_enabled', [index1, worldAndGeneration]); + + @override + void wheelJointEnableMotor( + int index1, + int worldAndGeneration, { + required bool enabled, + }) => _call('f2d_wheel_joint_enable_motor', [ + index1, + worldAndGeneration, + _b(enabled), + ]); + + @override + double wheelJointGetMotorSpeed(int index1, int worldAndGeneration) => + _callF('f2d_wheel_joint_get_motor_speed', [index1, worldAndGeneration]); + + @override + void wheelJointSetMotorSpeed( + int index1, + int worldAndGeneration, + double speed, + ) => _call('f2d_wheel_joint_set_motor_speed', [ + index1, + worldAndGeneration, + speed, + ]); + + @override + double wheelJointGetMaxMotorTorque(int index1, int worldAndGeneration) => + _callF('f2d_wheel_joint_get_max_motor_torque', [ + index1, + worldAndGeneration, + ]); + + @override + void wheelJointSetMaxMotorTorque( + int index1, + int worldAndGeneration, + double torque, + ) => _call('f2d_wheel_joint_set_max_motor_torque', [ + index1, + worldAndGeneration, + torque, + ]); + + @override + double wheelJointGetMotorTorque(int index1, int worldAndGeneration) => + _callF('f2d_wheel_joint_get_motor_torque', [index1, worldAndGeneration]); + + // Events. + + @override + RawContactEvents worldGetContactEvents(int worldId) { + _call('f2d_world_get_contact_event_counts', [worldId, _out]); + final beginCount = _runtime.readI32(_out); + final endCount = _runtime.readI32(_out + 4); + final hitCount = _runtime.readI32(_out + 8); + + var begin = []; + if (beginCount > 0) { + final buffer = _runtime.bulk(beginCount * 13 * 8); + final written = _callI('f2d_world_get_contact_begin_events', [ + worldId, + buffer, + ]); + final data = _runtime.readF64List(buffer, written * 13); + begin = [ + for (var i = 0; i < written; i++) + ( + shapeAIndex1: data[i * 13].toInt(), + shapeAWorldAndGeneration: data[i * 13 + 1].toInt(), + shapeBIndex1: data[i * 13 + 2].toInt(), + shapeBWorldAndGeneration: data[i * 13 + 3].toInt(), + normalX: data[i * 13 + 4], + normalY: data[i * 13 + 5], + points: [ + for (var p = 0; p < data[i * 13 + 6].toInt(); p++) + ( + x: data[i * 13 + 7 + 3 * p], + y: data[i * 13 + 8 + 3 * p], + separation: data[i * 13 + 9 + 3 * p], + ), + ], + ), + ]; + } + + var end = []; + if (endCount > 0) { + final buffer = _runtime.bulk(endCount * 4 * 8); + final written = _callI('f2d_world_get_contact_end_events', [ + worldId, + buffer, + ]); + final data = _runtime.readF64List(buffer, written * 4); + end = [ + for (var i = 0; i < written; i++) + ( + shapeAIndex1: data[i * 4].toInt(), + shapeAWorldAndGeneration: data[i * 4 + 1].toInt(), + shapeBIndex1: data[i * 4 + 2].toInt(), + shapeBWorldAndGeneration: data[i * 4 + 3].toInt(), + ), + ]; + } + + var hit = []; + if (hitCount > 0) { + final buffer = _runtime.bulk(hitCount * 9 * 8); + final written = _callI('f2d_world_get_contact_hit_events', [ + worldId, + buffer, + ]); + final data = _runtime.readF64List(buffer, written * 9); + hit = [ + for (var i = 0; i < written; i++) + ( + shapeAIndex1: data[i * 9].toInt(), + shapeAWorldAndGeneration: data[i * 9 + 1].toInt(), + shapeBIndex1: data[i * 9 + 2].toInt(), + shapeBWorldAndGeneration: data[i * 9 + 3].toInt(), + pointX: data[i * 9 + 4], + pointY: data[i * 9 + 5], + normalX: data[i * 9 + 6], + normalY: data[i * 9 + 7], + approachSpeed: data[i * 9 + 8], + ), + ]; + } + + return (begin: begin, end: end, hit: hit); + } + + @override + RawSensorEvents worldGetSensorEvents(int worldId) { + _call('f2d_world_get_sensor_event_counts', [worldId, _out]); + final beginCount = _runtime.readI32(_out); + final endCount = _runtime.readI32(_out + 4); + + List read(String function, int count) { + if (count == 0) { + return const []; + } + final buffer = _runtime.bulk(count * 4 * 8); + final written = _callI(function, [worldId, buffer]); + final data = _runtime.readF64List(buffer, written * 4); + return [ + for (var i = 0; i < written; i++) + ( + sensorIndex1: data[i * 4].toInt(), + sensorWorldAndGeneration: data[i * 4 + 1].toInt(), + visitorIndex1: data[i * 4 + 2].toInt(), + visitorWorldAndGeneration: data[i * 4 + 3].toInt(), + ), + ]; + } + + return ( + begin: read('f2d_world_get_sensor_begin_events', beginCount), + end: read('f2d_world_get_sensor_end_events', endCount), + ); + } + + @override + List worldGetBodyEvents(int worldId) { + final count = _callI('f2d_world_get_body_event_count', [worldId]); + if (count == 0) { + return const []; + } + final buffer = _runtime.bulk(count * 7 * 8); + final written = _callI('f2d_world_get_body_events', [worldId, buffer]); + final data = _runtime.readF64List(buffer, written * 7); + return [ + for (var i = 0; i < written; i++) + ( + bodyIndex1: data[i * 7].toInt(), + bodyWorldAndGeneration: data[i * 7 + 1].toInt(), + x: data[i * 7 + 2], + y: data[i * 7 + 3], + rotationCos: data[i * 7 + 4], + rotationSin: data[i * 7 + 5], + fellAsleep: data[i * 7 + 6] != 0, + ), + ]; + } + + // Queries. + + @override + RawRayHit? worldCastRayClosest( + int worldId, + double originX, + double originY, + double translationX, + double translationY, + int categoryBits, + int maskBits, + ) { + final (categoryLow, categoryHigh) = _splitBits(categoryBits); + final (maskLow, maskHigh) = _splitBits(maskBits); + final hitSomething = _callB('f2d_world_cast_ray_closest', [ + worldId, + originX, + originY, + translationX, + translationY, + categoryLow, + categoryHigh, + maskLow, + maskHigh, + _out, + ]); + if (!hitSomething) { + return null; + } + final data = _runtime.readF64List(_out, 7); + return ( + shapeIndex1: data[0].toInt(), + shapeWorldAndGeneration: data[1].toInt(), + pointX: data[2], + pointY: data[3], + normalX: data[4], + normalY: data[5], + fraction: data[6], + ); + } + + @override + void worldCastRay( + int worldId, + double originX, + double originY, + double translationX, + double translationY, + int categoryBits, + int maskBits, + double Function(RawRayHit hit) callback, + ) { + final (categoryLow, categoryHigh) = _splitBits(categoryBits); + final (maskLow, maskHigh) = _splitBits(maskBits); + final previous = _runtime.callbacks.castRay; + _runtime.callbacks.castRay = + ( + index1, + worldAndGeneration, + pointX, + pointY, + normalX, + normalY, + fraction, + ) => callback(( + shapeIndex1: index1, + shapeWorldAndGeneration: worldAndGeneration, + pointX: pointX, + pointY: pointY, + normalX: normalX, + normalY: normalY, + fraction: fraction, + )); + try { + _call('f2d_world_cast_ray', [ + worldId, + originX, + originY, + translationX, + translationY, + categoryLow, + categoryHigh, + maskLow, + maskHigh, + ]); + } finally { + _runtime.callbacks.castRay = previous; + } + } + + @override + List worldOverlapAabb( + int worldId, + double lowerX, + double lowerY, + double upperX, + double upperY, + int categoryBits, + int maskBits, + ) { + final (categoryLow, categoryHigh) = _splitBits(categoryBits); + final (maskLow, maskHigh) = _splitBits(maskBits); + final results = []; + final previous = _runtime.callbacks.overlap; + _runtime.callbacks.overlap = (index1, worldAndGeneration) { + results + ..add(index1) + ..add(worldAndGeneration); + return true; + }; + try { + _call('f2d_world_overlap_aabb', [ + worldId, + lowerX, + lowerY, + upperX, + upperY, + categoryLow, + categoryHigh, + maskLow, + maskHigh, + ]); + } finally { + _runtime.callbacks.overlap = previous; + } + return results; + } + + @override + void worldExplode( + int worldId, { + required int maskBits, + required double positionX, + required double positionY, + required double radius, + required double falloff, + required double impulsePerLength, + }) { + final (maskLow, maskHigh) = _splitBits(maskBits); + _call('f2d_world_explode', [ + worldId, + maskLow, + maskHigh, + positionX, + positionY, + radius, + falloff, + impulsePerLength, + ]); + } + + // Simulation callbacks, dispatched by the world index embedded in the + // shape ids (the low 16 bits of worldAndGeneration). Note the id + // conventions: a world id's index1 is 1-based while a shape id's world0 + // is 0-based, hence the -1 when registering. + + final Map _filterCallbacks = {}; + final Map + _preSolveCallbacks = {}; + + static int _world0(int worldId) => (worldId & 0xFFFF) - 1; + + @override + void worldSetCustomFilterCallback( + int worldId, + bool Function( + int shapeAIndex1, + int shapeAWorldAndGeneration, + int shapeBIndex1, + int shapeBWorldAndGeneration, + )? + callback, + ) { + if (callback == null) { + _filterCallbacks.remove(_world0(worldId)); + _call('f2d_world_set_custom_filter', [worldId, 0]); + } else { + _filterCallbacks[_world0(worldId)] = callback; + _runtime.callbacks.customFilter = + (a1, aWorldAndGeneration, b1, bWorldAndGeneration) => + _filterCallbacks[aWorldAndGeneration & 0xFFFF]?.call( + a1, + aWorldAndGeneration, + b1, + bWorldAndGeneration, + ) ?? + true; + _call('f2d_world_set_custom_filter', [worldId, 1]); + } + } + + @override + void worldSetPreSolveCallback( + int worldId, + bool Function( + int shapeAIndex1, + int shapeAWorldAndGeneration, + int shapeBIndex1, + int shapeBWorldAndGeneration, + double normalX, + double normalY, + )? + callback, + ) { + if (callback == null) { + _preSolveCallbacks.remove(_world0(worldId)); + _call('f2d_world_set_pre_solve', [worldId, 0]); + } else { + _preSolveCallbacks[_world0(worldId)] = callback; + _runtime.callbacks.preSolve = + ( + a1, + aWorldAndGeneration, + b1, + bWorldAndGeneration, + normalX, + normalY, + ) => + _preSolveCallbacks[aWorldAndGeneration & 0xFFFF]?.call( + a1, + aWorldAndGeneration, + b1, + bWorldAndGeneration, + normalX, + normalY, + ) ?? + true; + _call('f2d_world_set_pre_solve', [worldId, 1]); + } + } + + // Debug drawing. + + @override + void worldDraw(int worldId, RawDebugDraw draw) { + final previous = _runtime.callbacks.draw; + _runtime.callbacks.draw = WasmDrawTarget( + drawPolygon: draw.drawPolygon, + drawSolidPolygon: draw.drawSolidPolygon, + drawCircle: draw.drawCircle, + drawSolidCircle: draw.drawSolidCircle, + drawSolidCapsule: draw.drawSolidCapsule, + drawSegment: draw.drawSegment, + drawTransform: draw.drawTransform, + drawPoint: draw.drawPoint, + drawString: draw.drawString, + ); + try { + final bounds = draw.drawingBounds; + _call('f2d_world_draw', [ + worldId, + _b(bounds != null), + bounds?.$1 ?? 0, + bounds?.$2 ?? 0, + bounds?.$3 ?? 0, + bounds?.$4 ?? 0, + _b(draw.drawShapes), + _b(draw.drawJoints), + _b(draw.drawJointExtras), + _b(draw.drawBounds), + _b(draw.drawMass), + _b(draw.drawBodyNames), + _b(draw.drawContacts), + _b(draw.drawGraphColors), + _b(draw.drawContactNormals), + _b(draw.drawContactImpulses), + _b(draw.drawContactFeatures), + _b(draw.drawFrictionImpulses), + _b(draw.drawIslands), + ]); + } finally { + _runtime.callbacks.draw = previous; + } + } +} diff --git a/packages/forge2d/lib/src/backend/wasm/box2d.wasm b/packages/forge2d/lib/src/backend/wasm/box2d.wasm new file mode 100755 index 00000000..037e3e46 Binary files /dev/null and b/packages/forge2d/lib/src/backend/wasm/box2d.wasm differ diff --git a/packages/forge2d/lib/src/backend/wasm/wasm_runtime.dart b/packages/forge2d/lib/src/backend/wasm/wasm_runtime.dart new file mode 100644 index 00000000..4977c588 --- /dev/null +++ b/packages/forge2d/lib/src/backend/wasm/wasm_runtime.dart @@ -0,0 +1,584 @@ +/// The WebAssembly runtime plumbing of the web backend: module loading, +/// WASI stubs, host callback imports, and memory access. +/// +/// Works under both dart2js and dart2wasm; only `dart:js_interop` is used. +library; + +import 'dart:convert'; +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; + +@JS('fetch') +external JSPromise<_Response> _fetch(JSString url); + +extension type _Response._(JSObject _) implements JSObject { + external bool get ok; + external JSPromise arrayBuffer(); +} + +@JS('WebAssembly.instantiate') +external JSPromise<_InstantiateResult> _instantiate( + JSArrayBuffer bytes, + JSObject imports, +); + +extension type _InstantiateResult._(JSObject _) implements JSObject { + external _Instance get instance; +} + +extension type _Instance._(JSObject _) implements JSObject { + external JSObject get exports; +} + +extension type _Memory._(JSObject _) implements JSObject { + external JSArrayBuffer get buffer; +} + +@JS('Float32Array') +extension type _F32._(JSObject _) implements JSObject { + external _F32(JSArrayBuffer buffer); + external double operator [](int index); + external void operator []=(int index, double value); +} + +@JS('Float64Array') +extension type _F64._(JSObject _) implements JSObject { + external _F64(JSArrayBuffer buffer); + external double operator [](int index); + external void operator []=(int index, double value); +} + +@JS('Int32Array') +extension type _I32._(JSObject _) implements JSObject { + external _I32(JSArrayBuffer buffer); + external int operator [](int index); + external void operator []=(int index, int value); +} + +@JS('Uint8Array') +extension type _U8._(JSObject _) implements JSObject { + external _U8(JSArrayBuffer buffer); + external int operator [](int index); + external void operator []=(int index, int value); + external int get length; +} + +@JS('performance.now') +external double _performanceNow(); + +/// The host callbacks the shim can invoke synchronously during a call into +/// the module. Dispatch targets are swapped in by the backend around each +/// native call that can call back. +class WasmHostCallbacks { + /// See `f2d_host_cast_ray` in f2d_shim.c. + double Function(int, int, double, double, double, double, double)? castRay; + + /// See `f2d_host_overlap`. + bool Function(int, int)? overlap; + + /// See `f2d_host_custom_filter`, keyed by nothing: the world's callback. + bool Function(int, int, int, int)? customFilter; + + /// See `f2d_host_pre_solve`. + bool Function(int, int, int, int, double, double)? preSolve; + + /// The debug draw dispatch target, non-null only during `worldDraw`. + WasmDrawTarget? draw; +} + +/// The debug draw callbacks, mirroring the seam's RawDebugDraw callbacks +/// but with vertex pointers resolved by the runtime. +class WasmDrawTarget { + /// Creates a draw target. + WasmDrawTarget({ + required this.drawPolygon, + required this.drawSolidPolygon, + required this.drawCircle, + required this.drawSolidCircle, + required this.drawSolidCapsule, + required this.drawSegment, + required this.drawTransform, + required this.drawPoint, + required this.drawString, + }); + + final void Function(List vertices, int color) drawPolygon; + final void Function( + double, + double, + double, + double, + List vertices, + double radius, + int color, + ) + drawSolidPolygon; + final void Function(double, double, double, int) drawCircle; + final void Function(double, double, double, double, double, int) + drawSolidCircle; + final void Function(double, double, double, double, double, int) + drawSolidCapsule; + final void Function(double, double, double, double, int) drawSegment; + final void Function(double, double, double, double) drawTransform; + final void Function(double, double, double, int) drawPoint; + final void Function(double, double, String, int) drawString; +} + +/// A loaded Box2D WebAssembly module with typed memory access and a scratch +/// arena for argument and result passing. +class WasmRuntime { + WasmRuntime._(this._exports, this._memory) { + _refreshViews(); + _scratch = _malloc(_scratchSize); + } + + final JSObject _exports; + final _Memory _memory; + + /// The host callback dispatch table. + final WasmHostCallbacks callbacks = WasmHostCallbacks(); + + static const _scratchSize = 4096; + + late _F32 _f32; + late _F64 _f64; + late _I32 _i32; + late _U8 _u8; + int _bufferLength = 0; + + /// The fixed scratch arena, at least [_scratchSize] bytes. + late final int _scratch; + + /// A growable secondary buffer for bulk data (events, vertex lists). + int _bulk = 0; + int _bulkCapacity = 0; + + /// A growable buffer for strings, separate from [bulk] so that calls can + /// pass a string and bulk data at the same time. + int _stringBuffer = 0; + int _stringCapacity = 0; + + final Map _functions = {}; + + /// Fetches and instantiates the module, trying each of [candidates] in + /// order until one loads. + static Future load(List candidates) async { + Object? lastError; + for (final candidate in candidates) { + try { + final response = await _fetch(candidate.toString().toJS).toDart; + if (!response.ok) { + lastError = StateError('HTTP error fetching $candidate'); + continue; + } + final bytes = await response.arrayBuffer().toDart; + return await instantiate(bytes); + } on Object catch (error) { + lastError = error; + } + } + throw StateError( + 'Could not load box2d.wasm from any of: ${candidates.join(', ')}. ' + 'Pass the location explicitly: initializeForge2D(wasmUri: ...). ' + 'Last error: $lastError', + ); + } + + /// Instantiates the module from raw [bytes]. + static Future instantiate(JSArrayBuffer bytes) async { + late WasmRuntime runtime; + + final env = JSObject(); + void set(String name, JSFunction function) => + env.setProperty(name.toJS, function); + + set( + 'f2d_host_cast_ray', + ( + JSNumber i1, + JSNumber wg, + JSNumber px, + JSNumber py, + JSNumber nx, + JSNumber ny, + JSNumber fraction, + ) { + final callback = runtime.callbacks.castRay; + return (callback == null + ? 1.0 + : callback( + i1.toDartInt, + wg.toDartInt, + px.toDartDouble, + py.toDartDouble, + nx.toDartDouble, + ny.toDartDouble, + fraction.toDartDouble, + )) + .toJS; + } + .toJS, + ); + set( + 'f2d_host_overlap', + (JSNumber i1, JSNumber wg) { + final callback = runtime.callbacks.overlap; + final result = callback == null || callback(i1.toDartInt, wg.toDartInt); + return (result ? 1 : 0).toJS; + }.toJS, + ); + set( + 'f2d_host_custom_filter', + (JSNumber ai1, JSNumber awg, JSNumber bi1, JSNumber bwg) { + final callback = runtime.callbacks.customFilter; + final result = + callback == null || + callback( + ai1.toDartInt, + awg.toDartInt, + bi1.toDartInt, + bwg.toDartInt, + ); + return (result ? 1 : 0).toJS; + }.toJS, + ); + set( + 'f2d_host_pre_solve', + ( + JSNumber ai1, + JSNumber awg, + JSNumber bi1, + JSNumber bwg, + JSNumber nx, + JSNumber ny, + ) { + final callback = runtime.callbacks.preSolve; + final result = + callback == null || + callback( + ai1.toDartInt, + awg.toDartInt, + bi1.toDartInt, + bwg.toDartInt, + nx.toDartDouble, + ny.toDartDouble, + ); + return (result ? 1 : 0).toJS; + } + .toJS, + ); + set( + 'f2d_host_draw_polygon', + (JSNumber vertices, JSNumber count, JSNumber color) { + runtime.callbacks.draw?.drawPolygon( + runtime.readF32List(vertices.toDartInt, count.toDartInt * 2), + color.toDartInt, + ); + }.toJS, + ); + set( + 'f2d_host_draw_solid_polygon', + ( + JSNumber px, + JSNumber py, + JSNumber qc, + JSNumber qs, + JSNumber vertices, + JSNumber count, + JSNumber radius, + JSNumber color, + ) { + runtime.callbacks.draw?.drawSolidPolygon( + px.toDartDouble, + py.toDartDouble, + qc.toDartDouble, + qs.toDartDouble, + runtime.readF32List(vertices.toDartInt, count.toDartInt * 2), + radius.toDartDouble, + color.toDartInt, + ); + } + .toJS, + ); + set( + 'f2d_host_draw_circle', + (JSNumber x, JSNumber y, JSNumber radius, JSNumber color) { + runtime.callbacks.draw?.drawCircle( + x.toDartDouble, + y.toDartDouble, + radius.toDartDouble, + color.toDartInt, + ); + }.toJS, + ); + set( + 'f2d_host_draw_solid_circle', + ( + JSNumber px, + JSNumber py, + JSNumber qc, + JSNumber qs, + JSNumber radius, + JSNumber color, + ) { + runtime.callbacks.draw?.drawSolidCircle( + px.toDartDouble, + py.toDartDouble, + qc.toDartDouble, + qs.toDartDouble, + radius.toDartDouble, + color.toDartInt, + ); + } + .toJS, + ); + set( + 'f2d_host_draw_solid_capsule', + ( + JSNumber p1x, + JSNumber p1y, + JSNumber p2x, + JSNumber p2y, + JSNumber radius, + JSNumber color, + ) { + runtime.callbacks.draw?.drawSolidCapsule( + p1x.toDartDouble, + p1y.toDartDouble, + p2x.toDartDouble, + p2y.toDartDouble, + radius.toDartDouble, + color.toDartInt, + ); + } + .toJS, + ); + set( + 'f2d_host_draw_segment', + (JSNumber p1x, JSNumber p1y, JSNumber p2x, JSNumber p2y, JSNumber color) { + runtime.callbacks.draw?.drawSegment( + p1x.toDartDouble, + p1y.toDartDouble, + p2x.toDartDouble, + p2y.toDartDouble, + color.toDartInt, + ); + }.toJS, + ); + set( + 'f2d_host_draw_transform', + (JSNumber px, JSNumber py, JSNumber qc, JSNumber qs) { + runtime.callbacks.draw?.drawTransform( + px.toDartDouble, + py.toDartDouble, + qc.toDartDouble, + qs.toDartDouble, + ); + }.toJS, + ); + set( + 'f2d_host_draw_point', + (JSNumber x, JSNumber y, JSNumber size, JSNumber color) { + runtime.callbacks.draw?.drawPoint( + x.toDartDouble, + y.toDartDouble, + size.toDartDouble, + color.toDartInt, + ); + }.toJS, + ); + set( + 'f2d_host_draw_string', + (JSNumber x, JSNumber y, JSNumber text, JSNumber color) { + runtime.callbacks.draw?.drawString( + x.toDartDouble, + y.toDartDouble, + runtime.readCString(text.toDartInt), + color.toDartInt, + ); + }.toJS, + ); + set( + 'emscripten_notify_memory_growth', + (JSNumber index) { + runtime._refreshViews(); + }.toJS, + ); + + final wasi = JSObject(); + // clock_time_get(clock_id, precision (i64), out_ptr) -> errno + wasi.setProperty( + 'clock_time_get'.toJS, + (JSNumber clockId, JSAny? precision, JSNumber outPointer) { + final nanoseconds = _performanceNow() * 1e6; + final low = (nanoseconds % 4294967296).floor(); + final high = (nanoseconds / 4294967296).floor(); + final index = outPointer.toDartInt ~/ 4; + runtime._i32[index] = low; + runtime._i32[index + 1] = high; + return 0.toJS; + }.toJS, + ); + // fd_write(fd, iovs, iovs_len, nwritten_ptr) -> errno; swallow output. + wasi.setProperty( + 'fd_write'.toJS, + (JSNumber fd, JSNumber iovs, JSNumber iovsLength, JSNumber nwritten) { + runtime._i32[nwritten.toDartInt ~/ 4] = 0; + return 0.toJS; + }.toJS, + ); + // proc_exit(code) -> never; assert failures end up here. + // ignore: prefer_function_declarations_over_variables + final JSNumber Function(JSNumber) procExit = (code) { + throw StateError('box2d.wasm exited with code ${code.toDartInt}'); + }; + wasi.setProperty('proc_exit'.toJS, procExit.toJS); + + final imports = JSObject() + ..setProperty('env'.toJS, env) + ..setProperty('wasi_snapshot_preview1'.toJS, wasi); + + final result = await _instantiate(bytes, imports).toDart; + final exports = result.instance.exports; + final memory = exports.getProperty('memory'.toJS)! as _Memory; + return runtime = WasmRuntime._(exports, memory); + } + + void _refreshViews() { + final buffer = _memory.buffer; + _f32 = _F32(buffer); + _f64 = _F64(buffer); + _i32 = _I32(buffer); + _u8 = _U8(buffer); + _bufferLength = _u8.length; + } + + void _checkViews() { + // Growth notifications cover emscripten-driven growth; this cheap check + // covers anything else. + if (_memory.buffer.byteLength != _bufferLength) { + _refreshViews(); + } + } + + int _malloc(int bytes) => + ((_functions['malloc'] ??= + _exports.getProperty('malloc'.toJS)! as JSFunction) + .callAsFunction(null, bytes.toJS)! + as JSNumber) + .toDartDouble + .toInt(); + + /// Calls an exported function with number arguments (bools as 0/1) and + /// returns its number result, or 0 for void functions. + num call(String name, List arguments) { + final function = _functions[name] ??= + _exports.getProperty(name.toJS)! as JSFunction; + final jsArguments = [for (final a in arguments) a.toJS]; + final result = function.callAsFunctionVarArgs(null, jsArguments); + _checkViews(); + return switch (result) { + null => 0, + final JSNumber number => number.toDartDouble, + _ => 0, + }; + } + + /// The fixed scratch arena pointer (4 KB). + int get scratch => _scratch; + + /// Returns a pointer to at least [bytes] of bulk storage. + int bulk(int bytes) { + if (bytes > _bulkCapacity) { + if (_bulk != 0) { + (_functions['free'] ??= + _exports.getProperty('free'.toJS)! as JSFunction) + .callAsFunction(null, _bulk.toJS); + } + _bulkCapacity = bytes * 2; + _bulk = _malloc(_bulkCapacity); + } + return _bulk; + } + + // Typed reads and writes. Pointers are byte addresses. + + double readF32(int pointer) => _f32[pointer ~/ 4]; + double readF64(int pointer) => _f64[pointer ~/ 8]; + int readI32(int pointer) => _i32[pointer ~/ 4]; + + void writeF32(int pointer, double value) => _f32[pointer ~/ 4] = value; + void writeF64(int pointer, double value) => _f64[pointer ~/ 8] = value; + void writeI32(int pointer, int value) => _i32[pointer ~/ 4] = value; + + List readF32List(int pointer, int count) { + final base = pointer ~/ 4; + return [for (var i = 0; i < count; i++) _f32[base + i]]; + } + + List readF64List(int pointer, int count) { + final base = pointer ~/ 8; + return [for (var i = 0; i < count; i++) _f64[base + i]]; + } + + List readI32List(int pointer, int count) { + final base = pointer ~/ 4; + return [for (var i = 0; i < count; i++) _i32[base + i]]; + } + + void writeF32List(int pointer, List values) { + final base = pointer ~/ 4; + for (var i = 0; i < values.length; i++) { + _f32[base + i] = values[i]; + } + } + + /// Writes [text] as a NUL-terminated UTF-8 string into the growable + /// string buffer and returns the pointer, or 0 for null. + int writeCString(String? text) { + if (text == null) { + return 0; + } + final bytes = utf8.encode(text); + if (bytes.length + 1 > _stringCapacity) { + if (_stringBuffer != 0) { + (_functions['free'] ??= + _exports.getProperty('free'.toJS)! as JSFunction) + .callAsFunction(null, _stringBuffer.toJS); + } + _stringCapacity = (bytes.length + 1) * 2; + _stringBuffer = _malloc(_stringCapacity); + } + for (var i = 0; i < bytes.length; i++) { + _u8[_stringBuffer + i] = bytes[i]; + } + _u8[_stringBuffer + bytes.length] = 0; + return _stringBuffer; + } + + /// Reads a NUL-terminated UTF-8 string. + String readCString(int pointer) { + if (pointer == 0) { + return ''; + } + final bytes = []; + var index = pointer; + while (true) { + final byte = _u8[index++]; + if (byte == 0) { + break; + } + bytes.add(byte); + } + return utf8.decode(bytes); + } +} + +extension on JSFunction { + JSAny? callAsFunctionVarArgs(JSAny? thisArg, List arguments) => + callMethodVarArgs('apply'.toJS, [thisArg, arguments.toJS]); +} + +extension on JSArrayBuffer { + external int get byteLength; +} diff --git a/packages/forge2d/lib/src/initialize.dart b/packages/forge2d/lib/src/initialize.dart index 1a552d9b..19e1a6a6 100644 --- a/packages/forge2d/lib/src/initialize.dart +++ b/packages/forge2d/lib/src/initialize.dart @@ -9,12 +9,17 @@ RawBox2D get rawBox2D => _rawBox2D ??= createRawBox2D(); /// Initializes forge2d. /// -/// On native platforms this completes immediately. On the web (in a future -/// release) it loads and instantiates the Box2D WebAssembly module, and -/// calling it before creating any world is mandatory. +/// On native platforms this completes immediately. On the web it fetches +/// and instantiates the Box2D WebAssembly module, and calling it before +/// creating any world is mandatory. +/// +/// On the web the module is looked up at the package asset path served by +/// the Dart web tooling, at the package asset bundled into Flutter web +/// apps, and finally at `box2d.wasm` relative to the page. [wasmUri] +/// overrides the lookup for custom hosting setups. /// /// Cross-platform code should always call and await this first. -Future initializeForge2D() async { - await initializeBackend(); +Future initializeForge2D({Uri? wasmUri}) async { + await initializeBackend(wasmUri: wasmUri); _rawBox2D ??= createRawBox2D(); } diff --git a/packages/forge2d/native/wasm/f2d_shim.c b/packages/forge2d/native/wasm/f2d_shim.c new file mode 100644 index 00000000..46c4be1b --- /dev/null +++ b/packages/forge2d/native/wasm/f2d_shim.c @@ -0,0 +1,1495 @@ +// The forge2d WebAssembly shim. +// +// The wasm C ABI passes structs indirectly, which makes the raw Box2D +// exports impractical to call from JavaScript. This shim wraps every +// function the Dart backend needs with a flat scalar/pointer signature: +// +// - World ids cross as one uint32: index1 | (generation << 16). +// - Body/shape/chain/joint ids cross as (int32 index1, uint32 world_and_generation) where +// world_and_generation packs world0 | (generation << 16), matching the Dart seam. +// - Vectors and tuples are written into caller-provided buffers. +// - 64-bit filter bits cross as two uint32 halves. +// - Events are copied into caller-provided float64 buffers (float64 +// represents every int32 exactly, which keeps one buffer per event kind). +// - Synchronous callbacks (queries, custom filter, pre-solve, debug draw) +// call fixed host imports provided at instantiation time. +// +// Keep this file in sync with lib/src/backend/raw_box2d_wasm.dart. + +#include +#include +#include + +#include "box2d/box2d.h" + +#define F2D_EXPORT EMSCRIPTEN_KEEPALIVE + +// Host imports, provided by the Dart side at instantiation. +extern float f2d_host_cast_ray(int32_t shape_index1, uint32_t shape_wg, + float px, float py, float nx, float ny, + float fraction); +extern int f2d_host_overlap(int32_t shape_index1, uint32_t shape_wg); +extern int f2d_host_custom_filter(int32_t a_index1, uint32_t a_wg, + int32_t b_index1, uint32_t b_wg); +extern int f2d_host_pre_solve(int32_t a_index1, uint32_t a_wg, + int32_t b_index1, uint32_t b_wg, float nx, + float ny); +extern void f2d_host_draw_polygon(const float* vertices, int count, + uint32_t color); +extern void f2d_host_draw_solid_polygon(float px, float py, float qc, float qs, + const float* vertices, int count, + float radius, uint32_t color); +extern void f2d_host_draw_circle(float x, float y, float radius, + uint32_t color); +extern void f2d_host_draw_solid_circle(float px, float py, float qc, float qs, + float radius, uint32_t color); +extern void f2d_host_draw_solid_capsule(float p1x, float p1y, float p2x, + float p2y, float radius, + uint32_t color); +extern void f2d_host_draw_segment(float p1x, float p1y, float p2x, float p2y, + uint32_t color); +extern void f2d_host_draw_transform(float px, float py, float qc, float qs); +extern void f2d_host_draw_point(float x, float y, float size, uint32_t color); +extern void f2d_host_draw_string(float x, float y, const char* text, + uint32_t color); + +// Id conversions. + +static b2WorldId f2d_world(uint32_t id) { + return (b2WorldId){(uint16_t)(id & 0xFFFF), (uint16_t)(id >> 16)}; +} + +static uint32_t f2d_pack_world(b2WorldId id) { + return (uint32_t)id.index1 | ((uint32_t)id.generation << 16); +} + +static b2BodyId f2d_body(int32_t index1, uint32_t world_and_generation) { + return (b2BodyId){index1, (uint16_t)(world_and_generation & 0xFFFF), (uint16_t)(world_and_generation >> 16)}; +} + +static b2ShapeId f2d_shape(int32_t index1, uint32_t world_and_generation) { + return (b2ShapeId){index1, (uint16_t)(world_and_generation & 0xFFFF), (uint16_t)(world_and_generation >> 16)}; +} + +static b2ChainId f2d_chain(int32_t index1, uint32_t world_and_generation) { + return (b2ChainId){index1, (uint16_t)(world_and_generation & 0xFFFF), (uint16_t)(world_and_generation >> 16)}; +} + +static b2JointId f2d_joint(int32_t index1, uint32_t world_and_generation) { + return (b2JointId){index1, (uint16_t)(world_and_generation & 0xFFFF), (uint16_t)(world_and_generation >> 16)}; +} + +static uint32_t f2d_wg_body(b2BodyId id) { + return (uint32_t)id.world0 | ((uint32_t)id.generation << 16); +} + +static uint32_t f2d_wg_shape(b2ShapeId id) { + return (uint32_t)id.world0 | ((uint32_t)id.generation << 16); +} + +static uint32_t f2d_wg_chain(b2ChainId id) { + return (uint32_t)id.world0 | ((uint32_t)id.generation << 16); +} + +static uint32_t f2d_wg_joint(b2JointId id) { + return (uint32_t)id.world0 | ((uint32_t)id.generation << 16); +} + +static uint64_t f2d_bits(uint32_t lo, uint32_t hi) { + return (uint64_t)lo | ((uint64_t)hi << 32); +} + +static b2Filter f2d_filter(uint32_t category_lo, uint32_t category_hi, + uint32_t mask_lo, uint32_t mask_hi, + int32_t group_index) { + b2Filter filter = b2DefaultFilter(); + filter.categoryBits = f2d_bits(category_lo, category_hi); + filter.maskBits = f2d_bits(mask_lo, mask_hi); + filter.groupIndex = group_index; + return filter; +} + +static b2QueryFilter f2d_query_filter(uint32_t category_lo, + uint32_t category_hi, uint32_t mask_lo, + uint32_t mask_hi) { + b2QueryFilter filter = b2DefaultQueryFilter(); + filter.categoryBits = f2d_bits(category_lo, category_hi); + filter.maskBits = f2d_bits(mask_lo, mask_hi); + return filter; +} + +// World. + +F2D_EXPORT uint32_t f2d_create_world( + float gravity_x, float gravity_y, float restitution_threshold, + float hit_event_threshold, float contact_hertz, + float contact_damping_ratio, float max_contact_push_speed, + float maximum_linear_speed, int enable_sleep, int enable_continuous) { + b2WorldDef def = b2DefaultWorldDef(); + def.gravity = (b2Vec2){gravity_x, gravity_y}; + def.restitutionThreshold = restitution_threshold; + def.hitEventThreshold = hit_event_threshold; + def.contactHertz = contact_hertz; + def.contactDampingRatio = contact_damping_ratio; + def.maxContactPushSpeed = max_contact_push_speed; + def.maximumLinearSpeed = maximum_linear_speed; + def.enableSleep = enable_sleep; + def.enableContinuous = enable_continuous; + return f2d_pack_world(b2CreateWorld(&def)); +} + +F2D_EXPORT void f2d_destroy_world(uint32_t w) { b2DestroyWorld(f2d_world(w)); } + +F2D_EXPORT int f2d_world_is_valid(uint32_t w) { + return b2World_IsValid(f2d_world(w)); +} + +F2D_EXPORT void f2d_world_step(uint32_t w, float time_step, int sub_steps) { + b2World_Step(f2d_world(w), time_step, sub_steps); +} + +F2D_EXPORT void f2d_world_set_gravity(uint32_t w, float x, float y) { + b2World_SetGravity(f2d_world(w), (b2Vec2){x, y}); +} + +F2D_EXPORT void f2d_world_get_gravity(uint32_t w, float* out) { + b2Vec2 gravity = b2World_GetGravity(f2d_world(w)); + out[0] = gravity.x; + out[1] = gravity.y; +} + +F2D_EXPORT void f2d_world_enable_sleeping(uint32_t w, int enabled) { + b2World_EnableSleeping(f2d_world(w), enabled); +} + +F2D_EXPORT int f2d_world_is_sleeping_enabled(uint32_t w) { + return b2World_IsSleepingEnabled(f2d_world(w)); +} + +F2D_EXPORT void f2d_world_enable_continuous(uint32_t w, int enabled) { + b2World_EnableContinuous(f2d_world(w), enabled); +} + +F2D_EXPORT int f2d_world_is_continuous_enabled(uint32_t w) { + return b2World_IsContinuousEnabled(f2d_world(w)); +} + +F2D_EXPORT void f2d_world_explode(uint32_t w, uint32_t mask_lo, + uint32_t mask_hi, float x, float y, + float radius, float falloff, + float impulse_per_length) { + b2ExplosionDef def = b2DefaultExplosionDef(); + def.maskBits = f2d_bits(mask_lo, mask_hi); + def.position = (b2Vec2){x, y}; + def.radius = radius; + def.falloff = falloff; + def.impulsePerLength = impulse_per_length; + b2World_Explode(f2d_world(w), &def); +} + +// Bodies. + +F2D_EXPORT void f2d_create_body( + uint32_t w, int type, float px, float py, float qc, float qs, float lvx, + float lvy, float angular_velocity, float linear_damping, + float angular_damping, float gravity_scale, float sleep_threshold, + const char* name, int enable_sleep, int is_awake, int fixed_rotation, + int is_bullet, int is_enabled, int allow_fast_rotation, int32_t* out) { + b2BodyDef def = b2DefaultBodyDef(); + def.type = (b2BodyType)type; + def.position = (b2Vec2){px, py}; + def.rotation = (b2Rot){qc, qs}; + def.linearVelocity = (b2Vec2){lvx, lvy}; + def.angularVelocity = angular_velocity; + def.linearDamping = linear_damping; + def.angularDamping = angular_damping; + def.gravityScale = gravity_scale; + def.sleepThreshold = sleep_threshold; + def.name = name; + def.enableSleep = enable_sleep; + def.isAwake = is_awake; + def.fixedRotation = fixed_rotation; + def.isBullet = is_bullet; + def.isEnabled = is_enabled; + def.allowFastRotation = allow_fast_rotation; + b2BodyId id = b2CreateBody(f2d_world(w), &def); + out[0] = id.index1; + out[1] = (int32_t)f2d_wg_body(id); +} + +F2D_EXPORT void f2d_destroy_body(int32_t index1, uint32_t world_and_generation) { + b2DestroyBody(f2d_body(index1, world_and_generation)); +} + +F2D_EXPORT int f2d_body_is_valid(int32_t index1, uint32_t world_and_generation) { + return b2Body_IsValid(f2d_body(index1, world_and_generation)); +} + +F2D_EXPORT void f2d_body_get_position(int32_t index1, uint32_t world_and_generation, float* out) { + b2Vec2 position = b2Body_GetPosition(f2d_body(index1, world_and_generation)); + out[0] = position.x; + out[1] = position.y; +} + +F2D_EXPORT void f2d_body_get_rotation(int32_t index1, uint32_t world_and_generation, float* out) { + b2Rot rotation = b2Body_GetRotation(f2d_body(index1, world_and_generation)); + out[0] = rotation.c; + out[1] = rotation.s; +} + +F2D_EXPORT void f2d_body_set_transform(int32_t index1, uint32_t world_and_generation, float px, + float py, float qc, float qs) { + b2Body_SetTransform(f2d_body(index1, world_and_generation), (b2Vec2){px, py}, (b2Rot){qc, qs}); +} + +F2D_EXPORT void f2d_body_get_linear_velocity(int32_t index1, uint32_t world_and_generation, + float* out) { + b2Vec2 velocity = b2Body_GetLinearVelocity(f2d_body(index1, world_and_generation)); + out[0] = velocity.x; + out[1] = velocity.y; +} + +F2D_EXPORT void f2d_body_set_linear_velocity(int32_t index1, uint32_t world_and_generation, float x, + float y) { + b2Body_SetLinearVelocity(f2d_body(index1, world_and_generation), (b2Vec2){x, y}); +} + +F2D_EXPORT void f2d_body_apply_force(int32_t index1, uint32_t world_and_generation, float fx, + float fy, float px, float py, int wake) { + b2Body_ApplyForce(f2d_body(index1, world_and_generation), (b2Vec2){fx, fy}, (b2Vec2){px, py}, + wake); +} + +F2D_EXPORT void f2d_body_apply_force_to_center(int32_t index1, uint32_t world_and_generation, + float fx, float fy, int wake) { + b2Body_ApplyForceToCenter(f2d_body(index1, world_and_generation), (b2Vec2){fx, fy}, wake); +} + +F2D_EXPORT void f2d_body_apply_torque(int32_t index1, uint32_t world_and_generation, float torque, + int wake) { + b2Body_ApplyTorque(f2d_body(index1, world_and_generation), torque, wake); +} + +F2D_EXPORT void f2d_body_apply_linear_impulse(int32_t index1, uint32_t world_and_generation, + float ix, float iy, float px, + float py, int wake) { + b2Body_ApplyLinearImpulse(f2d_body(index1, world_and_generation), (b2Vec2){ix, iy}, + (b2Vec2){px, py}, wake); +} + +F2D_EXPORT void f2d_body_apply_linear_impulse_to_center(int32_t index1, + uint32_t world_and_generation, float ix, + float iy, int wake) { + b2Body_ApplyLinearImpulseToCenter(f2d_body(index1, world_and_generation), (b2Vec2){ix, iy}, wake); +} + +F2D_EXPORT void f2d_body_apply_angular_impulse(int32_t index1, uint32_t world_and_generation, + float impulse, int wake) { + b2Body_ApplyAngularImpulse(f2d_body(index1, world_and_generation), impulse, wake); +} + +F2D_EXPORT void f2d_body_get_local_center(int32_t index1, uint32_t world_and_generation, + float* out) { + b2Vec2 center = b2Body_GetLocalCenterOfMass(f2d_body(index1, world_and_generation)); + out[0] = center.x; + out[1] = center.y; +} + +F2D_EXPORT void f2d_body_get_world_center(int32_t index1, uint32_t world_and_generation, + float* out) { + b2Vec2 center = b2Body_GetWorldCenterOfMass(f2d_body(index1, world_and_generation)); + out[0] = center.x; + out[1] = center.y; +} + +F2D_EXPORT void f2d_body_set_mass_data(int32_t index1, uint32_t world_and_generation, float mass, + float rotational_inertia, float cx, + float cy) { + b2MassData mass_data = {mass, {cx, cy}, rotational_inertia}; + b2Body_SetMassData(f2d_body(index1, world_and_generation), mass_data); +} + +F2D_EXPORT const char* f2d_body_get_name(int32_t index1, uint32_t world_and_generation) { + return b2Body_GetName(f2d_body(index1, world_and_generation)); +} + +F2D_EXPORT void f2d_body_set_name(int32_t index1, uint32_t world_and_generation, const char* name) { + b2Body_SetName(f2d_body(index1, world_and_generation), name); +} + +F2D_EXPORT void f2d_body_get_world_point(int32_t index1, uint32_t world_and_generation, float x, + float y, float* out) { + b2Vec2 point = b2Body_GetWorldPoint(f2d_body(index1, world_and_generation), (b2Vec2){x, y}); + out[0] = point.x; + out[1] = point.y; +} + +F2D_EXPORT void f2d_body_get_local_point(int32_t index1, uint32_t world_and_generation, float x, + float y, float* out) { + b2Vec2 point = b2Body_GetLocalPoint(f2d_body(index1, world_and_generation), (b2Vec2){x, y}); + out[0] = point.x; + out[1] = point.y; +} + +F2D_EXPORT int f2d_body_get_shape_count(int32_t index1, uint32_t world_and_generation) { + return b2Body_GetShapeCount(f2d_body(index1, world_and_generation)); +} + +F2D_EXPORT int f2d_body_get_shapes(int32_t index1, uint32_t world_and_generation, int32_t* out, + int capacity) { + // Heap allocated: capacity is caller controlled and the wasm stack is + // only 64 KB. + b2ShapeId* shapes = malloc(capacity * sizeof(b2ShapeId)); + int count = b2Body_GetShapes(f2d_body(index1, world_and_generation), shapes, capacity); + for (int i = 0; i < count; ++i) { + out[2 * i] = shapes[i].index1; + out[2 * i + 1] = (int32_t)f2d_wg_shape(shapes[i]); + } + free(shapes); + return count; +} + +F2D_EXPORT int f2d_body_get_joint_count(int32_t index1, uint32_t world_and_generation) { + return b2Body_GetJointCount(f2d_body(index1, world_and_generation)); +} + +F2D_EXPORT int f2d_body_get_joints(int32_t index1, uint32_t world_and_generation, int32_t* out, + int capacity) { + b2JointId* joints = malloc(capacity * sizeof(b2JointId)); + int count = b2Body_GetJoints(f2d_body(index1, world_and_generation), joints, capacity); + for (int i = 0; i < count; ++i) { + out[2 * i] = joints[i].index1; + out[2 * i + 1] = (int32_t)f2d_wg_joint(joints[i]); + } + free(joints); + return count; +} + +// Scalar body accessors, generated by macro. +#define F2D_BODY_GET_FLOAT(name, fn) \ + F2D_EXPORT float name(int32_t index1, uint32_t world_and_generation) { return fn(f2d_body(index1, world_and_generation)); } +#define F2D_BODY_SET_FLOAT(name, fn) \ + F2D_EXPORT void name(int32_t index1, uint32_t world_and_generation, float v) { fn(f2d_body(index1, world_and_generation), v); } +#define F2D_BODY_GET_INT(name, fn) \ + F2D_EXPORT int name(int32_t index1, uint32_t world_and_generation) { return (int)fn(f2d_body(index1, world_and_generation)); } +#define F2D_BODY_SET_BOOL(name, fn) \ + F2D_EXPORT void name(int32_t index1, uint32_t world_and_generation, int v) { fn(f2d_body(index1, world_and_generation), v); } +#define F2D_BODY_CALL(name, fn) \ + F2D_EXPORT void name(int32_t index1, uint32_t world_and_generation) { fn(f2d_body(index1, world_and_generation)); } + +F2D_BODY_GET_FLOAT(f2d_body_get_angular_velocity, b2Body_GetAngularVelocity) +F2D_BODY_SET_FLOAT(f2d_body_set_angular_velocity, b2Body_SetAngularVelocity) +F2D_BODY_GET_FLOAT(f2d_body_get_mass, b2Body_GetMass) +F2D_BODY_GET_FLOAT(f2d_body_get_rotational_inertia, b2Body_GetRotationalInertia) +F2D_BODY_CALL(f2d_body_apply_mass_from_shapes, b2Body_ApplyMassFromShapes) +F2D_BODY_GET_INT(f2d_body_get_type, b2Body_GetType) +F2D_EXPORT void f2d_body_set_type(int32_t index1, uint32_t world_and_generation, int type) { + b2Body_SetType(f2d_body(index1, world_and_generation), (b2BodyType)type); +} +F2D_BODY_GET_INT(f2d_body_is_awake, b2Body_IsAwake) +F2D_BODY_SET_BOOL(f2d_body_set_awake, b2Body_SetAwake) +F2D_BODY_GET_INT(f2d_body_is_sleep_enabled, b2Body_IsSleepEnabled) +F2D_BODY_SET_BOOL(f2d_body_enable_sleep, b2Body_EnableSleep) +F2D_BODY_GET_FLOAT(f2d_body_get_sleep_threshold, b2Body_GetSleepThreshold) +F2D_BODY_SET_FLOAT(f2d_body_set_sleep_threshold, b2Body_SetSleepThreshold) +F2D_BODY_GET_INT(f2d_body_is_enabled, b2Body_IsEnabled) +F2D_BODY_CALL(f2d_body_disable, b2Body_Disable) +F2D_BODY_CALL(f2d_body_enable, b2Body_Enable) +F2D_BODY_GET_INT(f2d_body_is_fixed_rotation, b2Body_IsFixedRotation) +F2D_BODY_SET_BOOL(f2d_body_set_fixed_rotation, b2Body_SetFixedRotation) +F2D_BODY_GET_INT(f2d_body_is_bullet, b2Body_IsBullet) +F2D_BODY_SET_BOOL(f2d_body_set_bullet, b2Body_SetBullet) +F2D_BODY_GET_FLOAT(f2d_body_get_gravity_scale, b2Body_GetGravityScale) +F2D_BODY_SET_FLOAT(f2d_body_set_gravity_scale, b2Body_SetGravityScale) +F2D_BODY_GET_FLOAT(f2d_body_get_linear_damping, b2Body_GetLinearDamping) +F2D_BODY_SET_FLOAT(f2d_body_set_linear_damping, b2Body_SetLinearDamping) +F2D_BODY_GET_FLOAT(f2d_body_get_angular_damping, b2Body_GetAngularDamping) +F2D_BODY_SET_FLOAT(f2d_body_set_angular_damping, b2Body_SetAngularDamping) + +// Shapes. + +typedef struct f2dShapeDef { + float friction; + float restitution; + float rolling_resistance; + float tangent_speed; + int32_t user_material_id; + uint32_t custom_color; + float density; + uint32_t category_lo; + uint32_t category_hi; + uint32_t mask_lo; + uint32_t mask_hi; + int32_t group_index; + int32_t is_sensor; + int32_t enable_sensor_events; + int32_t enable_contact_events; + int32_t enable_hit_events; + int32_t enable_pre_solve_events; + int32_t invoke_contact_creation; + int32_t update_body_mass; +} f2dShapeDef; + +static b2ShapeDef f2d_shape_def(const f2dShapeDef* in) { + b2ShapeDef def = b2DefaultShapeDef(); + def.material.friction = in->friction; + def.material.restitution = in->restitution; + def.material.rollingResistance = in->rolling_resistance; + def.material.tangentSpeed = in->tangent_speed; + def.material.userMaterialId = in->user_material_id; + def.material.customColor = in->custom_color; + def.density = in->density; + def.filter = f2d_filter(in->category_lo, in->category_hi, in->mask_lo, + in->mask_hi, in->group_index); + def.isSensor = in->is_sensor; + def.enableSensorEvents = in->enable_sensor_events; + def.enableContactEvents = in->enable_contact_events; + def.enableHitEvents = in->enable_hit_events; + def.enablePreSolveEvents = in->enable_pre_solve_events; + def.invokeContactCreation = in->invoke_contact_creation; + def.updateBodyMass = in->update_body_mass; + return def; +} + +static void f2d_out_shape(b2ShapeId id, int32_t* out) { + out[0] = id.index1; + out[1] = (int32_t)f2d_wg_shape(id); +} + +F2D_EXPORT void f2d_create_circle_shape(int32_t index1, uint32_t world_and_generation, + const f2dShapeDef* def, float cx, + float cy, float radius, int32_t* out) { + b2ShapeDef shape_def = f2d_shape_def(def); + b2Circle circle = {{cx, cy}, radius}; + f2d_out_shape(b2CreateCircleShape(f2d_body(index1, world_and_generation), &shape_def, &circle), + out); +} + +F2D_EXPORT void f2d_create_capsule_shape(int32_t index1, uint32_t world_and_generation, + const f2dShapeDef* def, float c1x, + float c1y, float c2x, float c2y, + float radius, int32_t* out) { + b2ShapeDef shape_def = f2d_shape_def(def); + b2Capsule capsule = {{c1x, c1y}, {c2x, c2y}, radius}; + f2d_out_shape(b2CreateCapsuleShape(f2d_body(index1, world_and_generation), &shape_def, &capsule), + out); +} + +F2D_EXPORT void f2d_create_segment_shape(int32_t index1, uint32_t world_and_generation, + const f2dShapeDef* def, float p1x, + float p1y, float p2x, float p2y, + int32_t* out) { + b2ShapeDef shape_def = f2d_shape_def(def); + b2Segment segment = {{p1x, p1y}, {p2x, p2y}}; + f2d_out_shape(b2CreateSegmentShape(f2d_body(index1, world_and_generation), &shape_def, &segment), + out); +} + +F2D_EXPORT void f2d_create_box_shape(int32_t index1, uint32_t world_and_generation, + const f2dShapeDef* def, float half_width, + float half_height, float cx, float cy, + float qc, float qs, float radius, + int32_t* out) { + b2ShapeDef shape_def = f2d_shape_def(def); + b2Polygon polygon = b2MakeOffsetRoundedBox(half_width, half_height, + (b2Vec2){cx, cy}, (b2Rot){qc, qs}, + radius); + f2d_out_shape(b2CreatePolygonShape(f2d_body(index1, world_and_generation), &shape_def, &polygon), + out); +} + +// Returns false when the hull is degenerate. +F2D_EXPORT int f2d_create_polygon_shape(int32_t index1, uint32_t world_and_generation, + const f2dShapeDef* def, + const float* points, int count, + float radius, int32_t* out) { + b2Hull hull = b2ComputeHull((const b2Vec2*)points, count); + if (hull.count == 0) { + return 0; + } + b2ShapeDef shape_def = f2d_shape_def(def); + b2Polygon polygon = b2MakePolygon(&hull, radius); + f2d_out_shape(b2CreatePolygonShape(f2d_body(index1, world_and_generation), &shape_def, &polygon), + out); + return 1; +} + +F2D_EXPORT void f2d_destroy_shape(int32_t index1, uint32_t world_and_generation, + int update_body_mass) { + b2DestroyShape(f2d_shape(index1, world_and_generation), update_body_mass); +} + +F2D_EXPORT void f2d_shape_get_body(int32_t index1, uint32_t world_and_generation, int32_t* out) { + b2BodyId id = b2Shape_GetBody(f2d_shape(index1, world_and_generation)); + out[0] = id.index1; + out[1] = (int32_t)f2d_wg_body(id); +} + +F2D_EXPORT void f2d_shape_set_density(int32_t index1, uint32_t world_and_generation, float density, + int update_body_mass) { + b2Shape_SetDensity(f2d_shape(index1, world_and_generation), density, update_body_mass); +} + +F2D_EXPORT void f2d_shape_get_filter(int32_t index1, uint32_t world_and_generation, int32_t* out) { + b2Filter filter = b2Shape_GetFilter(f2d_shape(index1, world_and_generation)); + out[0] = (int32_t)(filter.categoryBits & 0xFFFFFFFF); + out[1] = (int32_t)(filter.categoryBits >> 32); + out[2] = (int32_t)(filter.maskBits & 0xFFFFFFFF); + out[3] = (int32_t)(filter.maskBits >> 32); + out[4] = filter.groupIndex; +} + +F2D_EXPORT void f2d_shape_set_filter(int32_t index1, uint32_t world_and_generation, + uint32_t category_lo, uint32_t category_hi, + uint32_t mask_lo, uint32_t mask_hi, + int32_t group_index) { + b2Shape_SetFilter(f2d_shape(index1, world_and_generation), f2d_filter(category_lo, category_hi, + mask_lo, mask_hi, + group_index)); +} + +F2D_EXPORT int f2d_shape_test_point(int32_t index1, uint32_t world_and_generation, float x, + float y) { + return b2Shape_TestPoint(f2d_shape(index1, world_and_generation), (b2Vec2){x, y}); +} + +F2D_EXPORT void f2d_shape_get_circle(int32_t index1, uint32_t world_and_generation, float* out) { + b2Circle circle = b2Shape_GetCircle(f2d_shape(index1, world_and_generation)); + out[0] = circle.center.x; + out[1] = circle.center.y; + out[2] = circle.radius; +} + +F2D_EXPORT void f2d_shape_get_capsule(int32_t index1, uint32_t world_and_generation, float* out) { + b2Capsule capsule = b2Shape_GetCapsule(f2d_shape(index1, world_and_generation)); + out[0] = capsule.center1.x; + out[1] = capsule.center1.y; + out[2] = capsule.center2.x; + out[3] = capsule.center2.y; + out[4] = capsule.radius; +} + +F2D_EXPORT void f2d_shape_get_segment(int32_t index1, uint32_t world_and_generation, float* out) { + b2Segment segment = b2Shape_GetSegment(f2d_shape(index1, world_and_generation)); + out[0] = segment.point1.x; + out[1] = segment.point1.y; + out[2] = segment.point2.x; + out[3] = segment.point2.y; +} + +F2D_EXPORT void f2d_shape_get_chain_segment(int32_t index1, uint32_t world_and_generation, + float* out) { + b2ChainSegment chain_segment = b2Shape_GetChainSegment(f2d_shape(index1, world_and_generation)); + out[0] = chain_segment.segment.point1.x; + out[1] = chain_segment.segment.point1.y; + out[2] = chain_segment.segment.point2.x; + out[3] = chain_segment.segment.point2.y; +} + +// Writes the radius followed by the vertex pairs; returns the vertex count. +F2D_EXPORT int f2d_shape_get_polygon(int32_t index1, uint32_t world_and_generation, float* out) { + b2Polygon polygon = b2Shape_GetPolygon(f2d_shape(index1, world_and_generation)); + out[0] = polygon.radius; + for (int i = 0; i < polygon.count; ++i) { + out[1 + 2 * i] = polygon.vertices[i].x; + out[2 + 2 * i] = polygon.vertices[i].y; + } + return polygon.count; +} + +F2D_EXPORT void f2d_shape_get_aabb(int32_t index1, uint32_t world_and_generation, float* out) { + b2AABB aabb = b2Shape_GetAABB(f2d_shape(index1, world_and_generation)); + out[0] = aabb.lowerBound.x; + out[1] = aabb.lowerBound.y; + out[2] = aabb.upperBound.x; + out[3] = aabb.upperBound.y; +} + +#define F2D_SHAPE_GET_FLOAT(name, fn) \ + F2D_EXPORT float name(int32_t index1, uint32_t world_and_generation) { return fn(f2d_shape(index1, world_and_generation)); } +#define F2D_SHAPE_SET_FLOAT(name, fn) \ + F2D_EXPORT void name(int32_t index1, uint32_t world_and_generation, float v) { fn(f2d_shape(index1, world_and_generation), v); } +#define F2D_SHAPE_GET_INT(name, fn) \ + F2D_EXPORT int name(int32_t index1, uint32_t world_and_generation) { return (int)fn(f2d_shape(index1, world_and_generation)); } +#define F2D_SHAPE_SET_BOOL(name, fn) \ + F2D_EXPORT void name(int32_t index1, uint32_t world_and_generation, int v) { fn(f2d_shape(index1, world_and_generation), v); } + +F2D_SHAPE_GET_INT(f2d_shape_is_valid, b2Shape_IsValid) +F2D_SHAPE_GET_INT(f2d_shape_get_type, b2Shape_GetType) +F2D_SHAPE_GET_INT(f2d_shape_is_sensor, b2Shape_IsSensor) +F2D_SHAPE_GET_FLOAT(f2d_shape_get_density, b2Shape_GetDensity) +F2D_SHAPE_GET_FLOAT(f2d_shape_get_friction, b2Shape_GetFriction) +F2D_SHAPE_SET_FLOAT(f2d_shape_set_friction, b2Shape_SetFriction) +F2D_SHAPE_GET_FLOAT(f2d_shape_get_restitution, b2Shape_GetRestitution) +F2D_SHAPE_SET_FLOAT(f2d_shape_set_restitution, b2Shape_SetRestitution) +F2D_SHAPE_GET_INT(f2d_shape_are_sensor_events_enabled, + b2Shape_AreSensorEventsEnabled) +F2D_SHAPE_SET_BOOL(f2d_shape_enable_sensor_events, b2Shape_EnableSensorEvents) +F2D_SHAPE_GET_INT(f2d_shape_are_contact_events_enabled, + b2Shape_AreContactEventsEnabled) +F2D_SHAPE_SET_BOOL(f2d_shape_enable_contact_events, b2Shape_EnableContactEvents) +F2D_SHAPE_GET_INT(f2d_shape_are_hit_events_enabled, b2Shape_AreHitEventsEnabled) +F2D_SHAPE_SET_BOOL(f2d_shape_enable_hit_events, b2Shape_EnableHitEvents) +F2D_SHAPE_GET_INT(f2d_shape_are_pre_solve_events_enabled, + b2Shape_ArePreSolveEventsEnabled) +F2D_SHAPE_SET_BOOL(f2d_shape_enable_pre_solve_events, + b2Shape_EnablePreSolveEvents) + +// Chains. + +F2D_EXPORT void f2d_create_chain(int32_t index1, uint32_t world_and_generation, const float* points, + int point_count, const float* materials, + int material_count, uint32_t category_lo, + uint32_t category_hi, uint32_t mask_lo, + uint32_t mask_hi, int32_t group_index, + int is_loop, int enable_sensor_events, + int32_t* out) { + b2SurfaceMaterial* surface_materials = + malloc(material_count * sizeof(b2SurfaceMaterial)); + for (int i = 0; i < material_count; ++i) { + const float* m = materials + i * 6; + surface_materials[i] = (b2SurfaceMaterial){ + m[0], m[1], m[2], m[3], (int)m[4], (uint32_t)m[5]}; + } + b2ChainDef def = b2DefaultChainDef(); + def.points = (const b2Vec2*)points; + def.count = point_count; + def.materials = surface_materials; + def.materialCount = material_count; + def.filter = f2d_filter(category_lo, category_hi, mask_lo, mask_hi, + group_index); + def.isLoop = is_loop; + def.enableSensorEvents = enable_sensor_events; + b2ChainId id = b2CreateChain(f2d_body(index1, world_and_generation), &def); + free(surface_materials); + out[0] = id.index1; + out[1] = (int32_t)f2d_wg_chain(id); +} + +F2D_EXPORT void f2d_destroy_chain(int32_t index1, uint32_t world_and_generation) { + b2DestroyChain(f2d_chain(index1, world_and_generation)); +} + +F2D_EXPORT int f2d_chain_is_valid(int32_t index1, uint32_t world_and_generation) { + return b2Chain_IsValid(f2d_chain(index1, world_and_generation)); +} + +F2D_EXPORT void f2d_chain_set_friction(int32_t index1, uint32_t world_and_generation, float v) { + b2Chain_SetFriction(f2d_chain(index1, world_and_generation), v); +} + +F2D_EXPORT float f2d_chain_get_friction(int32_t index1, uint32_t world_and_generation) { + return b2Chain_GetFriction(f2d_chain(index1, world_and_generation)); +} + +F2D_EXPORT void f2d_chain_set_restitution(int32_t index1, uint32_t world_and_generation, float v) { + b2Chain_SetRestitution(f2d_chain(index1, world_and_generation), v); +} + +F2D_EXPORT float f2d_chain_get_restitution(int32_t index1, uint32_t world_and_generation) { + return b2Chain_GetRestitution(f2d_chain(index1, world_and_generation)); +} + +F2D_EXPORT int f2d_chain_get_segment_count(int32_t index1, uint32_t world_and_generation) { + return b2Chain_GetSegmentCount(f2d_chain(index1, world_and_generation)); +} + +F2D_EXPORT int f2d_chain_get_segments(int32_t index1, uint32_t world_and_generation, int32_t* out, + int capacity) { + b2ShapeId* segments = malloc(capacity * sizeof(b2ShapeId)); + int count = b2Chain_GetSegments(f2d_chain(index1, world_and_generation), segments, capacity); + for (int i = 0; i < count; ++i) { + out[2 * i] = segments[i].index1; + out[2 * i + 1] = (int32_t)f2d_wg_shape(segments[i]); + } + free(segments); + return count; +} + +// Joints. + +static void f2d_out_joint(b2JointId id, int32_t* out) { + out[0] = id.index1; + out[1] = (int32_t)f2d_wg_joint(id); +} + +F2D_EXPORT void f2d_create_distance_joint( + uint32_t w, int32_t a_index1, uint32_t a_world_and_generation, int32_t b_index1, uint32_t b_world_and_generation, + float anchor_ax, float anchor_ay, float anchor_bx, float anchor_by, + float length, int enable_spring, float hertz, float damping_ratio, + int enable_limit, float min_length, float max_length, int enable_motor, + float max_motor_force, float motor_speed, int collide_connected, + int32_t* out) { + b2DistanceJointDef def = b2DefaultDistanceJointDef(); + def.bodyIdA = f2d_body(a_index1, a_world_and_generation); + def.bodyIdB = f2d_body(b_index1, b_world_and_generation); + def.localAnchorA = (b2Vec2){anchor_ax, anchor_ay}; + def.localAnchorB = (b2Vec2){anchor_bx, anchor_by}; + def.length = length; + def.enableSpring = enable_spring; + def.hertz = hertz; + def.dampingRatio = damping_ratio; + def.enableLimit = enable_limit; + def.minLength = min_length; + def.maxLength = max_length; + def.enableMotor = enable_motor; + def.maxMotorForce = max_motor_force; + def.motorSpeed = motor_speed; + def.collideConnected = collide_connected; + f2d_out_joint(b2CreateDistanceJoint(f2d_world(w), &def), out); +} + +F2D_EXPORT void f2d_create_filter_joint(uint32_t w, int32_t a_index1, uint32_t a_world_and_generation, + int32_t b_index1, uint32_t b_world_and_generation, + int32_t* out) { + b2FilterJointDef def = b2DefaultFilterJointDef(); + def.bodyIdA = f2d_body(a_index1, a_world_and_generation); + def.bodyIdB = f2d_body(b_index1, b_world_and_generation); + f2d_out_joint(b2CreateFilterJoint(f2d_world(w), &def), out); +} + +F2D_EXPORT void f2d_create_motor_joint(uint32_t w, int32_t a_index1, uint32_t a_world_and_generation, + int32_t b_index1, uint32_t b_world_and_generation, + float offset_x, float offset_y, + float angular_offset, float max_force, + float max_torque, + float correction_factor, + int collide_connected, int32_t* out) { + b2MotorJointDef def = b2DefaultMotorJointDef(); + def.bodyIdA = f2d_body(a_index1, a_world_and_generation); + def.bodyIdB = f2d_body(b_index1, b_world_and_generation); + def.linearOffset = (b2Vec2){offset_x, offset_y}; + def.angularOffset = angular_offset; + def.maxForce = max_force; + def.maxTorque = max_torque; + def.correctionFactor = correction_factor; + def.collideConnected = collide_connected; + f2d_out_joint(b2CreateMotorJoint(f2d_world(w), &def), out); +} + +F2D_EXPORT void f2d_create_mouse_joint(uint32_t w, int32_t a_index1, uint32_t a_world_and_generation, + int32_t b_index1, uint32_t b_world_and_generation, + float target_x, float target_y, + float hertz, float damping_ratio, + float max_force, int collide_connected, + int32_t* out) { + b2MouseJointDef def = b2DefaultMouseJointDef(); + def.bodyIdA = f2d_body(a_index1, a_world_and_generation); + def.bodyIdB = f2d_body(b_index1, b_world_and_generation); + def.target = (b2Vec2){target_x, target_y}; + def.hertz = hertz; + def.dampingRatio = damping_ratio; + def.maxForce = max_force; + def.collideConnected = collide_connected; + f2d_out_joint(b2CreateMouseJoint(f2d_world(w), &def), out); +} + +F2D_EXPORT void f2d_create_prismatic_joint( + uint32_t w, int32_t a_index1, uint32_t a_world_and_generation, int32_t b_index1, uint32_t b_world_and_generation, + float anchor_ax, float anchor_ay, float anchor_bx, float anchor_by, + float axis_x, float axis_y, float reference_angle, + float target_translation, int enable_spring, float hertz, + float damping_ratio, int enable_limit, float lower_translation, + float upper_translation, int enable_motor, float max_motor_force, + float motor_speed, int collide_connected, int32_t* out) { + b2PrismaticJointDef def = b2DefaultPrismaticJointDef(); + def.bodyIdA = f2d_body(a_index1, a_world_and_generation); + def.bodyIdB = f2d_body(b_index1, b_world_and_generation); + def.localAnchorA = (b2Vec2){anchor_ax, anchor_ay}; + def.localAnchorB = (b2Vec2){anchor_bx, anchor_by}; + def.localAxisA = (b2Vec2){axis_x, axis_y}; + def.referenceAngle = reference_angle; + def.targetTranslation = target_translation; + def.enableSpring = enable_spring; + def.hertz = hertz; + def.dampingRatio = damping_ratio; + def.enableLimit = enable_limit; + def.lowerTranslation = lower_translation; + def.upperTranslation = upper_translation; + def.enableMotor = enable_motor; + def.maxMotorForce = max_motor_force; + def.motorSpeed = motor_speed; + def.collideConnected = collide_connected; + f2d_out_joint(b2CreatePrismaticJoint(f2d_world(w), &def), out); +} + +F2D_EXPORT void f2d_create_revolute_joint( + uint32_t w, int32_t a_index1, uint32_t a_world_and_generation, int32_t b_index1, uint32_t b_world_and_generation, + float anchor_ax, float anchor_ay, float anchor_bx, float anchor_by, + float reference_angle, float target_angle, int enable_spring, float hertz, + float damping_ratio, int enable_limit, float lower_angle, + float upper_angle, int enable_motor, float max_motor_torque, + float motor_speed, float draw_size, int collide_connected, int32_t* out) { + b2RevoluteJointDef def = b2DefaultRevoluteJointDef(); + def.bodyIdA = f2d_body(a_index1, a_world_and_generation); + def.bodyIdB = f2d_body(b_index1, b_world_and_generation); + def.localAnchorA = (b2Vec2){anchor_ax, anchor_ay}; + def.localAnchorB = (b2Vec2){anchor_bx, anchor_by}; + def.referenceAngle = reference_angle; + def.targetAngle = target_angle; + def.enableSpring = enable_spring; + def.hertz = hertz; + def.dampingRatio = damping_ratio; + def.enableLimit = enable_limit; + def.lowerAngle = lower_angle; + def.upperAngle = upper_angle; + def.enableMotor = enable_motor; + def.maxMotorTorque = max_motor_torque; + def.motorSpeed = motor_speed; + def.drawSize = draw_size; + def.collideConnected = collide_connected; + f2d_out_joint(b2CreateRevoluteJoint(f2d_world(w), &def), out); +} + +F2D_EXPORT void f2d_create_weld_joint( + uint32_t w, int32_t a_index1, uint32_t a_world_and_generation, int32_t b_index1, uint32_t b_world_and_generation, + float anchor_ax, float anchor_ay, float anchor_bx, float anchor_by, + float reference_angle, float linear_hertz, float angular_hertz, + float linear_damping_ratio, float angular_damping_ratio, + int collide_connected, int32_t* out) { + b2WeldJointDef def = b2DefaultWeldJointDef(); + def.bodyIdA = f2d_body(a_index1, a_world_and_generation); + def.bodyIdB = f2d_body(b_index1, b_world_and_generation); + def.localAnchorA = (b2Vec2){anchor_ax, anchor_ay}; + def.localAnchorB = (b2Vec2){anchor_bx, anchor_by}; + def.referenceAngle = reference_angle; + def.linearHertz = linear_hertz; + def.angularHertz = angular_hertz; + def.linearDampingRatio = linear_damping_ratio; + def.angularDampingRatio = angular_damping_ratio; + def.collideConnected = collide_connected; + f2d_out_joint(b2CreateWeldJoint(f2d_world(w), &def), out); +} + +F2D_EXPORT void f2d_create_wheel_joint( + uint32_t w, int32_t a_index1, uint32_t a_world_and_generation, int32_t b_index1, uint32_t b_world_and_generation, + float anchor_ax, float anchor_ay, float anchor_bx, float anchor_by, + float axis_x, float axis_y, int enable_spring, float hertz, + float damping_ratio, int enable_limit, float lower_translation, + float upper_translation, int enable_motor, float max_motor_torque, + float motor_speed, int collide_connected, int32_t* out) { + b2WheelJointDef def = b2DefaultWheelJointDef(); + def.bodyIdA = f2d_body(a_index1, a_world_and_generation); + def.bodyIdB = f2d_body(b_index1, b_world_and_generation); + def.localAnchorA = (b2Vec2){anchor_ax, anchor_ay}; + def.localAnchorB = (b2Vec2){anchor_bx, anchor_by}; + def.localAxisA = (b2Vec2){axis_x, axis_y}; + def.enableSpring = enable_spring; + def.hertz = hertz; + def.dampingRatio = damping_ratio; + def.enableLimit = enable_limit; + def.lowerTranslation = lower_translation; + def.upperTranslation = upper_translation; + def.enableMotor = enable_motor; + def.maxMotorTorque = max_motor_torque; + def.motorSpeed = motor_speed; + def.collideConnected = collide_connected; + f2d_out_joint(b2CreateWheelJoint(f2d_world(w), &def), out); +} + +F2D_EXPORT void f2d_destroy_joint(int32_t index1, uint32_t world_and_generation) { + b2DestroyJoint(f2d_joint(index1, world_and_generation)); +} + +F2D_EXPORT int f2d_joint_is_valid(int32_t index1, uint32_t world_and_generation) { + return b2Joint_IsValid(f2d_joint(index1, world_and_generation)); +} + +F2D_EXPORT int f2d_joint_get_type(int32_t index1, uint32_t world_and_generation) { + return (int)b2Joint_GetType(f2d_joint(index1, world_and_generation)); +} + +F2D_EXPORT void f2d_joint_get_body_a(int32_t index1, uint32_t world_and_generation, int32_t* out) { + b2BodyId id = b2Joint_GetBodyA(f2d_joint(index1, world_and_generation)); + out[0] = id.index1; + out[1] = (int32_t)f2d_wg_body(id); +} + +F2D_EXPORT void f2d_joint_get_body_b(int32_t index1, uint32_t world_and_generation, int32_t* out) { + b2BodyId id = b2Joint_GetBodyB(f2d_joint(index1, world_and_generation)); + out[0] = id.index1; + out[1] = (int32_t)f2d_wg_body(id); +} + +F2D_EXPORT void f2d_joint_get_local_anchor_a(int32_t index1, uint32_t world_and_generation, + float* out) { + b2Vec2 anchor = b2Joint_GetLocalAnchorA(f2d_joint(index1, world_and_generation)); + out[0] = anchor.x; + out[1] = anchor.y; +} + +F2D_EXPORT void f2d_joint_get_local_anchor_b(int32_t index1, uint32_t world_and_generation, + float* out) { + b2Vec2 anchor = b2Joint_GetLocalAnchorB(f2d_joint(index1, world_and_generation)); + out[0] = anchor.x; + out[1] = anchor.y; +} + +F2D_EXPORT int f2d_joint_get_collide_connected(int32_t index1, uint32_t world_and_generation) { + return b2Joint_GetCollideConnected(f2d_joint(index1, world_and_generation)); +} + +F2D_EXPORT void f2d_joint_set_collide_connected(int32_t index1, uint32_t world_and_generation, + int v) { + b2Joint_SetCollideConnected(f2d_joint(index1, world_and_generation), v); +} + +F2D_EXPORT void f2d_joint_wake_bodies(int32_t index1, uint32_t world_and_generation) { + b2Joint_WakeBodies(f2d_joint(index1, world_and_generation)); +} + +F2D_EXPORT void f2d_joint_get_constraint_force(int32_t index1, uint32_t world_and_generation, + float* out) { + b2Vec2 force = b2Joint_GetConstraintForce(f2d_joint(index1, world_and_generation)); + out[0] = force.x; + out[1] = force.y; +} + +F2D_EXPORT float f2d_joint_get_constraint_torque(int32_t index1, uint32_t world_and_generation) { + return b2Joint_GetConstraintTorque(f2d_joint(index1, world_and_generation)); +} + +#define F2D_JOINT_GET_FLOAT(name, fn) \ + F2D_EXPORT float name(int32_t index1, uint32_t world_and_generation) { return fn(f2d_joint(index1, world_and_generation)); } +#define F2D_JOINT_SET_FLOAT(name, fn) \ + F2D_EXPORT void name(int32_t index1, uint32_t world_and_generation, float v) { fn(f2d_joint(index1, world_and_generation), v); } +#define F2D_JOINT_GET_INT(name, fn) \ + F2D_EXPORT int name(int32_t index1, uint32_t world_and_generation) { return (int)fn(f2d_joint(index1, world_and_generation)); } +#define F2D_JOINT_SET_BOOL(name, fn) \ + F2D_EXPORT void name(int32_t index1, uint32_t world_and_generation, int v) { fn(f2d_joint(index1, world_and_generation), v); } +#define F2D_JOINT_SET_RANGE(name, fn) \ + F2D_EXPORT void name(int32_t index1, uint32_t world_and_generation, float a, float b) { \ + fn(f2d_joint(index1, world_and_generation), a, b); \ + } + +F2D_JOINT_GET_FLOAT(f2d_distance_joint_get_length, b2DistanceJoint_GetLength) +F2D_JOINT_SET_FLOAT(f2d_distance_joint_set_length, b2DistanceJoint_SetLength) +F2D_JOINT_GET_FLOAT(f2d_distance_joint_get_current_length, + b2DistanceJoint_GetCurrentLength) +F2D_JOINT_GET_INT(f2d_distance_joint_is_spring_enabled, + b2DistanceJoint_IsSpringEnabled) +F2D_JOINT_SET_BOOL(f2d_distance_joint_enable_spring, + b2DistanceJoint_EnableSpring) +F2D_JOINT_GET_FLOAT(f2d_distance_joint_get_spring_hertz, + b2DistanceJoint_GetSpringHertz) +F2D_JOINT_SET_FLOAT(f2d_distance_joint_set_spring_hertz, + b2DistanceJoint_SetSpringHertz) +F2D_JOINT_GET_FLOAT(f2d_distance_joint_get_spring_damping_ratio, + b2DistanceJoint_GetSpringDampingRatio) +F2D_JOINT_SET_FLOAT(f2d_distance_joint_set_spring_damping_ratio, + b2DistanceJoint_SetSpringDampingRatio) +F2D_JOINT_GET_INT(f2d_distance_joint_is_limit_enabled, + b2DistanceJoint_IsLimitEnabled) +F2D_JOINT_SET_BOOL(f2d_distance_joint_enable_limit, + b2DistanceJoint_EnableLimit) +F2D_JOINT_GET_FLOAT(f2d_distance_joint_get_min_length, + b2DistanceJoint_GetMinLength) +F2D_JOINT_GET_FLOAT(f2d_distance_joint_get_max_length, + b2DistanceJoint_GetMaxLength) +F2D_JOINT_SET_RANGE(f2d_distance_joint_set_length_range, + b2DistanceJoint_SetLengthRange) +F2D_JOINT_GET_INT(f2d_distance_joint_is_motor_enabled, + b2DistanceJoint_IsMotorEnabled) +F2D_JOINT_SET_BOOL(f2d_distance_joint_enable_motor, + b2DistanceJoint_EnableMotor) +F2D_JOINT_GET_FLOAT(f2d_distance_joint_get_motor_speed, + b2DistanceJoint_GetMotorSpeed) +F2D_JOINT_SET_FLOAT(f2d_distance_joint_set_motor_speed, + b2DistanceJoint_SetMotorSpeed) +F2D_JOINT_GET_FLOAT(f2d_distance_joint_get_max_motor_force, + b2DistanceJoint_GetMaxMotorForce) +F2D_JOINT_SET_FLOAT(f2d_distance_joint_set_max_motor_force, + b2DistanceJoint_SetMaxMotorForce) +F2D_JOINT_GET_FLOAT(f2d_distance_joint_get_motor_force, + b2DistanceJoint_GetMotorForce) + +F2D_EXPORT void f2d_motor_joint_get_linear_offset(int32_t index1, uint32_t world_and_generation, + float* out) { + b2Vec2 offset = b2MotorJoint_GetLinearOffset(f2d_joint(index1, world_and_generation)); + out[0] = offset.x; + out[1] = offset.y; +} + +F2D_EXPORT void f2d_motor_joint_set_linear_offset(int32_t index1, uint32_t world_and_generation, + float x, float y) { + b2MotorJoint_SetLinearOffset(f2d_joint(index1, world_and_generation), (b2Vec2){x, y}); +} + +F2D_JOINT_GET_FLOAT(f2d_motor_joint_get_angular_offset, + b2MotorJoint_GetAngularOffset) +F2D_JOINT_SET_FLOAT(f2d_motor_joint_set_angular_offset, + b2MotorJoint_SetAngularOffset) +F2D_JOINT_GET_FLOAT(f2d_motor_joint_get_max_force, b2MotorJoint_GetMaxForce) +F2D_JOINT_SET_FLOAT(f2d_motor_joint_set_max_force, b2MotorJoint_SetMaxForce) +F2D_JOINT_GET_FLOAT(f2d_motor_joint_get_max_torque, b2MotorJoint_GetMaxTorque) +F2D_JOINT_SET_FLOAT(f2d_motor_joint_set_max_torque, b2MotorJoint_SetMaxTorque) +F2D_JOINT_GET_FLOAT(f2d_motor_joint_get_correction_factor, + b2MotorJoint_GetCorrectionFactor) +F2D_JOINT_SET_FLOAT(f2d_motor_joint_set_correction_factor, + b2MotorJoint_SetCorrectionFactor) + +F2D_EXPORT void f2d_mouse_joint_get_target(int32_t index1, uint32_t world_and_generation, + float* out) { + b2Vec2 target = b2MouseJoint_GetTarget(f2d_joint(index1, world_and_generation)); + out[0] = target.x; + out[1] = target.y; +} + +F2D_EXPORT void f2d_mouse_joint_set_target(int32_t index1, uint32_t world_and_generation, float x, + float y) { + b2MouseJoint_SetTarget(f2d_joint(index1, world_and_generation), (b2Vec2){x, y}); +} + +F2D_JOINT_GET_FLOAT(f2d_mouse_joint_get_spring_hertz, + b2MouseJoint_GetSpringHertz) +F2D_JOINT_SET_FLOAT(f2d_mouse_joint_set_spring_hertz, + b2MouseJoint_SetSpringHertz) +F2D_JOINT_GET_FLOAT(f2d_mouse_joint_get_spring_damping_ratio, + b2MouseJoint_GetSpringDampingRatio) +F2D_JOINT_SET_FLOAT(f2d_mouse_joint_set_spring_damping_ratio, + b2MouseJoint_SetSpringDampingRatio) +F2D_JOINT_GET_FLOAT(f2d_mouse_joint_get_max_force, b2MouseJoint_GetMaxForce) +F2D_JOINT_SET_FLOAT(f2d_mouse_joint_set_max_force, b2MouseJoint_SetMaxForce) + +F2D_JOINT_GET_INT(f2d_prismatic_joint_is_spring_enabled, + b2PrismaticJoint_IsSpringEnabled) +F2D_JOINT_SET_BOOL(f2d_prismatic_joint_enable_spring, + b2PrismaticJoint_EnableSpring) +F2D_JOINT_GET_FLOAT(f2d_prismatic_joint_get_spring_hertz, + b2PrismaticJoint_GetSpringHertz) +F2D_JOINT_SET_FLOAT(f2d_prismatic_joint_set_spring_hertz, + b2PrismaticJoint_SetSpringHertz) +F2D_JOINT_GET_FLOAT(f2d_prismatic_joint_get_spring_damping_ratio, + b2PrismaticJoint_GetSpringDampingRatio) +F2D_JOINT_SET_FLOAT(f2d_prismatic_joint_set_spring_damping_ratio, + b2PrismaticJoint_SetSpringDampingRatio) +F2D_JOINT_GET_FLOAT(f2d_prismatic_joint_get_target_translation, + b2PrismaticJoint_GetTargetTranslation) +F2D_JOINT_SET_FLOAT(f2d_prismatic_joint_set_target_translation, + b2PrismaticJoint_SetTargetTranslation) +F2D_JOINT_GET_INT(f2d_prismatic_joint_is_limit_enabled, + b2PrismaticJoint_IsLimitEnabled) +F2D_JOINT_SET_BOOL(f2d_prismatic_joint_enable_limit, + b2PrismaticJoint_EnableLimit) +F2D_JOINT_GET_FLOAT(f2d_prismatic_joint_get_lower_limit, + b2PrismaticJoint_GetLowerLimit) +F2D_JOINT_GET_FLOAT(f2d_prismatic_joint_get_upper_limit, + b2PrismaticJoint_GetUpperLimit) +F2D_JOINT_SET_RANGE(f2d_prismatic_joint_set_limits, b2PrismaticJoint_SetLimits) +F2D_JOINT_GET_INT(f2d_prismatic_joint_is_motor_enabled, + b2PrismaticJoint_IsMotorEnabled) +F2D_JOINT_SET_BOOL(f2d_prismatic_joint_enable_motor, + b2PrismaticJoint_EnableMotor) +F2D_JOINT_GET_FLOAT(f2d_prismatic_joint_get_motor_speed, + b2PrismaticJoint_GetMotorSpeed) +F2D_JOINT_SET_FLOAT(f2d_prismatic_joint_set_motor_speed, + b2PrismaticJoint_SetMotorSpeed) +F2D_JOINT_GET_FLOAT(f2d_prismatic_joint_get_max_motor_force, + b2PrismaticJoint_GetMaxMotorForce) +F2D_JOINT_SET_FLOAT(f2d_prismatic_joint_set_max_motor_force, + b2PrismaticJoint_SetMaxMotorForce) +F2D_JOINT_GET_FLOAT(f2d_prismatic_joint_get_motor_force, + b2PrismaticJoint_GetMotorForce) +F2D_JOINT_GET_FLOAT(f2d_prismatic_joint_get_translation, + b2PrismaticJoint_GetTranslation) +F2D_JOINT_GET_FLOAT(f2d_prismatic_joint_get_speed, b2PrismaticJoint_GetSpeed) + +F2D_JOINT_GET_INT(f2d_revolute_joint_is_spring_enabled, + b2RevoluteJoint_IsSpringEnabled) +F2D_JOINT_SET_BOOL(f2d_revolute_joint_enable_spring, + b2RevoluteJoint_EnableSpring) +F2D_JOINT_GET_FLOAT(f2d_revolute_joint_get_spring_hertz, + b2RevoluteJoint_GetSpringHertz) +F2D_JOINT_SET_FLOAT(f2d_revolute_joint_set_spring_hertz, + b2RevoluteJoint_SetSpringHertz) +F2D_JOINT_GET_FLOAT(f2d_revolute_joint_get_spring_damping_ratio, + b2RevoluteJoint_GetSpringDampingRatio) +F2D_JOINT_SET_FLOAT(f2d_revolute_joint_set_spring_damping_ratio, + b2RevoluteJoint_SetSpringDampingRatio) +F2D_JOINT_GET_FLOAT(f2d_revolute_joint_get_target_angle, + b2RevoluteJoint_GetTargetAngle) +F2D_JOINT_SET_FLOAT(f2d_revolute_joint_set_target_angle, + b2RevoluteJoint_SetTargetAngle) +F2D_JOINT_GET_FLOAT(f2d_revolute_joint_get_angle, b2RevoluteJoint_GetAngle) +F2D_JOINT_GET_INT(f2d_revolute_joint_is_limit_enabled, + b2RevoluteJoint_IsLimitEnabled) +F2D_JOINT_SET_BOOL(f2d_revolute_joint_enable_limit, + b2RevoluteJoint_EnableLimit) +F2D_JOINT_GET_FLOAT(f2d_revolute_joint_get_lower_limit, + b2RevoluteJoint_GetLowerLimit) +F2D_JOINT_GET_FLOAT(f2d_revolute_joint_get_upper_limit, + b2RevoluteJoint_GetUpperLimit) +F2D_JOINT_SET_RANGE(f2d_revolute_joint_set_limits, b2RevoluteJoint_SetLimits) +F2D_JOINT_GET_INT(f2d_revolute_joint_is_motor_enabled, + b2RevoluteJoint_IsMotorEnabled) +F2D_JOINT_SET_BOOL(f2d_revolute_joint_enable_motor, + b2RevoluteJoint_EnableMotor) +F2D_JOINT_GET_FLOAT(f2d_revolute_joint_get_motor_speed, + b2RevoluteJoint_GetMotorSpeed) +F2D_JOINT_SET_FLOAT(f2d_revolute_joint_set_motor_speed, + b2RevoluteJoint_SetMotorSpeed) +F2D_JOINT_GET_FLOAT(f2d_revolute_joint_get_max_motor_torque, + b2RevoluteJoint_GetMaxMotorTorque) +F2D_JOINT_SET_FLOAT(f2d_revolute_joint_set_max_motor_torque, + b2RevoluteJoint_SetMaxMotorTorque) +F2D_JOINT_GET_FLOAT(f2d_revolute_joint_get_motor_torque, + b2RevoluteJoint_GetMotorTorque) + +F2D_JOINT_GET_FLOAT(f2d_weld_joint_get_linear_hertz, b2WeldJoint_GetLinearHertz) +F2D_JOINT_SET_FLOAT(f2d_weld_joint_set_linear_hertz, b2WeldJoint_SetLinearHertz) +F2D_JOINT_GET_FLOAT(f2d_weld_joint_get_angular_hertz, + b2WeldJoint_GetAngularHertz) +F2D_JOINT_SET_FLOAT(f2d_weld_joint_set_angular_hertz, + b2WeldJoint_SetAngularHertz) +F2D_JOINT_GET_FLOAT(f2d_weld_joint_get_linear_damping_ratio, + b2WeldJoint_GetLinearDampingRatio) +F2D_JOINT_SET_FLOAT(f2d_weld_joint_set_linear_damping_ratio, + b2WeldJoint_SetLinearDampingRatio) +F2D_JOINT_GET_FLOAT(f2d_weld_joint_get_angular_damping_ratio, + b2WeldJoint_GetAngularDampingRatio) +F2D_JOINT_SET_FLOAT(f2d_weld_joint_set_angular_damping_ratio, + b2WeldJoint_SetAngularDampingRatio) + +F2D_JOINT_GET_INT(f2d_wheel_joint_is_spring_enabled, + b2WheelJoint_IsSpringEnabled) +F2D_JOINT_SET_BOOL(f2d_wheel_joint_enable_spring, b2WheelJoint_EnableSpring) +F2D_JOINT_GET_FLOAT(f2d_wheel_joint_get_spring_hertz, + b2WheelJoint_GetSpringHertz) +F2D_JOINT_SET_FLOAT(f2d_wheel_joint_set_spring_hertz, + b2WheelJoint_SetSpringHertz) +F2D_JOINT_GET_FLOAT(f2d_wheel_joint_get_spring_damping_ratio, + b2WheelJoint_GetSpringDampingRatio) +F2D_JOINT_SET_FLOAT(f2d_wheel_joint_set_spring_damping_ratio, + b2WheelJoint_SetSpringDampingRatio) +F2D_JOINT_GET_INT(f2d_wheel_joint_is_limit_enabled, + b2WheelJoint_IsLimitEnabled) +F2D_JOINT_SET_BOOL(f2d_wheel_joint_enable_limit, b2WheelJoint_EnableLimit) +F2D_JOINT_GET_FLOAT(f2d_wheel_joint_get_lower_limit, + b2WheelJoint_GetLowerLimit) +F2D_JOINT_GET_FLOAT(f2d_wheel_joint_get_upper_limit, + b2WheelJoint_GetUpperLimit) +F2D_JOINT_SET_RANGE(f2d_wheel_joint_set_limits, b2WheelJoint_SetLimits) +F2D_JOINT_GET_INT(f2d_wheel_joint_is_motor_enabled, + b2WheelJoint_IsMotorEnabled) +F2D_JOINT_SET_BOOL(f2d_wheel_joint_enable_motor, b2WheelJoint_EnableMotor) +F2D_JOINT_GET_FLOAT(f2d_wheel_joint_get_motor_speed, + b2WheelJoint_GetMotorSpeed) +F2D_JOINT_SET_FLOAT(f2d_wheel_joint_set_motor_speed, + b2WheelJoint_SetMotorSpeed) +F2D_JOINT_GET_FLOAT(f2d_wheel_joint_get_max_motor_torque, + b2WheelJoint_GetMaxMotorTorque) +F2D_JOINT_SET_FLOAT(f2d_wheel_joint_set_max_motor_torque, + b2WheelJoint_SetMaxMotorTorque) +F2D_JOINT_GET_FLOAT(f2d_wheel_joint_get_motor_torque, + b2WheelJoint_GetMotorTorque) + +// Events. Counts are queried first; the fill functions then write float64 +// records into a caller-provided buffer and return the record count. +// +// Strides (in float64 elements): +// - begin contact: 13 [aI1, aWg, bI1, bWg, nx, ny, pointCount, +// p0x, p0y, sep0, p1x, p1y, sep1] +// - end contact: 4 [aI1, aWg, bI1, bWg] +// - hit contact: 9 [aI1, aWg, bI1, bWg, px, py, nx, ny, approachSpeed] +// - sensor: 4 [sensorI1, sensorWg, visitorI1, visitorWg] +// - body move: 7 [index1, world_and_generation, x, y, qc, qs, fellAsleep] + +F2D_EXPORT void f2d_world_get_contact_event_counts(uint32_t w, int32_t* out) { + b2ContactEvents events = b2World_GetContactEvents(f2d_world(w)); + out[0] = events.beginCount; + out[1] = events.endCount; + out[2] = events.hitCount; +} + +F2D_EXPORT int f2d_world_get_contact_begin_events(uint32_t w, double* out) { + b2ContactEvents events = b2World_GetContactEvents(f2d_world(w)); + for (int i = 0; i < events.beginCount; ++i) { + const b2ContactBeginTouchEvent* event = events.beginEvents + i; + double* record = out + i * 13; + record[0] = event->shapeIdA.index1; + record[1] = f2d_wg_shape(event->shapeIdA); + record[2] = event->shapeIdB.index1; + record[3] = f2d_wg_shape(event->shapeIdB); + record[4] = event->manifold.normal.x; + record[5] = event->manifold.normal.y; + record[6] = event->manifold.pointCount; + for (int p = 0; p < 2; ++p) { + record[7 + 3 * p] = event->manifold.points[p].point.x; + record[8 + 3 * p] = event->manifold.points[p].point.y; + record[9 + 3 * p] = event->manifold.points[p].separation; + } + } + return events.beginCount; +} + +F2D_EXPORT int f2d_world_get_contact_end_events(uint32_t w, double* out) { + b2ContactEvents events = b2World_GetContactEvents(f2d_world(w)); + for (int i = 0; i < events.endCount; ++i) { + const b2ContactEndTouchEvent* event = events.endEvents + i; + double* record = out + i * 4; + record[0] = event->shapeIdA.index1; + record[1] = f2d_wg_shape(event->shapeIdA); + record[2] = event->shapeIdB.index1; + record[3] = f2d_wg_shape(event->shapeIdB); + } + return events.endCount; +} + +F2D_EXPORT int f2d_world_get_contact_hit_events(uint32_t w, double* out) { + b2ContactEvents events = b2World_GetContactEvents(f2d_world(w)); + for (int i = 0; i < events.hitCount; ++i) { + const b2ContactHitEvent* event = events.hitEvents + i; + double* record = out + i * 9; + record[0] = event->shapeIdA.index1; + record[1] = f2d_wg_shape(event->shapeIdA); + record[2] = event->shapeIdB.index1; + record[3] = f2d_wg_shape(event->shapeIdB); + record[4] = event->point.x; + record[5] = event->point.y; + record[6] = event->normal.x; + record[7] = event->normal.y; + record[8] = event->approachSpeed; + } + return events.hitCount; +} + +F2D_EXPORT void f2d_world_get_sensor_event_counts(uint32_t w, int32_t* out) { + b2SensorEvents events = b2World_GetSensorEvents(f2d_world(w)); + out[0] = events.beginCount; + out[1] = events.endCount; +} + +F2D_EXPORT int f2d_world_get_sensor_begin_events(uint32_t w, double* out) { + b2SensorEvents events = b2World_GetSensorEvents(f2d_world(w)); + for (int i = 0; i < events.beginCount; ++i) { + const b2SensorBeginTouchEvent* event = events.beginEvents + i; + double* record = out + i * 4; + record[0] = event->sensorShapeId.index1; + record[1] = f2d_wg_shape(event->sensorShapeId); + record[2] = event->visitorShapeId.index1; + record[3] = f2d_wg_shape(event->visitorShapeId); + } + return events.beginCount; +} + +F2D_EXPORT int f2d_world_get_sensor_end_events(uint32_t w, double* out) { + b2SensorEvents events = b2World_GetSensorEvents(f2d_world(w)); + for (int i = 0; i < events.endCount; ++i) { + const b2SensorEndTouchEvent* event = events.endEvents + i; + double* record = out + i * 4; + record[0] = event->sensorShapeId.index1; + record[1] = f2d_wg_shape(event->sensorShapeId); + record[2] = event->visitorShapeId.index1; + record[3] = f2d_wg_shape(event->visitorShapeId); + } + return events.endCount; +} + +F2D_EXPORT int f2d_world_get_body_event_count(uint32_t w) { + return b2World_GetBodyEvents(f2d_world(w)).moveCount; +} + +F2D_EXPORT int f2d_world_get_body_events(uint32_t w, double* out) { + b2BodyEvents events = b2World_GetBodyEvents(f2d_world(w)); + for (int i = 0; i < events.moveCount; ++i) { + const b2BodyMoveEvent* event = events.moveEvents + i; + double* record = out + i * 7; + record[0] = event->bodyId.index1; + record[1] = f2d_wg_body(event->bodyId); + record[2] = event->transform.p.x; + record[3] = event->transform.p.y; + record[4] = event->transform.q.c; + record[5] = event->transform.q.s; + record[6] = event->fellAsleep; + } + return events.moveCount; +} + +// Queries. + +// Returns whether something was hit; the hit is written as +// [index1, world_and_generation, px, py, nx, ny, fraction] float32s with ids stored via the +// int32 view of the same buffer slots 0 and 1. +F2D_EXPORT int f2d_world_cast_ray_closest(uint32_t w, float ox, float oy, + float tx, float ty, + uint32_t category_lo, + uint32_t category_hi, + uint32_t mask_lo, uint32_t mask_hi, + double* out) { + b2RayResult result = b2World_CastRayClosest( + f2d_world(w), (b2Vec2){ox, oy}, (b2Vec2){tx, ty}, + f2d_query_filter(category_lo, category_hi, mask_lo, mask_hi)); + if (!result.hit) { + return 0; + } + out[0] = result.shapeId.index1; + out[1] = f2d_wg_shape(result.shapeId); + out[2] = result.point.x; + out[3] = result.point.y; + out[4] = result.normal.x; + out[5] = result.normal.y; + out[6] = result.fraction; + return 1; +} + +static float f2d_cast_ray_trampoline(b2ShapeId shape, b2Vec2 point, + b2Vec2 normal, float fraction, + void* context) { + (void)context; + return f2d_host_cast_ray(shape.index1, f2d_wg_shape(shape), point.x, point.y, + normal.x, normal.y, fraction); +} + +F2D_EXPORT void f2d_world_cast_ray(uint32_t w, float ox, float oy, float tx, + float ty, uint32_t category_lo, + uint32_t category_hi, uint32_t mask_lo, + uint32_t mask_hi) { + b2World_CastRay(f2d_world(w), (b2Vec2){ox, oy}, (b2Vec2){tx, ty}, + f2d_query_filter(category_lo, category_hi, mask_lo, mask_hi), + f2d_cast_ray_trampoline, NULL); +} + +static bool f2d_overlap_trampoline(b2ShapeId shape, void* context) { + (void)context; + return f2d_host_overlap(shape.index1, f2d_wg_shape(shape)); +} + +F2D_EXPORT void f2d_world_overlap_aabb(uint32_t w, float lx, float ly, + float ux, float uy, + uint32_t category_lo, + uint32_t category_hi, uint32_t mask_lo, + uint32_t mask_hi) { + b2AABB aabb = {{lx, ly}, {ux, uy}}; + b2World_OverlapAABB( + f2d_world(w), aabb, + f2d_query_filter(category_lo, category_hi, mask_lo, mask_hi), + f2d_overlap_trampoline, NULL); +} + +// Simulation callbacks. + +static bool f2d_custom_filter_trampoline(b2ShapeId a, b2ShapeId b, + void* context) { + (void)context; + return f2d_host_custom_filter(a.index1, f2d_wg_shape(a), b.index1, + f2d_wg_shape(b)); +} + +F2D_EXPORT void f2d_world_set_custom_filter(uint32_t w, int enabled) { + b2World_SetCustomFilterCallback( + f2d_world(w), enabled ? f2d_custom_filter_trampoline : NULL, NULL); +} + +static bool f2d_pre_solve_trampoline(b2ShapeId a, b2ShapeId b, + b2Manifold* manifold, void* context) { + (void)context; + return f2d_host_pre_solve(a.index1, f2d_wg_shape(a), b.index1, + f2d_wg_shape(b), manifold->normal.x, + manifold->normal.y); +} + +F2D_EXPORT void f2d_world_set_pre_solve(uint32_t w, int enabled) { + b2World_SetPreSolveCallback(f2d_world(w), + enabled ? f2d_pre_solve_trampoline : NULL, NULL); +} + +// Debug drawing. + +static void f2d_draw_polygon(const b2Vec2* vertices, int count, + b2HexColor color, void* context) { + (void)context; + f2d_host_draw_polygon((const float*)vertices, count, (uint32_t)color); +} + +static void f2d_draw_solid_polygon(b2Transform transform, + const b2Vec2* vertices, int count, + float radius, b2HexColor color, + void* context) { + (void)context; + f2d_host_draw_solid_polygon(transform.p.x, transform.p.y, transform.q.c, + transform.q.s, (const float*)vertices, count, + radius, (uint32_t)color); +} + +static void f2d_draw_circle(b2Vec2 center, float radius, b2HexColor color, + void* context) { + (void)context; + f2d_host_draw_circle(center.x, center.y, radius, (uint32_t)color); +} + +static void f2d_draw_solid_circle(b2Transform transform, float radius, + b2HexColor color, void* context) { + (void)context; + f2d_host_draw_solid_circle(transform.p.x, transform.p.y, transform.q.c, + transform.q.s, radius, (uint32_t)color); +} + +static void f2d_draw_solid_capsule(b2Vec2 p1, b2Vec2 p2, float radius, + b2HexColor color, void* context) { + (void)context; + f2d_host_draw_solid_capsule(p1.x, p1.y, p2.x, p2.y, radius, + (uint32_t)color); +} + +static void f2d_draw_segment(b2Vec2 p1, b2Vec2 p2, b2HexColor color, + void* context) { + (void)context; + f2d_host_draw_segment(p1.x, p1.y, p2.x, p2.y, (uint32_t)color); +} + +static void f2d_draw_transform(b2Transform transform, void* context) { + (void)context; + f2d_host_draw_transform(transform.p.x, transform.p.y, transform.q.c, + transform.q.s); +} + +static void f2d_draw_point(b2Vec2 point, float size, b2HexColor color, + void* context) { + (void)context; + f2d_host_draw_point(point.x, point.y, size, (uint32_t)color); +} + +static void f2d_draw_string(b2Vec2 point, const char* text, b2HexColor color, + void* context) { + (void)context; + f2d_host_draw_string(point.x, point.y, text, (uint32_t)color); +} + +F2D_EXPORT void f2d_world_draw(uint32_t w, int use_bounds, float bounds_lx, + float bounds_ly, float bounds_ux, + float bounds_uy, int draw_shapes, + int draw_joints, int draw_joint_extras, + int draw_bounds, int draw_mass, + int draw_body_names, int draw_contacts, + int draw_graph_colors, + int draw_contact_normals, + int draw_contact_impulses, + int draw_contact_features, + int draw_friction_impulses, int draw_islands) { + b2DebugDraw draw = b2DefaultDebugDraw(); + draw.DrawPolygonFcn = f2d_draw_polygon; + draw.DrawSolidPolygonFcn = f2d_draw_solid_polygon; + draw.DrawCircleFcn = f2d_draw_circle; + draw.DrawSolidCircleFcn = f2d_draw_solid_circle; + draw.DrawSolidCapsuleFcn = f2d_draw_solid_capsule; + draw.DrawSegmentFcn = f2d_draw_segment; + draw.DrawTransformFcn = f2d_draw_transform; + draw.DrawPointFcn = f2d_draw_point; + draw.DrawStringFcn = f2d_draw_string; + draw.useDrawingBounds = use_bounds; + draw.drawingBounds = (b2AABB){{bounds_lx, bounds_ly}, {bounds_ux, bounds_uy}}; + draw.drawShapes = draw_shapes; + draw.drawJoints = draw_joints; + draw.drawJointExtras = draw_joint_extras; + draw.drawBounds = draw_bounds; + draw.drawMass = draw_mass; + draw.drawBodyNames = draw_body_names; + draw.drawContacts = draw_contacts; + draw.drawGraphColors = draw_graph_colors; + draw.drawContactNormals = draw_contact_normals; + draw.drawContactImpulses = draw_contact_impulses; + draw.drawContactFeatures = draw_contact_features; + draw.drawFrictionImpulses = draw_friction_impulses; + draw.drawIslands = draw_islands; + b2World_Draw(f2d_world(w), &draw); +} diff --git a/packages/forge2d/pubspec.yaml b/packages/forge2d/pubspec.yaml index 866b42d3..897973e5 100644 --- a/packages/forge2d/pubspec.yaml +++ b/packages/forge2d/pubspec.yaml @@ -10,13 +10,19 @@ environment: dependencies: code_assets: ^1.2.1 ffi: ^2.1.4 - hooks: ^2.1.0 + hooks: ^2.0.0 logging: ^1.3.0 meta: ^1.16.0 - native_toolchain_c: ^0.19.3 + native_toolchain_c: ^0.19.0 vector_math: ^2.1.4 dev_dependencies: dartdoc: ^8.3.4 flame_lint: ^1.4.1 test: ^1.26.3 + +# Bundles the WebAssembly module as an asset in Flutter apps, so Flutter +# web needs no setup step. Non-Flutter tooling ignores this section. +flutter: + assets: + - lib/src/backend/wasm/box2d.wasm diff --git a/packages/forge2d/test/api/accessors_test.dart b/packages/forge2d/test/api/accessors_test.dart new file mode 100644 index 00000000..a0a3c9c6 --- /dev/null +++ b/packages/forge2d/test/api/accessors_test.dart @@ -0,0 +1,320 @@ +import 'dart:math' as math; + +import 'package:forge2d/forge2d.dart'; +import 'package:test/test.dart'; + +/// Exhaustive get/set round-trips for every public accessor, so that a +/// broken seam mapping (wrong native function, wrong argument order) in +/// either backend cannot go unnoticed. +void main() { + setUpAll(initializeForge2D); + + late World world; + late Body ground; + + Body dynamicBox(double x, double y) => world.createBody( + BodyDef(type: BodyType.dynamic, position: Vector2(x, y)), + )..createShape(Polygon.square(0.5)); + + setUp(() { + world = World(); + ground = world.createBody(BodyDef(position: Vector2(0, -1))) + ..createShape(Polygon.box(50, 1)); + }); + tearDown(() => world.destroy()); + + test('Body accessors round-trip', () { + final body = dynamicBox(0, 5) + ..angularVelocity = 1.25 + ..linearVelocity = Vector2(2, 3) + ..gravityScale = 0.5 + ..linearDamping = 0.125 + ..angularDamping = 0.25 + ..sleepThreshold = 0.75 + ..sleepEnabled = false + ..isBullet = true + ..name = 'renamed'; + + expect(body.angularVelocity, closeTo(1.25, 1e-6)); + expect(body.linearVelocity.x, closeTo(2, 1e-6)); + expect(body.gravityScale, closeTo(0.5, 1e-6)); + expect(body.linearDamping, closeTo(0.125, 1e-6)); + expect(body.angularDamping, closeTo(0.25, 1e-6)); + expect(body.sleepThreshold, closeTo(0.75, 1e-6)); + expect(body.sleepEnabled, isFalse); + expect(body.isBullet, isTrue); + // Fixing the rotation zeroes the angular velocity, so it is set last. + body.fixedRotation = true; + expect(body.fixedRotation, isTrue); + expect(body.angularVelocity, 0); + expect(body.name, 'renamed'); + expect(body.transform.position.y, closeTo(5, 1e-6)); + expect(body.worldCenterOfMass.y, closeTo(5, 1e-6)); + + body.applyForce(Vector2(10, 0), point: body.worldPoint(Vector2(0, 0.5))); + body.applyTorque(3); + body.applyLinearImpulse( + Vector2(0, 1), + point: body.worldPoint(Vector2(0.1, 0)), + ); + body.applyAngularImpulse(0.5); + world.step(1 / 60); + expect(body.isValid, isTrue); + }); + + test('Shape accessors round-trip', () { + final shape = dynamicBox(0, 5).shapes.single + ..preSolveEventsEnabled = true + ..userData = 'tagged'; + expect(shape.preSolveEventsEnabled, isTrue); + expect(shape.userData, 'tagged'); + shape.userData = null; + expect(shape.userData, isNull); + + shape.setDensity(4, updateBodyMass: false); + expect(shape.density, 4); + shape.body.applyMassFromShapes(); + expect(shape.body.mass, closeTo(4, 1e-5)); + }); + + test('Chain accessors round-trip', () { + final chain = ground.createChain( + ChainDef( + points: [Vector2(-6, 4), Vector2(-2, 4), Vector2(2, 4), Vector2(6, 4)], + ), + )..userData = 'rail'; + expect(chain.userData, 'rail'); + chain.userData = null; + expect(chain.userData, isNull); + expect(chain.segments.first.body, ground); + }); + + test('Joint common accessors round-trip', () { + final body = dynamicBox(0, 3); + final joint = world.createDistanceJoint( + DistanceJointDef( + bodyA: ground, + bodyB: body, + localAnchorA: Vector2(0, 7), + length: 4, + ), + )..collideConnected = true; + expect(joint.collideConnected, isTrue); + joint.wakeBodies(); + expect(body.isAwake, isTrue); + + for (var i = 0; i < 30; i++) { + world.step(1 / 60); + } + // The joint carries the hanging body against gravity. + expect(joint.constraintForce.length, greaterThan(0)); + expect(joint.constraintTorque.isFinite, isTrue); + }); + + test('DistanceJoint accessors round-trip', () { + final joint = + world.createDistanceJoint( + DistanceJointDef(bodyA: ground, bodyB: dynamicBox(0, 4), length: 2), + ) + ..length = 3 + ..springEnabled = true + ..springHertz = 2.5 + ..springDampingRatio = 0.3 + ..limitEnabled = true + ..motorEnabled = true + ..motorSpeed = 0.5 + ..maxMotorForce = 12 + ..setLengthRange(min: 1, max: 5); + expect(joint.length, closeTo(3, 1e-6)); + expect(joint.springEnabled, isTrue); + expect(joint.springHertz, closeTo(2.5, 1e-6)); + expect(joint.springDampingRatio, closeTo(0.3, 1e-6)); + expect(joint.limitEnabled, isTrue); + expect(joint.minLength, closeTo(1, 1e-6)); + expect(joint.maxLength, closeTo(5, 1e-6)); + expect(joint.motorEnabled, isTrue); + expect(joint.motorSpeed, closeTo(0.5, 1e-6)); + expect(joint.maxMotorForce, closeTo(12, 1e-6)); + world.step(1 / 60); + expect(joint.currentLength, greaterThan(0)); + expect(joint.motorForce.isFinite, isTrue); + }); + + test('MotorJoint accessors round-trip', () { + final joint = + world.createMotorJoint( + MotorJointDef(bodyA: ground, bodyB: dynamicBox(0, 3)), + ) + ..linearOffset = Vector2(1, 2) + ..angularOffset = 0.5 + ..maxForce = 40 + ..maxTorque = 20 + ..correctionFactor = 0.7; + expect(joint.linearOffset.x, closeTo(1, 1e-6)); + expect(joint.linearOffset.y, closeTo(2, 1e-6)); + expect(joint.angularOffset, closeTo(0.5, 1e-6)); + expect(joint.maxForce, 40); + expect(joint.maxTorque, 20); + expect(joint.correctionFactor, closeTo(0.7, 1e-6)); + }); + + test('MouseJoint accessors round-trip', () { + final joint = + world.createMouseJoint( + MouseJointDef(bodyA: ground, bodyB: dynamicBox(0, 3)), + ) + ..target = Vector2(3, 4) + ..springHertz = 6 + ..springDampingRatio = 0.9 + ..maxForce = 250; + expect(joint.target, Vector2(3, 4)); + expect(joint.springHertz, closeTo(6, 1e-6)); + expect(joint.springDampingRatio, closeTo(0.9, 1e-6)); + expect(joint.maxForce, 250); + }); + + test('PrismaticJoint accessors round-trip', () { + final joint = + world.createPrismaticJoint( + PrismaticJointDef( + bodyA: ground, + bodyB: dynamicBox(0, 3), + localAnchorA: Vector2(0, 4), + localAxisA: Vector2(0, 1), + ), + ) + ..springEnabled = true + ..springHertz = 3 + ..springDampingRatio = 0.4 + ..targetTranslation = 0.5 + ..limitEnabled = true + ..motorEnabled = true + ..motorSpeed = 1.5 + ..maxMotorForce = 60 + ..setLimits(lower: -2, upper: 2); + expect(joint.springEnabled, isTrue); + expect(joint.springHertz, closeTo(3, 1e-6)); + expect(joint.springDampingRatio, closeTo(0.4, 1e-6)); + expect(joint.targetTranslation, closeTo(0.5, 1e-6)); + expect(joint.limitEnabled, isTrue); + expect(joint.lowerLimit, closeTo(-2, 1e-6)); + expect(joint.upperLimit, closeTo(2, 1e-6)); + expect(joint.motorEnabled, isTrue); + expect(joint.motorSpeed, closeTo(1.5, 1e-6)); + expect(joint.maxMotorForce, closeTo(60, 1e-6)); + world.step(1 / 60); + expect(joint.translation.isFinite, isTrue); + expect(joint.speed.isFinite, isTrue); + expect(joint.motorForce.isFinite, isTrue); + }); + + test('RevoluteJoint accessors round-trip', () { + final joint = + world.createRevoluteJoint( + RevoluteJointDef( + bodyA: ground, + bodyB: dynamicBox(0, 3), + localAnchorA: Vector2(0, 4), + ), + ) + ..springEnabled = true + ..springHertz = 2 + ..springDampingRatio = 0.6 + ..targetAngle = 0.25 + ..limitEnabled = true + ..motorEnabled = true + ..motorSpeed = 2 + ..maxMotorTorque = 15 + ..setLimits(lower: -math.pi / 4, upper: math.pi / 4); + expect(joint.springEnabled, isTrue); + expect(joint.springHertz, closeTo(2, 1e-6)); + expect(joint.springDampingRatio, closeTo(0.6, 1e-6)); + expect(joint.targetAngle, closeTo(0.25, 1e-6)); + expect(joint.limitEnabled, isTrue); + expect(joint.lowerLimit, closeTo(-math.pi / 4, 1e-6)); + expect(joint.upperLimit, closeTo(math.pi / 4, 1e-6)); + expect(joint.motorEnabled, isTrue); + expect(joint.motorSpeed, closeTo(2, 1e-6)); + expect(joint.maxMotorTorque, closeTo(15, 1e-6)); + world.step(1 / 60); + expect(joint.angle.isFinite, isTrue); + expect(joint.motorTorque.isFinite, isTrue); + }); + + test('WeldJoint accessors round-trip', () { + final joint = + world.createWeldJoint( + WeldJointDef(bodyA: ground, bodyB: dynamicBox(0, 3)), + ) + ..linearHertz = 1.5 + ..angularHertz = 2.5 + ..linearDampingRatio = 0.2 + ..angularDampingRatio = 0.8; + expect(joint.linearHertz, closeTo(1.5, 1e-6)); + expect(joint.angularHertz, closeTo(2.5, 1e-6)); + expect(joint.linearDampingRatio, closeTo(0.2, 1e-6)); + expect(joint.angularDampingRatio, closeTo(0.8, 1e-6)); + }); + + test('WheelJoint accessors round-trip', () { + final joint = + world.createWheelJoint( + WheelJointDef(bodyA: ground, bodyB: dynamicBox(0, 3)), + ) + ..springEnabled = false + ..springHertz = 5 + ..springDampingRatio = 0.1 + ..limitEnabled = true + ..motorEnabled = true + ..motorSpeed = 3 + ..maxMotorTorque = 25 + ..setLimits(lower: -1, upper: 1); + expect(joint.springEnabled, isFalse); + expect(joint.springHertz, closeTo(5, 1e-6)); + expect(joint.springDampingRatio, closeTo(0.1, 1e-6)); + expect(joint.limitEnabled, isTrue); + expect(joint.lowerLimit, closeTo(-1, 1e-6)); + expect(joint.upperLimit, closeTo(1, 1e-6)); + expect(joint.motorEnabled, isTrue); + expect(joint.motorSpeed, closeTo(3, 1e-6)); + expect(joint.maxMotorTorque, closeTo(25, 1e-6)); + world.step(1 / 60); + expect(joint.motorTorque.isFinite, isTrue); + }); + + test('the DebugDraw defaults are callable no-ops', () { + dynamicBox(0, 2); + world.createRevoluteJoint( + RevoluteJointDef(bodyA: ground, bodyB: dynamicBox(3, 2)), + ); + // A bare subclass with every toggle on: the default empty draw methods + // must all be safely callable. + final draw = _BareDebugDraw() + ..drawJointExtras = true + ..drawBounds = true + ..drawMass = true + ..drawBodyNames = true + ..drawContacts = true + ..drawGraphColors = true + ..drawContactNormals = true + ..drawContactImpulses = true + ..drawContactFeatures = true + ..drawFrictionImpulses = true + ..drawIslands = true; + world + ..step(1 / 60) + ..draw(draw); + }); + + test('math conveniences', () { + expect(const Rot.identity(), Rot.fromAngle(0)); + expect({const Rot.identity(), Rot.fromAngle(0)}, hasLength(1)); + expect(const Rot.identity().toString(), contains('Rot')); + expect(Transform.identity().toString(), contains('Transform')); + expect(Aabb(Vector2.zero(), Vector2(1, 1)).toString(), contains('Aabb')); + final rotated = Rot.fromAngle(math.pi / 2).inverseRotate(Vector2(0, 1)); + expect(rotated.x, closeTo(1, 1e-9)); + }); +} + +class _BareDebugDraw extends DebugDraw {} diff --git a/packages/forge2d/test/api/body_test.dart b/packages/forge2d/test/api/body_test.dart index b46718d4..9c056f67 100644 --- a/packages/forge2d/test/api/body_test.dart +++ b/packages/forge2d/test/api/body_test.dart @@ -1,6 +1,3 @@ -@TestOn('vm') -library; - import 'dart:math' as math; import 'package:forge2d/forge2d.dart'; diff --git a/packages/forge2d/test/api/debug_draw_test.dart b/packages/forge2d/test/api/debug_draw_test.dart index a72d37db..fc5bfa04 100644 --- a/packages/forge2d/test/api/debug_draw_test.dart +++ b/packages/forge2d/test/api/debug_draw_test.dart @@ -1,6 +1,3 @@ -@TestOn('vm') -library; - import 'package:forge2d/forge2d.dart'; import 'package:test/test.dart'; diff --git a/packages/forge2d/test/api/events_test.dart b/packages/forge2d/test/api/events_test.dart index 385d7d26..49964037 100644 --- a/packages/forge2d/test/api/events_test.dart +++ b/packages/forge2d/test/api/events_test.dart @@ -1,6 +1,3 @@ -@TestOn('vm') -library; - import 'package:forge2d/forge2d.dart'; import 'package:test/test.dart'; diff --git a/packages/forge2d/test/api/joints_test.dart b/packages/forge2d/test/api/joints_test.dart index 5472efee..28d79d69 100644 --- a/packages/forge2d/test/api/joints_test.dart +++ b/packages/forge2d/test/api/joints_test.dart @@ -1,6 +1,3 @@ -@TestOn('vm') -library; - import 'dart:math' as math; import 'package:forge2d/forge2d.dart'; diff --git a/packages/forge2d/test/api/locked_world_test.dart b/packages/forge2d/test/api/locked_world_test.dart new file mode 100644 index 00000000..6302835e --- /dev/null +++ b/packages/forge2d/test/api/locked_world_test.dart @@ -0,0 +1,132 @@ +import 'package:forge2d/forge2d.dart'; +import 'package:test/test.dart'; + +void main() { + setUpAll(initializeForge2D); + + late World world; + + setUp(() => world = World()); + tearDown(() { + if (world.isValid) { + world.destroy(); + } + }); + + Body ground() => + world.createBody(BodyDef(position: Vector2(0, -1))) + ..createShape(Polygon.box(50, 1), ShapeDef(enablePreSolveEvents: true)); + + test('destroying a body from a pre-solve callback is deferred', () { + ground(); + final ball = world.createBody( + BodyDef(type: BodyType.dynamic, position: Vector2(0, 2)), + )..createShape(Circle(radius: 0.5), ShapeDef(enablePreSolveEvents: true)); + + var sawContact = false; + world.preSolveCallback = (shapeA, shapeB, normal) { + sawContact = true; + // Destroying mid-step must not abort or corrupt; it is deferred. + ball.destroy(); + expect(ball.isValid, isTrue, reason: 'still valid inside the step'); + return true; + }; + + for (var i = 0; i < 120 && !sawContact; i++) { + world.step(1 / 60); + } + expect(sawContact, isTrue); + expect(ball.isValid, isFalse, reason: 'destroyed after the step ended'); + }); + + test('a deferred destroy runs only once even if requested twice', () { + ground(); + final ball = world.createBody( + BodyDef(type: BodyType.dynamic, position: Vector2(0, 2)), + )..createShape(Circle(radius: 0.5), ShapeDef(enablePreSolveEvents: true)); + + var calls = 0; + world.preSolveCallback = (shapeA, shapeB, normal) { + calls++; + ball + ..destroy() + ..destroy(); + return true; + }; + for (var i = 0; i < 120 && calls == 0; i++) { + world.step(1 / 60); + } + expect(ball.isValid, isFalse); + // A second step must not blow up on the already-destroyed body. + world.step(1 / 60); + }); + + test('creating a body from a pre-solve callback throws a StateError', () { + ground(); + world + .createBody(BodyDef(type: BodyType.dynamic, position: Vector2(0, 2))) + .createShape( + Circle(radius: 0.5), + ShapeDef(enablePreSolveEvents: true), + ); + + Object? caught; + world.preSolveCallback = (shapeA, shapeB, normal) { + try { + world.createBody(); + } on Object catch (error) { + caught = error; + } + return true; + }; + for (var i = 0; i < 120 && caught == null; i++) { + world.step(1 / 60); + } + expect(caught, isA()); + }); + + test('destroying a world clears its simulation callbacks', () { + // A world whose filter callback rejects every collision. + ground(); + world.customFilterCallback = (shapeA, shapeB) => false; + world.destroy(); + + // A new world likely reuses the same slot; its collisions must not be + // filtered by the dead world's callback. + world = World(); + ground(); + final ball = world.createBody( + BodyDef(type: BodyType.dynamic, position: Vector2(0, 2)), + )..createShape(Circle(radius: 0.5)); + for (var i = 0; i < 120; i++) { + world.step(1 / 60); + } + expect( + ball.position.y, + greaterThan(0), + reason: "ball must land, not fall through the dead world's filter", + ); + }); + + test('destroying a body releases the user data of its chains', () { + final carrier = world.createBody(); + carrier.createChain( + ChainDef( + points: [Vector2(-6, 0), Vector2(-2, 0), Vector2(2, 0), Vector2(6, 0)], + userData: 'terrain', + ), + ); + expect(world.chainUserData, hasLength(1)); + carrier.destroy(); + expect(world.chainUserData, isEmpty); + expect(world.chainOwners, isEmpty); + }); + + test('long body names are safe and truncated to 31 bytes', () { + // The native library caps names at 31 bytes; the point of the long + // input is that the web backend must not overflow its string buffer. + final name = 'domino ${'x' * 5000}'; + final body = world.createBody(BodyDef(name: name)); + expect(body.name, name.substring(0, 31)); + }); +} diff --git a/packages/forge2d/test/api/shape_test.dart b/packages/forge2d/test/api/shape_test.dart index d6040f3d..5385dcdc 100644 --- a/packages/forge2d/test/api/shape_test.dart +++ b/packages/forge2d/test/api/shape_test.dart @@ -1,6 +1,3 @@ -@TestOn('vm') -library; - import 'package:forge2d/forge2d.dart'; import 'package:test/test.dart'; @@ -116,6 +113,65 @@ void main() { expect(shape.userData, 'updated'); }); + test('geometry reads back every shape kind', () { + final circle = body + .createShape(Circle(center: Vector2(1, 2), radius: 0.75)) + .geometry; + expect(circle, isA()); + circle as Circle; + expect(circle.center.x, closeTo(1, 1e-6)); + expect(circle.center.y, closeTo(2, 1e-6)); + expect(circle.radius, closeTo(0.75, 1e-6)); + + final capsule = body + .createShape( + Capsule( + center1: Vector2(0, -1), + center2: Vector2(0, 1), + radius: 0.5, + ), + ) + .geometry; + expect(capsule, isA()); + capsule as Capsule; + expect(capsule.center1.y, closeTo(-1, 1e-6)); + expect(capsule.center2.y, closeTo(1, 1e-6)); + expect(capsule.radius, closeTo(0.5, 1e-6)); + + final segment = body + .createShape(Segment(point1: Vector2(-2, 0), point2: Vector2(2, 1))) + .geometry; + expect(segment, isA()); + segment as Segment; + expect(segment.point1.x, closeTo(-2, 1e-6)); + expect(segment.point2.y, closeTo(1, 1e-6)); + + final box = body.createShape(Polygon.box(1.5, 0.5)).geometry; + expect(box, isA()); + box as Polygon; + expect(box.points, hasLength(4)); + expect(box.radius, 0); + final xs = box.points!.map((point) => point.x.abs()); + final ys = box.points!.map((point) => point.y.abs()); + expect(xs.every((x) => (x - 1.5).abs() < 1e-5), isTrue); + expect(ys.every((y) => (y - 0.5).abs() < 1e-5), isTrue); + + final chainSegments = body + .createChain( + ChainDef( + points: [ + Vector2(-6, 0), + Vector2(-2, 0), + Vector2(2, 0), + Vector2(6, 0), + ], + ), + ) + .segments; + final chainSegment = chainSegments.first.geometry; + expect(chainSegment, isA()); + }); + test('testPoint and aabb agree with the geometry', () { final shape = body.createShape(Polygon.square(1)); expect(shape.testPoint(Vector2(0.5, 0.5)), isTrue); @@ -166,7 +222,12 @@ void main() { test('friction and restitution apply to all segments', () { final chain = body.createChain( ChainDef( - points: [Vector2(-5, 0), Vector2(0, 0), Vector2(5, 0)], + points: [ + Vector2(-6, 0), + Vector2(-2, 0), + Vector2(2, 0), + Vector2(6, 0), + ], materials: [SurfaceMaterial(friction: 0.3, restitution: 0.2)], ), )..friction = 0.8; @@ -178,7 +239,14 @@ void main() { test('destroy removes the chain and its segments', () { final chain = body.createChain( - ChainDef(points: [Vector2(-5, 0), Vector2(0, 0), Vector2(5, 0)]), + ChainDef( + points: [ + Vector2(-6, 0), + Vector2(-2, 0), + Vector2(2, 0), + Vector2(6, 0), + ], + ), )..destroy(); expect(chain.isValid, isFalse); }); diff --git a/packages/forge2d/test/api/world_test.dart b/packages/forge2d/test/api/world_test.dart index b1c5cd9e..fe268edd 100644 --- a/packages/forge2d/test/api/world_test.dart +++ b/packages/forge2d/test/api/world_test.dart @@ -1,6 +1,3 @@ -@TestOn('vm') -library; - import 'package:forge2d/forge2d.dart'; import 'package:test/test.dart'; diff --git a/packages/forge2d/test/backend/world_backend_test.dart b/packages/forge2d/test/backend/world_backend_test.dart index b95cafc0..502daf51 100644 --- a/packages/forge2d/test/backend/world_backend_test.dart +++ b/packages/forge2d/test/backend/world_backend_test.dart @@ -1,6 +1,3 @@ -@TestOn('vm') -library; - import 'package:forge2d/forge2d.dart'; import 'package:forge2d/src/initialize.dart'; import 'package:test/test.dart'; diff --git a/packages/forge2d/tool/build_wasm.sh b/packages/forge2d/tool/build_wasm.sh new file mode 100755 index 00000000..da5436cc --- /dev/null +++ b/packages/forge2d/tool/build_wasm.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Builds the Box2D WebAssembly module used by the web backend. +# +# Requires an activated emsdk (https://github.com/emscripten-core/emsdk); +# CI pins the version, see .github/workflows/build-wasm.yml. +# +# Usage: tool/build_wasm.sh +set -euo pipefail + +PACKAGE_DIR="$(cd "$(dirname "$0")/.." && pwd)" +BOX2D="$PACKAGE_DIR/third_party/box2d" +OUT="$PACKAGE_DIR/lib/src/backend/wasm/box2d.wasm" + +emcc \ + -O3 \ + -std=gnu17 \ + -DNDEBUG \ + -msimd128 \ + -msse2 \ + --no-entry \ + -sSTANDALONE_WASM=1 \ + -sALLOW_MEMORY_GROWTH=1 \ + -sINITIAL_MEMORY=16MB \ + -sFILESYSTEM=0 \ + -sEXPORTED_FUNCTIONS=_malloc,_free \ + -sERROR_ON_UNDEFINED_SYMBOLS=0 \ + -I"$BOX2D/include" \ + -I"$BOX2D/src" \ + "$BOX2D"/src/*.c \ + "$PACKAGE_DIR/native/wasm/f2d_shim.c" \ + -o "$OUT" + +ls -la "$OUT"