diff --git a/packages/rapier2d/module.json b/packages/rapier2d/module.json new file mode 100644 index 00000000..4b44e05c --- /dev/null +++ b/packages/rapier2d/module.json @@ -0,0 +1,32 @@ +{ + "id": "rapier2d", + "name": "@esengine/rapier2d", + "displayName": "Rapier2D", + "description": "Rapier2D physics engine WASM bindings | Rapier2D 物理引擎 WASM 绑定", + "version": "0.14.0", + "category": "Physics", + "icon": "Atom", + "tags": [ + "physics", + "wasm", + "rapier" + ], + "isCore": false, + "defaultEnabled": false, + "isEngineModule": true, + "canContainContent": false, + "platforms": [ + "web", + "desktop" + ], + "dependencies": [], + "exports": { + "other": ["RAPIER"] + }, + "requiresWasm": true, + "wasmPaths": [ + "pkg/rapier_wasm2d_bg.wasm" + ], + "outputPath": "dist/index.js", + "isExternalDependency": true +} diff --git a/packages/rapier2d/package.json b/packages/rapier2d/package.json new file mode 100644 index 00000000..da402246 --- /dev/null +++ b/packages/rapier2d/package.json @@ -0,0 +1,31 @@ +{ + "name": "@esengine/rapier2d", + "version": "0.14.0", + "description": "Rapier2D physics engine with dynamic WASM loading support", + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./pkg/*": "./pkg/*" + }, + "files": [ + "dist", + "pkg/*.wasm" + ], + "scripts": { + "gen:src": "node scripts/gen-src.mjs", + "build": "pnpm gen:src && tsup", + "clean": "rimraf dist src" + }, + "license": "Apache-2.0", + "devDependencies": { + "rimraf": "^5.0.0", + "tsup": "^8.0.0", + "typescript": "^5.0.0" + } +} diff --git a/packages/rapier2d/scripts/gen-src.mjs b/packages/rapier2d/scripts/gen-src.mjs new file mode 100644 index 00000000..4dcd68c3 --- /dev/null +++ b/packages/rapier2d/scripts/gen-src.mjs @@ -0,0 +1,125 @@ +/** + * Generate 2D-specific source code from rapier.js source. + * 从 rapier.js 源码生成 2D 专用代码。 + * + * This script: + * 1. Copies TypeScript source from rapier.js/src.ts + * 2. Removes #if DIM3 ... #endif blocks (keeps only 2D code) + * 3. Overwrites raw.ts and init.ts with 2D-specific versions + */ + +import { readFileSync, writeFileSync, readdirSync, mkdirSync, cpSync, existsSync, renameSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const packageRoot = join(__dirname, '..'); +const rapierRoot = join(packageRoot, '..', '..', 'thirdparty', 'rapier.js'); +const srcTsDir = join(rapierRoot, 'src.ts'); +const src2dDir = join(rapierRoot, 'rapier-compat', 'src2d'); +const outputDir = join(packageRoot, 'src'); + +// Check if rapier.js exists +if (!existsSync(srcTsDir)) { + console.error(`Error: rapier.js source not found at ${rapierRoot}`); + console.error('Please clone https://github.com/esengine/rapier.js.git to thirdparty/rapier.js'); + process.exit(1); +} + +/** + * Remove #if DIM3 ... #endif blocks from source code + */ +function removeDim3Blocks(content) { + // Remove lines between #if DIM3 and #endif (inclusive) + const lines = content.split('\n'); + const result = []; + let skipDepth = 0; + + for (const line of lines) { + const trimmed = line.trim(); + + if (trimmed.startsWith('//#if DIM3') || trimmed.startsWith('// #if DIM3')) { + skipDepth++; + continue; + } + + if (skipDepth > 0 && (trimmed.startsWith('//#endif') || trimmed.startsWith('// #endif'))) { + skipDepth--; + continue; + } + + if (skipDepth === 0) { + // Also remove #if DIM2 and its #endif (but keep the content) + if (trimmed.startsWith('//#if DIM2') || trimmed.startsWith('// #if DIM2')) { + continue; + } + if (trimmed.startsWith('//#endif') || trimmed.startsWith('// #endif')) { + continue; + } + result.push(line); + } + } + + return result.join('\n'); +} + +/** + * Process a single TypeScript file + */ +function processFile(srcPath, destPath) { + const content = readFileSync(srcPath, 'utf-8'); + const processed = removeDim3Blocks(content); + writeFileSync(destPath, processed); +} + +/** + * Recursively copy and process directory + */ +function processDirectory(srcDir, destDir) { + mkdirSync(destDir, { recursive: true }); + + const entries = readdirSync(srcDir, { withFileTypes: true }); + + for (const entry of entries) { + const srcPath = join(srcDir, entry.name); + const destPath = join(destDir, entry.name); + + if (entry.isDirectory()) { + processDirectory(srcPath, destPath); + } else if (entry.name.endsWith('.ts')) { + processFile(srcPath, destPath); + console.log(`Processed: ${entry.name}`); + } + } +} + +// Main +console.log('Generating 2D source code...'); +console.log(`Source: ${srcTsDir}`); +console.log(`Output: ${outputDir}`); + +// Step 1: Copy and process src.ts directory +processDirectory(srcTsDir, outputDir); + +// Step 2: Overwrite with 2D-specific files (raw.ts, init.ts) +if (existsSync(src2dDir)) { + const entries = readdirSync(src2dDir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isFile() && entry.name.endsWith('.ts')) { + const srcPath = join(src2dDir, entry.name); + const destPath = join(outputDir, entry.name); + cpSync(srcPath, destPath); + console.log(`Overwrote: ${entry.name} (2D-specific)`); + } + } +} + +// Step 3: Rename rapier.ts to index.ts +const rapierTs = join(outputDir, 'rapier.ts'); +const indexTs = join(outputDir, 'index.ts'); +if (existsSync(rapierTs)) { + renameSync(rapierTs, indexTs); + console.log('Renamed: rapier.ts -> index.ts'); +} + +console.log('Done!'); diff --git a/packages/rapier2d/src/coarena.ts b/packages/rapier2d/src/coarena.ts new file mode 100644 index 00000000..9bf978a5 --- /dev/null +++ b/packages/rapier2d/src/coarena.ts @@ -0,0 +1,70 @@ +export class Coarena { + fconv: Float64Array; + uconv: Uint32Array; + data: Array; + size: number; + + public constructor() { + this.fconv = new Float64Array(1); + this.uconv = new Uint32Array(this.fconv.buffer); + this.data = new Array(); + this.size = 0; + } + + public set(handle: number, data: T) { + let i = this.index(handle); + while (this.data.length <= i) { + this.data.push(null); + } + + if (this.data[i] == null) this.size += 1; + this.data[i] = data; + } + + public len(): number { + return this.size; + } + + public delete(handle: number) { + let i = this.index(handle); + if (i < this.data.length) { + if (this.data[i] != null) this.size -= 1; + this.data[i] = null; + } + } + + public clear() { + this.data = new Array(); + } + + public get(handle: number): T | null { + let i = this.index(handle); + if (i < this.data.length) { + return this.data[i]; + } else { + return null; + } + } + + public forEach(f: (elt: T) => void) { + for (const elt of this.data) { + if (elt != null) f(elt); + } + } + + public getAll(): Array { + return this.data.filter((elt) => elt != null); + } + + private index(handle: number): number { + /// Extracts the index part of a handle (the lower 32 bits). + /// This is done by first injecting the handle into an Float64Array + /// which is itself injected into an Uint32Array (at construction time). + /// The 0-th value of the Uint32Array will become the `number` integer + /// representation of the lower 32 bits. + /// Also `this.uconv[1]` then contains the generation number as a `number`, + /// which we don’t really need. + this.fconv[0] = handle; + return this.uconv[0]; + } +} diff --git a/packages/rapier2d/src/control/character_controller.ts b/packages/rapier2d/src/control/character_controller.ts new file mode 100644 index 00000000..8e536392 --- /dev/null +++ b/packages/rapier2d/src/control/character_controller.ts @@ -0,0 +1,386 @@ +import {RawKinematicCharacterController, RawCharacterCollision} from "../raw"; +import {Rotation, Vector, VectorOps} from "../math"; +import { + BroadPhase, + Collider, + ColliderSet, + InteractionGroups, + NarrowPhase, + Shape, +} from "../geometry"; +import {QueryFilterFlags, World} from "../pipeline"; +import {IntegrationParameters, RigidBody, RigidBodySet} from "../dynamics"; + +/** + * A collision between the character and an obstacle hit on its path. + */ +export class CharacterCollision { + /** The collider involved in the collision. Null if the collider no longer exists in the physics world. */ + public collider: Collider | null; + /** The translation delta applied to the character before this collision took place. */ + public translationDeltaApplied: Vector; + /** The translation delta the character would move after this collision if there is no other obstacles. */ + public translationDeltaRemaining: Vector; + /** The time-of-impact between the character and the obstacles. */ + public toi: number; + /** The world-space contact point on the collider when the collision happens. */ + public witness1: Vector; + /** The local-space contact point on the character when the collision happens. */ + public witness2: Vector; + /** The world-space outward contact normal on the collider when the collision happens. */ + public normal1: Vector; + /** The local-space outward contact normal on the character when the collision happens. */ + public normal2: Vector; +} + +/** + * A character controller for controlling kinematic bodies and parentless colliders by hitting + * and sliding against obstacles. + */ +export class KinematicCharacterController { + private raw: RawKinematicCharacterController; + private rawCharacterCollision: RawCharacterCollision; + + private params: IntegrationParameters; + private broadPhase: BroadPhase; + private narrowPhase: NarrowPhase; + private bodies: RigidBodySet; + private colliders: ColliderSet; + private _applyImpulsesToDynamicBodies: boolean; + private _characterMass: number | null; + + constructor( + offset: number, + params: IntegrationParameters, + broadPhase: BroadPhase, + narrowPhase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + ) { + this.params = params; + this.bodies = bodies; + this.colliders = colliders; + this.broadPhase = broadPhase; + this.narrowPhase = narrowPhase; + this.raw = new RawKinematicCharacterController(offset); + this.rawCharacterCollision = new RawCharacterCollision(); + this._applyImpulsesToDynamicBodies = false; + this._characterMass = null; + } + + /** @internal */ + public free() { + if (!!this.raw) { + this.raw.free(); + this.rawCharacterCollision.free(); + } + + this.raw = undefined; + this.rawCharacterCollision = undefined; + } + + /** + * The direction that goes "up". Used to determine where the floor is, and the floor’s angle. + */ + public up(): Vector { + return this.raw.up(); + } + + /** + * Sets the direction that goes "up". Used to determine where the floor is, and the floor’s angle. + */ + public setUp(vector: Vector) { + let rawVect = VectorOps.intoRaw(vector); + return this.raw.setUp(rawVect); + rawVect.free(); + } + + public applyImpulsesToDynamicBodies(): boolean { + return this._applyImpulsesToDynamicBodies; + } + + public setApplyImpulsesToDynamicBodies(enabled: boolean) { + this._applyImpulsesToDynamicBodies = enabled; + } + + /** + * Returns the custom value of the character mass, if it was set by `this.setCharacterMass`. + */ + public characterMass(): number | null { + return this._characterMass; + } + + /** + * Set the mass of the character to be used for impulse resolution if `self.applyImpulsesToDynamicBodies` + * is set to `true`. + * + * If no character mass is set explicitly (or if it is set to `null`) it is automatically assumed to be equal + * to the mass of the rigid-body the character collider is attached to; or equal to 0 if the character collider + * isn’t attached to any rigid-body. + * + * @param mass - The mass to set. + */ + public setCharacterMass(mass: number | null) { + this._characterMass = mass; + } + + /** + * A small gap to preserve between the character and its surroundings. + * + * This value should not be too large to avoid visual artifacts, but shouldn’t be too small + * (must not be zero) to improve numerical stability of the character controller. + */ + public offset(): number { + return this.raw.offset(); + } + + /** + * Sets a small gap to preserve between the character and its surroundings. + * + * This value should not be too large to avoid visual artifacts, but shouldn’t be too small + * (must not be zero) to improve numerical stability of the character controller. + */ + public setOffset(value: number) { + this.raw.setOffset(value); + } + + /// Increase this number if your character appears to get stuck when sliding against surfaces. + /// + /// This is a small distance applied to the movement toward the contact normals of shapes hit + /// by the character controller. This helps shape-casting not getting stuck in an always-penetrating + /// state during the sliding calculation. + /// + /// This value should remain fairly small since it can introduce artificial "bumps" when sliding + /// along a flat surface. + public normalNudgeFactor(): number { + return this.raw.normalNudgeFactor(); + } + + /// Increase this number if your character appears to get stuck when sliding against surfaces. + /// + /// This is a small distance applied to the movement toward the contact normals of shapes hit + /// by the character controller. This helps shape-casting not getting stuck in an always-penetrating + /// state during the sliding calculation. + /// + /// This value should remain fairly small since it can introduce artificial "bumps" when sliding + /// along a flat surface. + public setNormalNudgeFactor(value: number) { + this.raw.setNormalNudgeFactor(value); + } + + /** + * Is sliding against obstacles enabled? + */ + public slideEnabled(): boolean { + return this.raw.slideEnabled(); + } + + /** + * Enable or disable sliding against obstacles. + */ + public setSlideEnabled(enabled: boolean) { + this.raw.setSlideEnabled(enabled); + } + + /** + * The maximum step height a character can automatically step over. + */ + public autostepMaxHeight(): number | null { + return this.raw.autostepMaxHeight(); + } + + /** + * The minimum width of free space that must be available after stepping on a stair. + */ + public autostepMinWidth(): number | null { + return this.raw.autostepMinWidth(); + } + + /** + * Can the character automatically step over dynamic bodies too? + */ + public autostepIncludesDynamicBodies(): boolean | null { + return this.raw.autostepIncludesDynamicBodies(); + } + + /** + * Is automatically stepping over small objects enabled? + */ + public autostepEnabled(): boolean { + return this.raw.autostepEnabled(); + } + + /** + * Enabled automatically stepping over small objects. + * + * @param maxHeight - The maximum step height a character can automatically step over. + * @param minWidth - The minimum width of free space that must be available after stepping on a stair. + * @param includeDynamicBodies - Can the character automatically step over dynamic bodies too? + */ + public enableAutostep( + maxHeight: number, + minWidth: number, + includeDynamicBodies: boolean, + ) { + this.raw.enableAutostep(maxHeight, minWidth, includeDynamicBodies); + } + + /** + * Disable automatically stepping over small objects. + */ + public disableAutostep() { + return this.raw.disableAutostep(); + } + + /** + * The maximum angle (radians) between the floor’s normal and the `up` vector that the + * character is able to climb. + */ + public maxSlopeClimbAngle(): number { + return this.raw.maxSlopeClimbAngle(); + } + + /** + * Sets the maximum angle (radians) between the floor’s normal and the `up` vector that the + * character is able to climb. + */ + public setMaxSlopeClimbAngle(angle: number) { + this.raw.setMaxSlopeClimbAngle(angle); + } + + /** + * The minimum angle (radians) between the floor’s normal and the `up` vector before the + * character starts to slide down automatically. + */ + public minSlopeSlideAngle(): number { + return this.raw.minSlopeSlideAngle(); + } + + /** + * Sets the minimum angle (radians) between the floor’s normal and the `up` vector before the + * character starts to slide down automatically. + */ + public setMinSlopeSlideAngle(angle: number) { + this.raw.setMinSlopeSlideAngle(angle); + } + + /** + * If snap-to-ground is enabled, should the character be automatically snapped to the ground if + * the distance between the ground and its feet are smaller than the specified threshold? + */ + public snapToGroundDistance(): number | null { + return this.raw.snapToGroundDistance(); + } + + /** + * Enables automatically snapping the character to the ground if the distance between + * the ground and its feet are smaller than the specified threshold. + */ + public enableSnapToGround(distance: number) { + this.raw.enableSnapToGround(distance); + } + + /** + * Disables automatically snapping the character to the ground. + */ + public disableSnapToGround() { + this.raw.disableSnapToGround(); + } + + /** + * Is automatically snapping the character to the ground enabled? + */ + public snapToGroundEnabled(): boolean { + return this.raw.snapToGroundEnabled(); + } + + /** + * Computes the movement the given collider is able to execute after hitting and sliding on obstacles. + * + * @param collider - The collider to move. + * @param desiredTranslationDelta - The desired collider movement. + * @param filterFlags - Flags for excluding whole subsets of colliders from the obstacles taken into account. + * @param filterGroups - Groups for excluding colliders with incompatible collision groups from the obstacles + * taken into account. + * @param filterPredicate - Any collider for which this closure returns `false` will be excluded from the + * obstacles taken into account. + */ + public computeColliderMovement( + collider: Collider, + desiredTranslationDelta: Vector, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterPredicate?: (collider: Collider) => boolean, + ) { + let rawTranslationDelta = VectorOps.intoRaw(desiredTranslationDelta); + this.raw.computeColliderMovement( + this.params.dt, + this.broadPhase.raw, + this.narrowPhase.raw, + this.bodies.raw, + this.colliders.raw, + collider.handle, + rawTranslationDelta, + this._applyImpulsesToDynamicBodies, + this._characterMass, + filterFlags, + filterGroups, + this.colliders.castClosure(filterPredicate), + ); + rawTranslationDelta.free(); + } + + /** + * The movement computed by the last call to `this.computeColliderMovement`. + */ + public computedMovement(): Vector { + return VectorOps.fromRaw(this.raw.computedMovement()); + } + + /** + * The result of ground detection computed by the last call to `this.computeColliderMovement`. + */ + public computedGrounded(): boolean { + return this.raw.computedGrounded(); + } + + /** + * The number of collisions against obstacles detected along the path of the last call + * to `this.computeColliderMovement`. + */ + public numComputedCollisions(): number { + return this.raw.numComputedCollisions(); + } + + /** + * Returns the collision against one of the obstacles detected along the path of the last + * call to `this.computeColliderMovement`. + * + * @param i - The i-th collision will be returned. + * @param out - If this argument is set, it will be filled with the collision information. + */ + public computedCollision( + i: number, + out?: CharacterCollision, + ): CharacterCollision | null { + if (!this.raw.computedCollision(i, this.rawCharacterCollision)) { + return null; + } else { + let c = this.rawCharacterCollision; + out = out ?? new CharacterCollision(); + out.translationDeltaApplied = VectorOps.fromRaw( + c.translationDeltaApplied(), + ); + out.translationDeltaRemaining = VectorOps.fromRaw( + c.translationDeltaRemaining(), + ); + out.toi = c.toi(); + out.witness1 = VectorOps.fromRaw(c.worldWitness1()); + out.witness2 = VectorOps.fromRaw(c.worldWitness2()); + out.normal1 = VectorOps.fromRaw(c.worldNormal1()); + out.normal2 = VectorOps.fromRaw(c.worldNormal2()); + out.collider = this.colliders.get(c.handle()); + return out; + } + } +} diff --git a/packages/rapier2d/src/control/index.ts b/packages/rapier2d/src/control/index.ts new file mode 100644 index 00000000..45098d25 --- /dev/null +++ b/packages/rapier2d/src/control/index.ts @@ -0,0 +1,3 @@ +export * from "./character_controller"; +export * from "./pid_controller"; + diff --git a/packages/rapier2d/src/control/pid_controller.ts b/packages/rapier2d/src/control/pid_controller.ts new file mode 100644 index 00000000..97b3ba11 --- /dev/null +++ b/packages/rapier2d/src/control/pid_controller.ts @@ -0,0 +1,153 @@ +import {RawPidController} from "../raw"; +import {Rotation, RotationOps, Vector, VectorOps} from "../math"; +import {Collider, ColliderSet, InteractionGroups, Shape} from "../geometry"; +import {QueryFilterFlags, World} from "../pipeline"; +import {IntegrationParameters, RigidBody, RigidBodySet} from "../dynamics"; + +// TODO: unify with the JointAxesMask +/** + * An enum representing the possible joint axes controlled by a PidController. + * They can be ORed together, like: + * PidAxesMask.LinX || PidAxesMask.LinY + * to get a pid controller that only constraints the translational X and Y axes. + * + * Possible axes are: + * + * - `X`: X translation axis + * - `Y`: Y translation axis + * - `Z`: Z translation axis + * - `AngX`: X angular rotation axis (3D only) + * - `AngY`: Y angular rotation axis (3D only) + * - `AngZ`: Z angular rotation axis + */ +export enum PidAxesMask { + None = 0, + LinX = 1 << 0, + LinY = 1 << 1, + LinZ = 1 << 2, + AngZ = 1 << 5, + AllLin = PidAxesMask.LinX | PidAxesMask.LinY, + AllAng = PidAxesMask.AngZ, + All = PidAxesMask.AllLin | PidAxesMask.AllAng, +} + +/** + * A controller for controlling dynamic bodies using the + * Proportional-Integral-Derivative correction model. + */ +export class PidController { + private raw: RawPidController; + + private params: IntegrationParameters; + private bodies: RigidBodySet; + + constructor( + params: IntegrationParameters, + bodies: RigidBodySet, + kp: number, + ki: number, + kd: number, + axes: PidAxesMask, + ) { + this.params = params; + this.bodies = bodies; + this.raw = new RawPidController(kp, ki, kd, axes); + } + + /** @internal */ + public free() { + if (!!this.raw) { + this.raw.free(); + } + + this.raw = undefined; + } + + public setKp(kp: number, axes: PidAxesMask) { + this.raw.set_kp(kp, axes); + } + + public setKi(ki: number, axes: PidAxesMask) { + this.raw.set_kp(ki, axes); + } + + public setKd(kd: number, axes: PidAxesMask) { + this.raw.set_kp(kd, axes); + } + + public setAxes(axes: PidAxesMask) { + this.raw.set_axes_mask(axes); + } + + public resetIntegrals() { + this.raw.reset_integrals(); + } + + public applyLinearCorrection( + body: RigidBody, + targetPosition: Vector, + targetLinvel: Vector, + ) { + let rawPos = VectorOps.intoRaw(targetPosition); + let rawVel = VectorOps.intoRaw(targetLinvel); + this.raw.apply_linear_correction( + this.params.dt, + this.bodies.raw, + body.handle, + rawPos, + rawVel, + ); + rawPos.free(); + rawVel.free(); + } + + public applyAngularCorrection( + body: RigidBody, + targetRotation: number, + targetAngVel: number, + ) { + this.raw.apply_angular_correction( + this.params.dt, + this.bodies.raw, + body.handle, + targetRotation, + targetAngVel, + ); + } + + + public linearCorrection( + body: RigidBody, + targetPosition: Vector, + targetLinvel: Vector, + ): Vector { + let rawPos = VectorOps.intoRaw(targetPosition); + let rawVel = VectorOps.intoRaw(targetLinvel); + let correction = this.raw.linear_correction( + this.params.dt, + this.bodies.raw, + body.handle, + rawPos, + rawVel, + ); + rawPos.free(); + rawVel.free(); + + return VectorOps.fromRaw(correction); + } + + public angularCorrection( + body: RigidBody, + targetRotation: number, + targetAngVel: number, + ): number { + return this.raw.angular_correction( + this.params.dt, + this.bodies.raw, + body.handle, + targetRotation, + targetAngVel, + ); + } + +} diff --git a/packages/rapier2d/src/control/ray_cast_vehicle_controller.ts b/packages/rapier2d/src/control/ray_cast_vehicle_controller.ts new file mode 100644 index 00000000..fa65b3e3 --- /dev/null +++ b/packages/rapier2d/src/control/ray_cast_vehicle_controller.ts @@ -0,0 +1,481 @@ +import {RawDynamicRayCastVehicleController} from "../raw"; +import {Vector, VectorOps} from "../math"; +import { + BroadPhase, + Collider, + ColliderSet, + InteractionGroups, + NarrowPhase, +} from "../geometry"; +import {QueryFilterFlags} from "../pipeline"; +import {RigidBody, RigidBodyHandle, RigidBodySet} from "../dynamics"; + +/** + * A character controller to simulate vehicles using ray-casting for the wheels. + */ +export class DynamicRayCastVehicleController { + private raw: RawDynamicRayCastVehicleController; + private broadPhase: BroadPhase; + private narrowPhase: NarrowPhase; + private bodies: RigidBodySet; + private colliders: ColliderSet; + private _chassis: RigidBody; + + constructor( + chassis: RigidBody, + broadPhase: BroadPhase, + narrowPhase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + ) { + this.raw = new RawDynamicRayCastVehicleController(chassis.handle); + this.broadPhase = broadPhase; + this.narrowPhase = narrowPhase; + this.bodies = bodies; + this.colliders = colliders; + this._chassis = chassis; + } + + /** @internal */ + public free() { + if (!!this.raw) { + this.raw.free(); + } + + this.raw = undefined; + } + + /** + * Updates the vehicle’s velocity based on its suspension, engine force, and brake. + * + * This directly updates the velocity of its chassis rigid-body. + * + * @param dt - Time increment used to integrate forces. + * @param filterFlags - Flag to exclude categories of objects from the wheels’ ray-cast. + * @param filterGroups - Only colliders compatible with these groups will be hit by the wheels’ ray-casts. + * @param filterPredicate - Callback to filter out which collider will be hit by the wheels’ ray-casts. + */ + public updateVehicle( + dt: number, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterPredicate?: (collider: Collider) => boolean, + ) { + this.raw.update_vehicle( + dt, + this.broadPhase.raw, + this.narrowPhase.raw, + this.bodies.raw, + this.colliders.raw, + filterFlags, + filterGroups, + this.colliders.castClosure(filterPredicate), + ); + } + + /** + * The current forward speed of the vehicle. + */ + public currentVehicleSpeed(): number { + return this.raw.current_vehicle_speed(); + } + + /** + * The rigid-body used as the chassis. + */ + public chassis(): RigidBody { + return this._chassis; + } + + /** + * The chassis’ local _up_ direction (`0 = x, 1 = y, 2 = z`). + */ + get indexUpAxis(): number { + return this.raw.index_up_axis(); + } + + /** + * Sets the chassis’ local _up_ direction (`0 = x, 1 = y, 2 = z`). + */ + set indexUpAxis(axis: number) { + this.raw.set_index_up_axis(axis); + } + + /** + * The chassis’ local _forward_ direction (`0 = x, 1 = y, 2 = z`). + */ + get indexForwardAxis(): number { + return this.raw.index_forward_axis(); + } + + /** + * Sets the chassis’ local _forward_ direction (`0 = x, 1 = y, 2 = z`). + */ + set setIndexForwardAxis(axis: number) { + this.raw.set_index_forward_axis(axis); + } + + /** + * Adds a new wheel attached to this vehicle. + * @param chassisConnectionCs - The position of the wheel relative to the chassis. + * @param directionCs - The direction of the wheel’s suspension, relative to the chassis. The ray-casting will + * happen following this direction to detect the ground. + * @param axleCs - The wheel’s axle axis, relative to the chassis. + * @param suspensionRestLength - The rest length of the wheel’s suspension spring. + * @param radius - The wheel’s radius. + */ + public addWheel( + chassisConnectionCs: Vector, + directionCs: Vector, + axleCs: Vector, + suspensionRestLength: number, + radius: number, + ) { + let rawChassisConnectionCs = VectorOps.intoRaw(chassisConnectionCs); + let rawDirectionCs = VectorOps.intoRaw(directionCs); + let rawAxleCs = VectorOps.intoRaw(axleCs); + + this.raw.add_wheel( + rawChassisConnectionCs, + rawDirectionCs, + rawAxleCs, + suspensionRestLength, + radius, + ); + + rawChassisConnectionCs.free(); + rawDirectionCs.free(); + rawAxleCs.free(); + } + + /** + * The number of wheels attached to this vehicle. + */ + public numWheels(): number { + return this.raw.num_wheels(); + } + + /* + * + * Access to wheel properties. + * + */ + /* + * Getters + setters + */ + /** + * The position of the i-th wheel, relative to the chassis. + */ + public wheelChassisConnectionPointCs(i: number): Vector | null { + return VectorOps.fromRaw(this.raw.wheel_chassis_connection_point_cs(i)); + } + + /** + * Sets the position of the i-th wheel, relative to the chassis. + */ + public setWheelChassisConnectionPointCs(i: number, value: Vector) { + let rawValue = VectorOps.intoRaw(value); + this.raw.set_wheel_chassis_connection_point_cs(i, rawValue); + rawValue.free(); + } + + /** + * The rest length of the i-th wheel’s suspension spring. + */ + public wheelSuspensionRestLength(i: number): number | null { + return this.raw.wheel_suspension_rest_length(i); + } + + /** + * Sets the rest length of the i-th wheel’s suspension spring. + */ + public setWheelSuspensionRestLength(i: number, value: number) { + this.raw.set_wheel_suspension_rest_length(i, value); + } + + /** + * The maximum distance the i-th wheel suspension can travel before and after its resting length. + */ + public wheelMaxSuspensionTravel(i: number): number | null { + return this.raw.wheel_max_suspension_travel(i); + } + + /** + * Sets the maximum distance the i-th wheel suspension can travel before and after its resting length. + */ + public setWheelMaxSuspensionTravel(i: number, value: number) { + this.raw.set_wheel_max_suspension_travel(i, value); + } + + /** + * The i-th wheel’s radius. + */ + public wheelRadius(i: number): number | null { + return this.raw.wheel_radius(i); + } + + /** + * Sets the i-th wheel’s radius. + */ + public setWheelRadius(i: number, value: number) { + this.raw.set_wheel_radius(i, value); + } + + /** + * The i-th wheel’s suspension stiffness. + * + * Increase this value if the suspension appears to not push the vehicle strong enough. + */ + public wheelSuspensionStiffness(i: number): number | null { + return this.raw.wheel_suspension_stiffness(i); + } + + /** + * Sets the i-th wheel’s suspension stiffness. + * + * Increase this value if the suspension appears to not push the vehicle strong enough. + */ + public setWheelSuspensionStiffness(i: number, value: number) { + this.raw.set_wheel_suspension_stiffness(i, value); + } + + /** + * The i-th wheel’s suspension’s damping when it is being compressed. + */ + public wheelSuspensionCompression(i: number): number | null { + return this.raw.wheel_suspension_compression(i); + } + + /** + * The i-th wheel’s suspension’s damping when it is being compressed. + */ + public setWheelSuspensionCompression(i: number, value: number) { + this.raw.set_wheel_suspension_compression(i, value); + } + + /** + * The i-th wheel’s suspension’s damping when it is being released. + * + * Increase this value if the suspension appears to overshoot. + */ + public wheelSuspensionRelaxation(i: number): number | null { + return this.raw.wheel_suspension_relaxation(i); + } + + /** + * Sets the i-th wheel’s suspension’s damping when it is being released. + * + * Increase this value if the suspension appears to overshoot. + */ + public setWheelSuspensionRelaxation(i: number, value: number) { + this.raw.set_wheel_suspension_relaxation(i, value); + } + + /** + * The maximum force applied by the i-th wheel’s suspension. + */ + public wheelMaxSuspensionForce(i: number): number | null { + return this.raw.wheel_max_suspension_force(i); + } + + /** + * Sets the maximum force applied by the i-th wheel’s suspension. + */ + public setWheelMaxSuspensionForce(i: number, value: number) { + this.raw.set_wheel_max_suspension_force(i, value); + } + + /** + * The maximum amount of braking impulse applied on the i-th wheel to slow down the vehicle. + */ + public wheelBrake(i: number): number | null { + return this.raw.wheel_brake(i); + } + + /** + * Set the maximum amount of braking impulse applied on the i-th wheel to slow down the vehicle. + */ + public setWheelBrake(i: number, value: number) { + this.raw.set_wheel_brake(i, value); + } + + /** + * The steering angle (radians) for the i-th wheel. + */ + public wheelSteering(i: number): number | null { + return this.raw.wheel_steering(i); + } + + /** + * Sets the steering angle (radians) for the i-th wheel. + */ + public setWheelSteering(i: number, value: number) { + this.raw.set_wheel_steering(i, value); + } + + /** + * The forward force applied by the i-th wheel on the chassis. + */ + public wheelEngineForce(i: number): number | null { + return this.raw.wheel_engine_force(i); + } + + /** + * Sets the forward force applied by the i-th wheel on the chassis. + */ + public setWheelEngineForce(i: number, value: number) { + this.raw.set_wheel_engine_force(i, value); + } + + /** + * The direction of the i-th wheel’s suspension, relative to the chassis. + * + * The ray-casting will happen following this direction to detect the ground. + */ + public wheelDirectionCs(i: number): Vector | null { + return VectorOps.fromRaw(this.raw.wheel_direction_cs(i)); + } + + /** + * Sets the direction of the i-th wheel’s suspension, relative to the chassis. + * + * The ray-casting will happen following this direction to detect the ground. + */ + public setWheelDirectionCs(i: number, value: Vector) { + let rawValue = VectorOps.intoRaw(value); + this.raw.set_wheel_direction_cs(i, rawValue); + rawValue.free(); + } + + /** + * The i-th wheel’s axle axis, relative to the chassis. + * + * The axis index defined as 0 = X, 1 = Y, 2 = Z. + */ + public wheelAxleCs(i: number): Vector | null { + return VectorOps.fromRaw(this.raw.wheel_axle_cs(i)); + } + + /** + * Sets the i-th wheel’s axle axis, relative to the chassis. + * + * The axis index defined as 0 = X, 1 = Y, 2 = Z. + */ + public setWheelAxleCs(i: number, value: Vector) { + let rawValue = VectorOps.intoRaw(value); + this.raw.set_wheel_axle_cs(i, rawValue); + rawValue.free(); + } + + /** + * Parameter controlling how much traction the tire has. + * + * The larger the value, the more instantaneous braking will happen (with the risk of + * causing the vehicle to flip if it’s too strong). + */ + public wheelFrictionSlip(i: number): number | null { + return this.raw.wheel_friction_slip(i); + } + + /** + * Sets the parameter controlling how much traction the tire has. + * + * The larger the value, the more instantaneous braking will happen (with the risk of + * causing the vehicle to flip if it’s too strong). + */ + public setWheelFrictionSlip(i: number, value: number) { + this.raw.set_wheel_friction_slip(i, value); + } + + /** + * The multiplier of friction between a tire and the collider it’s on top of. + * + * The larger the value, the stronger side friction will be. + */ + public wheelSideFrictionStiffness(i: number): number | null { + return this.raw.wheel_side_friction_stiffness(i); + } + + /** + * The multiplier of friction between a tire and the collider it’s on top of. + * + * The larger the value, the stronger side friction will be. + */ + public setWheelSideFrictionStiffness(i: number, value: number) { + this.raw.set_wheel_side_friction_stiffness(i, value); + } + + /* + * Getters only. + */ + + /** + * The i-th wheel’s current rotation angle (radians) on its axle. + */ + public wheelRotation(i: number): number | null { + return this.raw.wheel_rotation(i); + } + + /** + * The forward impulses applied by the i-th wheel on the chassis. + */ + public wheelForwardImpulse(i: number): number | null { + return this.raw.wheel_forward_impulse(i); + } + + /** + * The side impulses applied by the i-th wheel on the chassis. + */ + public wheelSideImpulse(i: number): number | null { + return this.raw.wheel_side_impulse(i); + } + + /** + * The force applied by the i-th wheel suspension. + */ + public wheelSuspensionForce(i: number): number | null { + return this.raw.wheel_suspension_force(i); + } + + /** + * The (world-space) contact normal between the i-th wheel and the floor. + */ + public wheelContactNormal(i: number): Vector | null { + return VectorOps.fromRaw(this.raw.wheel_contact_normal_ws(i)); + } + + /** + * The (world-space) point hit by the wheel’s ray-cast for the i-th wheel. + */ + public wheelContactPoint(i: number): Vector | null { + return VectorOps.fromRaw(this.raw.wheel_contact_point_ws(i)); + } + + /** + * The suspension length for the i-th wheel. + */ + public wheelSuspensionLength(i: number): number | null { + return this.raw.wheel_suspension_length(i); + } + + /** + * The (world-space) starting point of the ray-cast for the i-th wheel. + */ + public wheelHardPoint(i: number): Vector | null { + return VectorOps.fromRaw(this.raw.wheel_hard_point_ws(i)); + } + + /** + * Is the i-th wheel in contact with the ground? + */ + public wheelIsInContact(i: number): boolean { + return this.raw.wheel_is_in_contact(i); + } + + /** + * The collider hit by the ray-cast for the i-th wheel. + */ + public wheelGroundObject(i: number): Collider | null { + return this.colliders.get(this.raw.wheel_ground_object(i)); + } +} diff --git a/packages/rapier2d/src/dynamics/ccd_solver.ts b/packages/rapier2d/src/dynamics/ccd_solver.ts new file mode 100644 index 00000000..7f794222 --- /dev/null +++ b/packages/rapier2d/src/dynamics/ccd_solver.ts @@ -0,0 +1,25 @@ +import {RawCCDSolver} from "../raw"; + +/** + * The CCD solver responsible for resolving Continuous Collision Detection. + * + * To avoid leaking WASM resources, this MUST be freed manually with `ccdSolver.free()` + * once you are done using it. + */ +export class CCDSolver { + raw: RawCCDSolver; + + /** + * Release the WASM memory occupied by this narrow-phase. + */ + public free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + } + + constructor(raw?: RawCCDSolver) { + this.raw = raw || new RawCCDSolver(); + } +} diff --git a/packages/rapier2d/src/dynamics/coefficient_combine_rule.ts b/packages/rapier2d/src/dynamics/coefficient_combine_rule.ts new file mode 100644 index 00000000..680b402a --- /dev/null +++ b/packages/rapier2d/src/dynamics/coefficient_combine_rule.ts @@ -0,0 +1,13 @@ +/** + * A rule applied to combine coefficients. + * + * Use this when configuring the `ColliderDesc` to specify + * how friction and restitution coefficient should be combined + * in a contact. + */ +export enum CoefficientCombineRule { + Average = 0, + Min = 1, + Multiply = 2, + Max = 3, +} diff --git a/packages/rapier2d/src/dynamics/impulse_joint.ts b/packages/rapier2d/src/dynamics/impulse_joint.ts new file mode 100644 index 00000000..05f0961a --- /dev/null +++ b/packages/rapier2d/src/dynamics/impulse_joint.ts @@ -0,0 +1,485 @@ +import {Rotation, Vector, VectorOps, RotationOps} from "../math"; +import { + RawGenericJoint, + RawImpulseJointSet, + RawRigidBodySet, + RawJointAxis, + RawJointType, + RawMotorModel, +} from "../raw"; +import {RigidBody, RigidBodyHandle} from "./rigid_body"; +import {RigidBodySet} from "./rigid_body_set"; + +/** + * The integer identifier of a collider added to a `ColliderSet`. + */ +export type ImpulseJointHandle = number; + +/** + * An enum grouping all possible types of joints: + * + * - `Revolute`: A revolute joint that removes all degrees of freedom between the affected + * bodies except for the rotation along one axis. + * - `Fixed`: A fixed joint that removes all relative degrees of freedom between the affected bodies. + * - `Prismatic`: A prismatic joint that removes all degrees of freedom between the affected + * bodies except for the translation along one axis. + * - `Spherical`: (3D only) A spherical joint that removes all relative linear degrees of freedom between the affected bodies. + * - `Generic`: (3D only) A joint with customizable degrees of freedom, allowing any of the 6 axes to be locked. + */ +export enum JointType { + Revolute, + Fixed, + Prismatic, + Rope, + Spring, +} + +export enum MotorModel { + AccelerationBased, + ForceBased, +} + +/** + * An enum representing the possible joint axes of a generic joint. + * They can be ORed together, like: + * JointAxesMask.LinX || JointAxesMask.LinY + * to get a joint that is only free in the X and Y translational (positional) axes. + * + * Possible free axes are: + * + * - `X`: X translation axis + * - `Y`: Y translation axis + * - `Z`: Z translation axis + * - `AngX`: X angular rotation axis + * - `AngY`: Y angular rotations axis + * - `AngZ`: Z angular rotation axis + */ +export enum JointAxesMask { + LinX = 1 << 0, + LinY = 1 << 1, + LinZ = 1 << 2, + AngX = 1 << 3, + AngY = 1 << 4, + AngZ = 1 << 5, +} + +export class ImpulseJoint { + protected rawSet: RawImpulseJointSet; // The ImpulseJoint won't need to free this. + protected bodySet: RigidBodySet; // The ImpulseJoint won’t need to free this. + handle: ImpulseJointHandle; + + constructor( + rawSet: RawImpulseJointSet, + bodySet: RigidBodySet, + handle: ImpulseJointHandle, + ) { + this.rawSet = rawSet; + this.bodySet = bodySet; + this.handle = handle; + } + + public static newTyped( + rawSet: RawImpulseJointSet, + bodySet: RigidBodySet, + handle: ImpulseJointHandle, + ): ImpulseJoint { + switch (rawSet.jointType(handle)) { + case RawJointType.Revolute: + return new RevoluteImpulseJoint(rawSet, bodySet, handle); + case RawJointType.Prismatic: + return new PrismaticImpulseJoint(rawSet, bodySet, handle); + case RawJointType.Fixed: + return new FixedImpulseJoint(rawSet, bodySet, handle); + case RawJointType.Spring: + return new SpringImpulseJoint(rawSet, bodySet, handle); + case RawJointType.Rope: + return new RopeImpulseJoint(rawSet, bodySet, handle); + default: + return new ImpulseJoint(rawSet, bodySet, handle); + } + } + + /** @internal */ + public finalizeDeserialization(bodySet: RigidBodySet) { + this.bodySet = bodySet; + } + + /** + * Checks if this joint is still valid (i.e. that it has + * not been deleted from the joint set yet). + */ + public isValid(): boolean { + return this.rawSet.contains(this.handle); + } + + /** + * The first rigid-body this joint it attached to. + */ + public body1(): RigidBody { + return this.bodySet.get(this.rawSet.jointBodyHandle1(this.handle)); + } + + /** + * The second rigid-body this joint is attached to. + */ + public body2(): RigidBody { + return this.bodySet.get(this.rawSet.jointBodyHandle2(this.handle)); + } + + /** + * The type of this joint given as a string. + */ + public type(): JointType { + return this.rawSet.jointType(this.handle) as number as JointType; + } + + + + /** + * The position of the first anchor of this joint. + * + * The first anchor gives the position of the application point on the + * local frame of the first rigid-body it is attached to. + */ + public anchor1(): Vector { + return VectorOps.fromRaw(this.rawSet.jointAnchor1(this.handle)); + } + + /** + * The position of the second anchor of this joint. + * + * The second anchor gives the position of the application point on the + * local frame of the second rigid-body it is attached to. + */ + public anchor2(): Vector { + return VectorOps.fromRaw(this.rawSet.jointAnchor2(this.handle)); + } + + /** + * Sets the position of the first anchor of this joint. + * + * The first anchor gives the position of the application point on the + * local frame of the first rigid-body it is attached to. + */ + public setAnchor1(newPos: Vector) { + const rawPoint = VectorOps.intoRaw(newPos); + this.rawSet.jointSetAnchor1(this.handle, rawPoint); + rawPoint.free(); + } + + /** + * Sets the position of the second anchor of this joint. + * + * The second anchor gives the position of the application point on the + * local frame of the second rigid-body it is attached to. + */ + public setAnchor2(newPos: Vector) { + const rawPoint = VectorOps.intoRaw(newPos); + this.rawSet.jointSetAnchor2(this.handle, rawPoint); + rawPoint.free(); + } + + /** + * Controls whether contacts are computed between colliders attached + * to the rigid-bodies linked by this joint. + */ + public setContactsEnabled(enabled: boolean) { + this.rawSet.jointSetContactsEnabled(this.handle, enabled); + } + + /** + * Indicates if contacts are enabled between colliders attached + * to the rigid-bodies linked by this joint. + */ + public contactsEnabled(): boolean { + return this.rawSet.jointContactsEnabled(this.handle); + } +} + +export class UnitImpulseJoint extends ImpulseJoint { + /** + * The axis left free by this joint. + */ + protected rawAxis?(): RawJointAxis; + + /** + * Are the limits enabled for this joint? + */ + public limitsEnabled(): boolean { + return this.rawSet.jointLimitsEnabled(this.handle, this.rawAxis()); + } + + /** + * The min limit of this joint. + */ + public limitsMin(): number { + return this.rawSet.jointLimitsMin(this.handle, this.rawAxis()); + } + + /** + * The max limit of this joint. + */ + public limitsMax(): number { + return this.rawSet.jointLimitsMax(this.handle, this.rawAxis()); + } + + /** + * Sets the limits of this joint. + * + * @param min - The minimum bound of this joint’s free coordinate. + * @param max - The maximum bound of this joint’s free coordinate. + */ + public setLimits(min: number, max: number) { + this.rawSet.jointSetLimits(this.handle, this.rawAxis(), min, max); + } + + public configureMotorModel(model: MotorModel) { + this.rawSet.jointConfigureMotorModel( + this.handle, + this.rawAxis(), + model as number as RawMotorModel, + ); + } + + public configureMotorVelocity(targetVel: number, factor: number) { + this.rawSet.jointConfigureMotorVelocity( + this.handle, + this.rawAxis(), + targetVel, + factor, + ); + } + + public configureMotorPosition( + targetPos: number, + stiffness: number, + damping: number, + ) { + this.rawSet.jointConfigureMotorPosition( + this.handle, + this.rawAxis(), + targetPos, + stiffness, + damping, + ); + } + + public configureMotor( + targetPos: number, + targetVel: number, + stiffness: number, + damping: number, + ) { + this.rawSet.jointConfigureMotor( + this.handle, + this.rawAxis(), + targetPos, + targetVel, + stiffness, + damping, + ); + } +} + +export class FixedImpulseJoint extends ImpulseJoint {} + +export class RopeImpulseJoint extends ImpulseJoint {} + +export class SpringImpulseJoint extends ImpulseJoint {} + +export class PrismaticImpulseJoint extends UnitImpulseJoint { + public rawAxis(): RawJointAxis { + return RawJointAxis.LinX; + } +} + +export class RevoluteImpulseJoint extends UnitImpulseJoint { + public rawAxis(): RawJointAxis { + return RawJointAxis.AngX; + } +} + + +export class JointData { + anchor1: Vector; + anchor2: Vector; + axis: Vector; + frame1: Rotation; + frame2: Rotation; + jointType: JointType; + limitsEnabled: boolean; + limits: Array; + axesMask: JointAxesMask; + stiffness: number; + damping: number; + length: number; + + private constructor() {} + + /** + * Creates a new joint descriptor that builds a Fixed joint. + * + * A fixed joint removes all the degrees of freedom between the affected bodies, ensuring their + * anchor and local frames coincide in world-space. + * + * @param anchor1 - Point where the joint is attached on the first rigid-body affected by this joint. Expressed in the + * local-space of the rigid-body. + * @param frame1 - The reference orientation of the joint wrt. the first rigid-body. + * @param anchor2 - Point where the joint is attached on the second rigid-body affected by this joint. Expressed in the + * local-space of the rigid-body. + * @param frame2 - The reference orientation of the joint wrt. the second rigid-body. + */ + public static fixed( + anchor1: Vector, + frame1: Rotation, + anchor2: Vector, + frame2: Rotation, + ): JointData { + let res = new JointData(); + res.anchor1 = anchor1; + res.anchor2 = anchor2; + res.frame1 = frame1; + res.frame2 = frame2; + res.jointType = JointType.Fixed; + return res; + } + + public static spring( + rest_length: number, + stiffness: number, + damping: number, + anchor1: Vector, + anchor2: Vector, + ): JointData { + let res = new JointData(); + res.anchor1 = anchor1; + res.anchor2 = anchor2; + res.length = rest_length; + res.stiffness = stiffness; + res.damping = damping; + res.jointType = JointType.Spring; + return res; + } + + public static rope( + length: number, + anchor1: Vector, + anchor2: Vector, + ): JointData { + let res = new JointData(); + res.anchor1 = anchor1; + res.anchor2 = anchor2; + res.length = length; + res.jointType = JointType.Rope; + return res; + } + + + /** + * Create a new joint descriptor that builds revolute joints. + * + * A revolute joint allows three relative rotational degrees of freedom + * by preventing any relative translation between the anchors of the + * two attached rigid-bodies. + * + * @param anchor1 - Point where the joint is attached on the first rigid-body affected by this joint. Expressed in the + * local-space of the rigid-body. + * @param anchor2 - Point where the joint is attached on the second rigid-body affected by this joint. Expressed in the + * local-space of the rigid-body. + */ + public static revolute(anchor1: Vector, anchor2: Vector): JointData { + let res = new JointData(); + res.anchor1 = anchor1; + res.anchor2 = anchor2; + res.jointType = JointType.Revolute; + return res; + } + + /** + * Creates a new joint descriptor that builds a Prismatic joint. + * + * A prismatic joint removes all the degrees of freedom between the + * affected bodies, except for the translation along one axis. + * + * @param anchor1 - Point where the joint is attached on the first rigid-body affected by this joint. Expressed in the + * local-space of the rigid-body. + * @param anchor2 - Point where the joint is attached on the second rigid-body affected by this joint. Expressed in the + * local-space of the rigid-body. + * @param axis - Axis of the joint, expressed in the local-space of the rigid-bodies it is attached to. + */ + public static prismatic( + anchor1: Vector, + anchor2: Vector, + axis: Vector, + ): JointData { + let res = new JointData(); + res.anchor1 = anchor1; + res.anchor2 = anchor2; + res.axis = axis; + res.jointType = JointType.Prismatic; + return res; + } + + + + public intoRaw(): RawGenericJoint { + let rawA1 = VectorOps.intoRaw(this.anchor1); + let rawA2 = VectorOps.intoRaw(this.anchor2); + let rawAx; + let result; + let limitsEnabled = false; + let limitsMin = 0.0; + let limitsMax = 0.0; + + switch (this.jointType) { + case JointType.Fixed: + let rawFra1 = RotationOps.intoRaw(this.frame1); + let rawFra2 = RotationOps.intoRaw(this.frame2); + result = RawGenericJoint.fixed(rawA1, rawFra1, rawA2, rawFra2); + rawFra1.free(); + rawFra2.free(); + break; + case JointType.Spring: + result = RawGenericJoint.spring( + this.length, + this.stiffness, + this.damping, + rawA1, + rawA2, + ); + break; + case JointType.Rope: + result = RawGenericJoint.rope(this.length, rawA1, rawA2); + break; + case JointType.Prismatic: + rawAx = VectorOps.intoRaw(this.axis); + + if (!!this.limitsEnabled) { + limitsEnabled = true; + limitsMin = this.limits[0]; + limitsMax = this.limits[1]; + } + + result = RawGenericJoint.prismatic( + rawA1, + rawA2, + rawAx, + limitsEnabled, + limitsMin, + limitsMax, + ); + + + rawAx.free(); + break; + case JointType.Revolute: + result = RawGenericJoint.revolute(rawA1, rawA2); + break; + } + + rawA1.free(); + rawA2.free(); + + return result; + } +} diff --git a/packages/rapier2d/src/dynamics/impulse_joint_set.ts b/packages/rapier2d/src/dynamics/impulse_joint_set.ts new file mode 100644 index 00000000..015d06b3 --- /dev/null +++ b/packages/rapier2d/src/dynamics/impulse_joint_set.ts @@ -0,0 +1,162 @@ +import {RawImpulseJointSet} from "../raw"; +import {Coarena} from "../coarena"; +import {RigidBodySet} from "./rigid_body_set"; +import { + RevoluteImpulseJoint, + FixedImpulseJoint, + ImpulseJoint, + ImpulseJointHandle, + JointData, + JointType, + PrismaticImpulseJoint, +} from "./impulse_joint"; +import {IslandManager} from "./island_manager"; +import {RigidBodyHandle} from "./rigid_body"; +import {Collider, ColliderHandle} from "../geometry"; + +/** + * A set of joints. + * + * To avoid leaking WASM resources, this MUST be freed manually with `jointSet.free()` + * once you are done using it (and all the joints it created). + */ +export class ImpulseJointSet { + raw: RawImpulseJointSet; + private map: Coarena; + + /** + * Release the WASM memory occupied by this joint set. + */ + public free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + + if (!!this.map) { + this.map.clear(); + } + this.map = undefined; + } + + constructor(raw?: RawImpulseJointSet) { + this.raw = raw || new RawImpulseJointSet(); + this.map = new Coarena(); + // Initialize the map with the existing elements, if any. + if (raw) { + raw.forEachJointHandle((handle: ImpulseJointHandle) => { + this.map.set(handle, ImpulseJoint.newTyped(raw, null, handle)); + }); + } + } + + /** @internal */ + public finalizeDeserialization(bodies: RigidBodySet) { + this.map.forEach((joint) => joint.finalizeDeserialization(bodies)); + } + + /** + * Creates a new joint and return its integer handle. + * + * @param bodies - The set of rigid-bodies containing the bodies the joint is attached to. + * @param desc - The joint's parameters. + * @param parent1 - The handle of the first rigid-body this joint is attached to. + * @param parent2 - The handle of the second rigid-body this joint is attached to. + * @param wakeUp - Should the attached rigid-bodies be awakened? + */ + public createJoint( + bodies: RigidBodySet, + desc: JointData, + parent1: RigidBodyHandle, + parent2: RigidBodyHandle, + wakeUp: boolean, + ): ImpulseJoint { + const rawParams = desc.intoRaw(); + const handle = this.raw.createJoint( + rawParams, + parent1, + parent2, + wakeUp, + ); + rawParams.free(); + let joint = ImpulseJoint.newTyped(this.raw, bodies, handle); + this.map.set(handle, joint); + return joint; + } + + /** + * Remove a joint from this set. + * + * @param handle - The integer handle of the joint. + * @param wakeUp - If `true`, the rigid-bodies attached by the removed joint will be woken-up automatically. + */ + public remove(handle: ImpulseJointHandle, wakeUp: boolean) { + this.raw.remove(handle, wakeUp); + this.unmap(handle); + } + + /** + * Calls the given closure with the integer handle of each impulse joint attached to this rigid-body. + * + * @param f - The closure called with the integer handle of each impulse joint attached to the rigid-body. + */ + public forEachJointHandleAttachedToRigidBody( + handle: RigidBodyHandle, + f: (handle: ImpulseJointHandle) => void, + ) { + this.raw.forEachJointAttachedToRigidBody(handle, f); + } + + /** + * Internal function, do not call directly. + * @param handle + */ + public unmap(handle: ImpulseJointHandle) { + this.map.delete(handle); + } + + /** + * The number of joints on this set. + */ + public len(): number { + return this.map.len(); + } + + /** + * Does this set contain a joint with the given handle? + * + * @param handle - The joint handle to check. + */ + public contains(handle: ImpulseJointHandle): boolean { + return this.get(handle) != null; + } + + /** + * Gets the joint with the given handle. + * + * Returns `null` if no joint with the specified handle exists. + * + * @param handle - The integer handle of the joint to retrieve. + */ + public get(handle: ImpulseJointHandle): ImpulseJoint | null { + return this.map.get(handle); + } + + /** + * Applies the given closure to each joint contained by this set. + * + * @param f - The closure to apply. + */ + public forEach(f: (joint: ImpulseJoint) => void) { + this.map.forEach(f); + } + + /** + * Gets all joints in the list. + * + * @returns joint list. + */ + public getAll(): ImpulseJoint[] { + return this.map.getAll(); + } +} diff --git a/packages/rapier2d/src/dynamics/index.ts b/packages/rapier2d/src/dynamics/index.ts new file mode 100644 index 00000000..4141658e --- /dev/null +++ b/packages/rapier2d/src/dynamics/index.ts @@ -0,0 +1,10 @@ +export * from "./rigid_body"; +export * from "./rigid_body_set"; +export * from "./integration_parameters"; +export * from "./impulse_joint"; +export * from "./impulse_joint_set"; +export * from "./multibody_joint"; +export * from "./multibody_joint_set"; +export * from "./coefficient_combine_rule"; +export * from "./ccd_solver"; +export * from "./island_manager"; diff --git a/packages/rapier2d/src/dynamics/integration_parameters.ts b/packages/rapier2d/src/dynamics/integration_parameters.ts new file mode 100644 index 00000000..b7253e21 --- /dev/null +++ b/packages/rapier2d/src/dynamics/integration_parameters.ts @@ -0,0 +1,126 @@ +import {RawIntegrationParameters} from "../raw"; + +export class IntegrationParameters { + raw: RawIntegrationParameters; + + constructor(raw?: RawIntegrationParameters) { + this.raw = raw || new RawIntegrationParameters(); + } + + /** + * Free the WASM memory used by these integration parameters. + */ + public free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + } + + /** + * The timestep length (default: `1.0 / 60.0`) + */ + get dt(): number { + return this.raw.dt; + } + + /** + * The Error Reduction Parameter in `[0, 1]` is the proportion of + * the positional error to be corrected at each time step (default: `0.2`). + */ + get contact_erp(): number { + return this.raw.contact_erp; + } + + get lengthUnit(): number { + return this.raw.lengthUnit; + } + + /** + * Normalized amount of penetration the engine won’t attempt to correct (default: `0.001m`). + * + * This threshold considered by the physics engine is this value multiplied by the `lengthUnit`. + */ + get normalizedAllowedLinearError(): number { + return this.raw.normalizedAllowedLinearError; + } + + /** + * The maximal normalized distance separating two objects that will generate predictive contacts (default: `0.002`). + * + * This threshold considered by the physics engine is this value multiplied by the `lengthUnit`. + */ + get normalizedPredictionDistance(): number { + return this.raw.normalizedPredictionDistance; + } + + /** + * The number of solver iterations run by the constraints solver for calculating forces (default: `4`). + */ + get numSolverIterations(): number { + return this.raw.numSolverIterations; + } + + /** + * Number of internal Project Gauss Seidel (PGS) iterations run at each solver iteration (default: `1`). + */ + get numInternalPgsIterations(): number { + return this.raw.numInternalPgsIterations; + } + + /** + * Minimum number of dynamic bodies in each active island (default: `128`). + */ + get minIslandSize(): number { + return this.raw.minIslandSize; + } + + /** + * Maximum number of substeps performed by the solver (default: `1`). + */ + get maxCcdSubsteps(): number { + return this.raw.maxCcdSubsteps; + } + + set dt(value: number) { + this.raw.dt = value; + } + + set contact_natural_frequency(value: number) { + this.raw.contact_natural_frequency = value; + } + + set lengthUnit(value: number) { + this.raw.lengthUnit = value; + } + + set normalizedAllowedLinearError(value: number) { + this.raw.normalizedAllowedLinearError = value; + } + + set normalizedPredictionDistance(value: number) { + this.raw.normalizedPredictionDistance = value; + } + + /** + * Sets the number of solver iterations run by the constraints solver for calculating forces (default: `4`). + */ + set numSolverIterations(value: number) { + this.raw.numSolverIterations = value; + } + + /** + * Sets the number of internal Project Gauss Seidel (PGS) iterations run at each solver iteration (default: `1`). + */ + set numInternalPgsIterations(value: number) { + this.raw.numInternalPgsIterations = value; + } + + set minIslandSize(value: number) { + this.raw.minIslandSize = value; + } + + set maxCcdSubsteps(value: number) { + this.raw.maxCcdSubsteps = value; + } +} diff --git a/packages/rapier2d/src/dynamics/island_manager.ts b/packages/rapier2d/src/dynamics/island_manager.ts new file mode 100644 index 00000000..65ed21dc --- /dev/null +++ b/packages/rapier2d/src/dynamics/island_manager.ts @@ -0,0 +1,37 @@ +import {RawIslandManager} from "../raw"; +import {RigidBodyHandle} from "./rigid_body"; + +/** + * The CCD solver responsible for resolving Continuous Collision Detection. + * + * To avoid leaking WASM resources, this MUST be freed manually with `ccdSolver.free()` + * once you are done using it. + */ +export class IslandManager { + raw: RawIslandManager; + + /** + * Release the WASM memory occupied by this narrow-phase. + */ + public free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + } + + constructor(raw?: RawIslandManager) { + this.raw = raw || new RawIslandManager(); + } + + /** + * Applies the given closure to the handle of each active rigid-bodies contained by this set. + * + * A rigid-body is active if it is not sleeping, i.e., if it moved recently. + * + * @param f - The closure to apply. + */ + public forEachActiveRigidBodyHandle(f: (handle: RigidBodyHandle) => void) { + this.raw.forEachActiveRigidBodyHandle(f); + } +} diff --git a/packages/rapier2d/src/dynamics/multibody_joint.ts b/packages/rapier2d/src/dynamics/multibody_joint.ts new file mode 100644 index 00000000..fd97ad5f --- /dev/null +++ b/packages/rapier2d/src/dynamics/multibody_joint.ts @@ -0,0 +1,190 @@ +import { + RawImpulseJointSet, + RawJointAxis, + RawJointType, + RawMultibodyJointSet, +} from "../raw"; +import { + FixedImpulseJoint, + ImpulseJointHandle, + JointType, + MotorModel, + PrismaticImpulseJoint, + RevoluteImpulseJoint, +} from "./impulse_joint"; + + +/** + * The integer identifier of a collider added to a `ColliderSet`. + */ +export type MultibodyJointHandle = number; + +export class MultibodyJoint { + protected rawSet: RawMultibodyJointSet; // The MultibodyJoint won't need to free this. + handle: MultibodyJointHandle; + + constructor(rawSet: RawMultibodyJointSet, handle: MultibodyJointHandle) { + this.rawSet = rawSet; + this.handle = handle; + } + + public static newTyped( + rawSet: RawMultibodyJointSet, + handle: MultibodyJointHandle, + ): MultibodyJoint { + switch (rawSet.jointType(handle)) { + case RawJointType.Revolute: + return new RevoluteMultibodyJoint(rawSet, handle); + case RawJointType.Prismatic: + return new PrismaticMultibodyJoint(rawSet, handle); + case RawJointType.Fixed: + return new FixedMultibodyJoint(rawSet, handle); + default: + return new MultibodyJoint(rawSet, handle); + } + } + + /** + * Checks if this joint is still valid (i.e. that it has + * not been deleted from the joint set yet). + */ + public isValid(): boolean { + return this.rawSet.contains(this.handle); + } + + // /** + // * The unique integer identifier of the first rigid-body this joint it attached to. + // */ + // public bodyHandle1(): RigidBodyHandle { + // return this.rawSet.jointBodyHandle1(this.handle); + // } + // + // /** + // * The unique integer identifier of the second rigid-body this joint is attached to. + // */ + // public bodyHandle2(): RigidBodyHandle { + // return this.rawSet.jointBodyHandle2(this.handle); + // } + // + // /** + // * The type of this joint given as a string. + // */ + // public type(): JointType { + // return this.rawSet.jointType(this.handle); + // } + // + // // #if DIM3 + // /** + // * The rotation quaternion that aligns this joint's first local axis to the `x` axis. + // */ + // public frameX1(): Rotation { + // return RotationOps.fromRaw(this.rawSet.jointFrameX1(this.handle)); + // } + // + // // #endif + // + // // #if DIM3 + // /** + // * The rotation matrix that aligns this joint's second local axis to the `x` axis. + // */ + // public frameX2(): Rotation { + // return RotationOps.fromRaw(this.rawSet.jointFrameX2(this.handle)); + // } + // + // // #endif + // + // /** + // * The position of the first anchor of this joint. + // * + // * The first anchor gives the position of the points application point on the + // * local frame of the first rigid-body it is attached to. + // */ + // public anchor1(): Vector { + // return VectorOps.fromRaw(this.rawSet.jointAnchor1(this.handle)); + // } + // + // /** + // * The position of the second anchor of this joint. + // * + // * The second anchor gives the position of the points application point on the + // * local frame of the second rigid-body it is attached to. + // */ + // public anchor2(): Vector { + // return VectorOps.fromRaw(this.rawSet.jointAnchor2(this.handle)); + // } + + /** + * Controls whether contacts are computed between colliders attached + * to the rigid-bodies linked by this joint. + */ + public setContactsEnabled(enabled: boolean) { + this.rawSet.jointSetContactsEnabled(this.handle, enabled); + } + + /** + * Indicates if contacts are enabled between colliders attached + * to the rigid-bodies linked by this joint. + */ + public contactsEnabled(): boolean { + return this.rawSet.jointContactsEnabled(this.handle); + } +} + +export class UnitMultibodyJoint extends MultibodyJoint { + /** + * The axis left free by this joint. + */ + protected rawAxis?(): RawJointAxis; + + // /** + // * Are the limits enabled for this joint? + // */ + // public limitsEnabled(): boolean { + // return this.rawSet.jointLimitsEnabled(this.handle, this.rawAxis()); + // } + // + // /** + // * The min limit of this joint. + // */ + // public limitsMin(): number { + // return this.rawSet.jointLimitsMin(this.handle, this.rawAxis()); + // } + // + // /** + // * The max limit of this joint. + // */ + // public limitsMax(): number { + // return this.rawSet.jointLimitsMax(this.handle, this.rawAxis()); + // } + // + // public configureMotorModel(model: MotorModel) { + // this.rawSet.jointConfigureMotorModel(this.handle, this.rawAxis(), model); + // } + // + // public configureMotorVelocity(targetVel: number, factor: number) { + // this.rawSet.jointConfigureMotorVelocity(this.handle, this.rawAxis(), targetVel, factor); + // } + // + // public configureMotorPosition(targetPos: number, stiffness: number, damping: number) { + // this.rawSet.jointConfigureMotorPosition(this.handle, this.rawAxis(), targetPos, stiffness, damping); + // } + // + // public configureMotor(targetPos: number, targetVel: number, stiffness: number, damping: number) { + // this.rawSet.jointConfigureMotor(this.handle, this.rawAxis(), targetPos, targetVel, stiffness, damping); + // } +} + +export class FixedMultibodyJoint extends MultibodyJoint {} + +export class PrismaticMultibodyJoint extends UnitMultibodyJoint { + public rawAxis(): RawJointAxis { + return RawJointAxis.LinX; + } +} + +export class RevoluteMultibodyJoint extends UnitMultibodyJoint { + public rawAxis(): RawJointAxis { + return RawJointAxis.AngX; + } +} + diff --git a/packages/rapier2d/src/dynamics/multibody_joint_set.ts b/packages/rapier2d/src/dynamics/multibody_joint_set.ts new file mode 100644 index 00000000..eee5985a --- /dev/null +++ b/packages/rapier2d/src/dynamics/multibody_joint_set.ts @@ -0,0 +1,154 @@ +import {RawMultibodyJointSet} from "../raw"; +import {Coarena} from "../coarena"; +import {RigidBodySet} from "./rigid_body_set"; +import { + MultibodyJoint, + MultibodyJointHandle, + RevoluteMultibodyJoint, + FixedMultibodyJoint, + PrismaticMultibodyJoint, +} from "./multibody_joint"; +import {ImpulseJointHandle, JointData, JointType} from "./impulse_joint"; +import {IslandManager} from "./island_manager"; +import {ColliderHandle} from "../geometry"; +import {RigidBodyHandle} from "./rigid_body"; + +/** + * A set of joints. + * + * To avoid leaking WASM resources, this MUST be freed manually with `jointSet.free()` + * once you are done using it (and all the joints it created). + */ +export class MultibodyJointSet { + raw: RawMultibodyJointSet; + private map: Coarena; + + /** + * Release the WASM memory occupied by this joint set. + */ + public free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + + if (!!this.map) { + this.map.clear(); + } + this.map = undefined; + } + + constructor(raw?: RawMultibodyJointSet) { + this.raw = raw || new RawMultibodyJointSet(); + this.map = new Coarena(); + // Initialize the map with the existing elements, if any. + if (raw) { + raw.forEachJointHandle((handle: MultibodyJointHandle) => { + this.map.set(handle, MultibodyJoint.newTyped(this.raw, handle)); + }); + } + } + + /** + * Creates a new joint and return its integer handle. + * + * @param desc - The joint's parameters. + * @param parent1 - The handle of the first rigid-body this joint is attached to. + * @param parent2 - The handle of the second rigid-body this joint is attached to. + * @param wakeUp - Should the attached rigid-bodies be awakened? + */ + public createJoint( + desc: JointData, + parent1: RigidBodyHandle, + parent2: RigidBodyHandle, + wakeUp: boolean, + ): MultibodyJoint { + const rawParams = desc.intoRaw(); + const handle = this.raw.createJoint( + rawParams, + parent1, + parent2, + wakeUp, + ); + rawParams.free(); + let joint = MultibodyJoint.newTyped(this.raw, handle); + this.map.set(handle, joint); + return joint; + } + + /** + * Remove a joint from this set. + * + * @param handle - The integer handle of the joint. + * @param wake_up - If `true`, the rigid-bodies attached by the removed joint will be woken-up automatically. + */ + public remove(handle: MultibodyJointHandle, wake_up: boolean) { + this.raw.remove(handle, wake_up); + this.map.delete(handle); + } + + /** + * Internal function, do not call directly. + * @param handle + */ + public unmap(handle: MultibodyJointHandle) { + this.map.delete(handle); + } + + /** + * The number of joints on this set. + */ + public len(): number { + return this.map.len(); + } + + /** + * Does this set contain a joint with the given handle? + * + * @param handle - The joint handle to check. + */ + public contains(handle: MultibodyJointHandle): boolean { + return this.get(handle) != null; + } + + /** + * Gets the joint with the given handle. + * + * Returns `null` if no joint with the specified handle exists. + * + * @param handle - The integer handle of the joint to retrieve. + */ + public get(handle: MultibodyJointHandle): MultibodyJoint | null { + return this.map.get(handle); + } + + /** + * Applies the given closure to each joint contained by this set. + * + * @param f - The closure to apply. + */ + public forEach(f: (joint: MultibodyJoint) => void) { + this.map.forEach(f); + } + + /** + * Calls the given closure with the integer handle of each multibody joint attached to this rigid-body. + * + * @param f - The closure called with the integer handle of each multibody joint attached to the rigid-body. + */ + public forEachJointHandleAttachedToRigidBody( + handle: RigidBodyHandle, + f: (handle: MultibodyJointHandle) => void, + ) { + this.raw.forEachJointAttachedToRigidBody(handle, f); + } + + /** + * Gets all joints in the list. + * + * @returns joint list. + */ + public getAll(): MultibodyJoint[] { + return this.map.getAll(); + } +} diff --git a/packages/rapier2d/src/dynamics/rigid_body.ts b/packages/rapier2d/src/dynamics/rigid_body.ts new file mode 100644 index 00000000..2d8c1f04 --- /dev/null +++ b/packages/rapier2d/src/dynamics/rigid_body.ts @@ -0,0 +1,1162 @@ +import {RawRigidBodySet, RawRigidBodyType} from "../raw"; +import {Rotation, RotationOps, Vector, VectorOps} from "../math"; +import {Collider, ColliderSet} from "../geometry"; + +/** + * The integer identifier of a collider added to a `ColliderSet`. + */ +export type RigidBodyHandle = number; + +/** + * The simulation status of a rigid-body. + */ +// TODO: rename this to RigidBodyType +export enum RigidBodyType { + /** + * A `RigidBodyType::Dynamic` body can be affected by all external forces. + */ + Dynamic = 0, + /** + * A `RigidBodyType::Fixed` body cannot be affected by external forces. + */ + Fixed, + /** + * A `RigidBodyType::KinematicPositionBased` body cannot be affected by any external forces but can be controlled + * by the user at the position level while keeping realistic one-way interaction with dynamic bodies. + * + * One-way interaction means that a kinematic body can push a dynamic body, but a kinematic body + * cannot be pushed by anything. In other words, the trajectory of a kinematic body can only be + * modified by the user and is independent from any contact or joint it is involved in. + */ + KinematicPositionBased, + /** + * A `RigidBodyType::KinematicVelocityBased` body cannot be affected by any external forces but can be controlled + * by the user at the velocity level while keeping realistic one-way interaction with dynamic bodies. + * + * One-way interaction means that a kinematic body can push a dynamic body, but a kinematic body + * cannot be pushed by anything. In other words, the trajectory of a kinematic body can only be + * modified by the user and is independent from any contact or joint it is involved in. + */ + KinematicVelocityBased, +} + +/** + * A rigid-body. + */ +export class RigidBody { + private rawSet: RawRigidBodySet; // The RigidBody won't need to free this. + private colliderSet: ColliderSet; + readonly handle: RigidBodyHandle; + + /** + * An arbitrary user-defined object associated with this rigid-body. + */ + public userData?: unknown; + + constructor( + rawSet: RawRigidBodySet, + colliderSet: ColliderSet, + handle: RigidBodyHandle, + ) { + this.rawSet = rawSet; + this.colliderSet = colliderSet; + this.handle = handle; + } + + /** @internal */ + public finalizeDeserialization(colliderSet: ColliderSet) { + this.colliderSet = colliderSet; + } + + /** + * Checks if this rigid-body is still valid (i.e. that it has + * not been deleted from the rigid-body set yet. + */ + public isValid(): boolean { + return this.rawSet.contains(this.handle); + } + + /** + * Locks or unlocks the ability of this rigid-body to translate. + * + * @param locked - If `true`, this rigid-body will no longer translate due to forces and impulses. + * @param wakeUp - If `true`, this rigid-body will be automatically awaken if it is currently asleep. + */ + public lockTranslations(locked: boolean, wakeUp: boolean) { + return this.rawSet.rbLockTranslations(this.handle, locked, wakeUp); + } + + /** + * Locks or unlocks the ability of this rigid-body to rotate. + * + * @param locked - If `true`, this rigid-body will no longer rotate due to torques and impulses. + * @param wakeUp - If `true`, this rigid-body will be automatically awaken if it is currently asleep. + */ + public lockRotations(locked: boolean, wakeUp: boolean) { + return this.rawSet.rbLockRotations(this.handle, locked, wakeUp); + } + + /** + * Locks or unlocks the ability of this rigid-body to translation along individual coordinate axes. + * + * @param enableX - If `false`, this rigid-body will no longer rotate due to torques and impulses, along the X coordinate axis. + * @param enableY - If `false`, this rigid-body will no longer rotate due to torques and impulses, along the Y coordinate axis. + * @param wakeUp - If `true`, this rigid-body will be automatically awaken if it is currently asleep. + */ + public setEnabledTranslations( + enableX: boolean, + enableY: boolean, + wakeUp: boolean, + ) { + return this.rawSet.rbSetEnabledTranslations( + this.handle, + enableX, + enableY, + wakeUp, + ); + } + + /** + * Locks or unlocks the ability of this rigid-body to translation along individual coordinate axes. + * + * @param enableX - If `false`, this rigid-body will no longer rotate due to torques and impulses, along the X coordinate axis. + * @param enableY - If `false`, this rigid-body will no longer rotate due to torques and impulses, along the Y coordinate axis. + * @param wakeUp - If `true`, this rigid-body will be automatically awaken if it is currently asleep. + * @deprecated use `this.setEnabledTranslations` with the same arguments instead. + */ + public restrictTranslations( + enableX: boolean, + enableY: boolean, + wakeUp: boolean, + ) { + this.setEnabledTranslations(enableX, enableX, wakeUp); + } + + + /** + * The dominance group, in [-127, +127] this rigid-body is part of. + */ + public dominanceGroup(): number { + return this.rawSet.rbDominanceGroup(this.handle); + } + + /** + * Sets the dominance group of this rigid-body. + * + * @param group - The dominance group of this rigid-body. Must be a signed integer in the range [-127, +127]. + */ + public setDominanceGroup(group: number) { + this.rawSet.rbSetDominanceGroup(this.handle, group); + } + + /** + * The number of additional solver iterations that will be run for this + * rigid-body and everything that interacts with it directly or indirectly + * through contacts or joints. + */ + public additionalSolverIterations(): number { + return this.rawSet.rbAdditionalSolverIterations(this.handle); + } + + /** + * Sets the number of additional solver iterations that will be run for this + * rigid-body and everything that interacts with it directly or indirectly + * through contacts or joints. + * + * Compared to increasing the global `World.numSolverIteration`, setting this + * value lets you increase accuracy on only a subset of the scene, resulting in reduced + * performance loss. + * + * @param iters - The new number of additional solver iterations (default: 0). + */ + public setAdditionalSolverIterations(iters: number) { + this.rawSet.rbSetAdditionalSolverIterations(this.handle, iters); + } + + /** + * Enable or disable CCD (Continuous Collision Detection) for this rigid-body. + * + * @param enabled - If `true`, CCD will be enabled for this rigid-body. + */ + public enableCcd(enabled: boolean) { + this.rawSet.rbEnableCcd(this.handle, enabled); + } + + /** + * Sets the soft-CCD prediction distance for this rigid-body. + * + * See the documentation of `RigidBodyDesc.setSoftCcdPrediction` for + * additional details. + */ + public setSoftCcdPrediction(distance: number) { + this.rawSet.rbSetSoftCcdPrediction(this.handle, distance); + } + + /** + * Gets the soft-CCD prediction distance for this rigid-body. + * + * See the documentation of `RigidBodyDesc.setSoftCcdPrediction` for + * additional details. + */ + public softCcdPrediction(): number { + return this.rawSet.rbSoftCcdPrediction(this.handle); + } + + /** + * The world-space translation of this rigid-body. + */ + public translation(): Vector { + let res = this.rawSet.rbTranslation(this.handle); + return VectorOps.fromRaw(res); + } + + /** + * The world-space orientation of this rigid-body. + */ + public rotation(): Rotation { + let res = this.rawSet.rbRotation(this.handle); + return RotationOps.fromRaw(res); + } + + /** + * The world-space next translation of this rigid-body. + * + * If this rigid-body is kinematic this value is set by the `setNextKinematicTranslation` + * method and is used for estimating the kinematic body velocity at the next timestep. + * For non-kinematic bodies, this value is currently unspecified. + */ + public nextTranslation(): Vector { + let res = this.rawSet.rbNextTranslation(this.handle); + return VectorOps.fromRaw(res); + } + + /** + * The world-space next orientation of this rigid-body. + * + * If this rigid-body is kinematic this value is set by the `setNextKinematicRotation` + * method and is used for estimating the kinematic body velocity at the next timestep. + * For non-kinematic bodies, this value is currently unspecified. + */ + public nextRotation(): Rotation { + let res = this.rawSet.rbNextRotation(this.handle); + return RotationOps.fromRaw(res); + } + + /** + * Sets the translation of this rigid-body. + * + * @param tra - The world-space position of the rigid-body. + * @param wakeUp - Forces the rigid-body to wake-up so it is properly affected by forces if it + * wasn't moving before modifying its position. + */ + public setTranslation(tra: Vector, wakeUp: boolean) { + this.rawSet.rbSetTranslation(this.handle, tra.x, tra.y, wakeUp); + } + + /** + * Sets the linear velocity of this rigid-body. + * + * @param vel - The linear velocity to set. + * @param wakeUp - Forces the rigid-body to wake-up if it was asleep. + */ + public setLinvel(vel: Vector, wakeUp: boolean) { + let rawVel = VectorOps.intoRaw(vel); + this.rawSet.rbSetLinvel(this.handle, rawVel, wakeUp); + rawVel.free(); + } + + /** + * The scale factor applied to the gravity affecting + * this rigid-body. + */ + public gravityScale(): number { + return this.rawSet.rbGravityScale(this.handle); + } + + /** + * Sets the scale factor applied to the gravity affecting + * this rigid-body. + * + * @param factor - The scale factor to set. A value of 0.0 means + * that this rigid-body will on longer be affected by gravity. + * @param wakeUp - Forces the rigid-body to wake-up if it was asleep. + */ + public setGravityScale(factor: number, wakeUp: boolean) { + this.rawSet.rbSetGravityScale(this.handle, factor, wakeUp); + } + + + /** + * Sets the rotation angle of this rigid-body. + * + * @param angle - The rotation angle, in radians. + * @param wakeUp - Forces the rigid-body to wake-up so it is properly affected by forces if it + * wasn't moving before modifying its position. + */ + public setRotation(angle: number, wakeUp: boolean) { + this.rawSet.rbSetRotation(this.handle, angle, wakeUp); + } + + /** + * Sets the angular velocity fo this rigid-body. + * + * @param vel - The angular velocity to set. + * @param wakeUp - Forces the rigid-body to wake-up if it was asleep. + */ + public setAngvel(vel: number, wakeUp: boolean) { + this.rawSet.rbSetAngvel(this.handle, vel, wakeUp); + } + + + /** + * If this rigid body is kinematic, sets its future translation after the next timestep integration. + * + * This should be used instead of `rigidBody.setTranslation` to make the dynamic object + * interacting with this kinematic body behave as expected. Internally, Rapier will compute + * an artificial velocity for this rigid-body from its current position and its next kinematic + * position. This velocity will be used to compute forces on dynamic bodies interacting with + * this body. + * + * @param t - The kinematic translation to set. + */ + public setNextKinematicTranslation(t: Vector) { + this.rawSet.rbSetNextKinematicTranslation(this.handle, t.x, t.y); + } + + + /** + * If this rigid body is kinematic, sets its future rotation after the next timestep integration. + * + * This should be used instead of `rigidBody.setRotation` to make the dynamic object + * interacting with this kinematic body behave as expected. Internally, Rapier will compute + * an artificial velocity for this rigid-body from its current position and its next kinematic + * position. This velocity will be used to compute forces on dynamic bodies interacting with + * this body. + * + * @param angle - The kinematic rotation angle, in radians. + */ + public setNextKinematicRotation(angle: number) { + this.rawSet.rbSetNextKinematicRotation(this.handle, angle); + } + + + /** + * The linear velocity of this rigid-body. + */ + public linvel(): Vector { + return VectorOps.fromRaw(this.rawSet.rbLinvel(this.handle)); + } + + /** + * The velocity of the given world-space point on this rigid-body. + */ + public velocityAtPoint(point: Vector): Vector { + const rawPoint = VectorOps.intoRaw(point); + let result = VectorOps.fromRaw( + this.rawSet.rbVelocityAtPoint(this.handle, rawPoint), + ); + rawPoint.free(); + return result; + } + + + /** + * The angular velocity of this rigid-body. + */ + public angvel(): number { + return this.rawSet.rbAngvel(this.handle); + } + + + /** + * The mass of this rigid-body. + */ + public mass(): number { + return this.rawSet.rbMass(this.handle); + } + + /** + * The inverse mass taking into account translation locking. + */ + public effectiveInvMass(): Vector { + return VectorOps.fromRaw(this.rawSet.rbEffectiveInvMass(this.handle)); + } + + /** + * The inverse of the mass of a rigid-body. + * + * If this is zero, the rigid-body is assumed to have infinite mass. + */ + public invMass(): number { + return this.rawSet.rbInvMass(this.handle); + } + + /** + * The center of mass of a rigid-body expressed in its local-space. + */ + public localCom(): Vector { + return VectorOps.fromRaw(this.rawSet.rbLocalCom(this.handle)); + } + + /** + * The world-space center of mass of the rigid-body. + */ + public worldCom(): Vector { + return VectorOps.fromRaw(this.rawSet.rbWorldCom(this.handle)); + } + + /** + * The inverse of the principal angular inertia of the rigid-body. + * + * Components set to zero are assumed to be infinite along the corresponding principal axis. + */ + public invPrincipalInertia(): number { + return this.rawSet.rbInvPrincipalInertia(this.handle); + } + + + + /** + * The angular inertia along the principal inertia axes of the rigid-body. + */ + public principalInertia(): number { + return this.rawSet.rbPrincipalInertia(this.handle); + } + + + + + /** + * The world-space inverse angular inertia tensor of the rigid-body, + * taking into account rotation locking. + */ + public effectiveWorldInvInertia(): number { + return this.rawSet.rbEffectiveWorldInvInertia(this.handle); + } + + + + /** + * The effective world-space angular inertia (that takes the potential rotation locking into account) of + * this rigid-body. + */ + public effectiveAngularInertia(): number { + return this.rawSet.rbEffectiveAngularInertia(this.handle); + } + + + + /** + * Put this rigid body to sleep. + * + * A sleeping body no longer moves and is no longer simulated by the physics engine unless + * it is waken up. It can be woken manually with `this.wakeUp()` or automatically due to + * external forces like contacts. + */ + public sleep() { + this.rawSet.rbSleep(this.handle); + } + + /** + * Wakes this rigid-body up. + * + * A dynamic rigid-body that does not move during several consecutive frames will + * be put to sleep by the physics engine, i.e., it will stop being simulated in order + * to avoid useless computations. + * This methods forces a sleeping rigid-body to wake-up. This is useful, e.g., before modifying + * the position of a dynamic body so that it is properly simulated afterwards. + */ + public wakeUp() { + this.rawSet.rbWakeUp(this.handle); + } + + /** + * Is CCD enabled for this rigid-body? + */ + public isCcdEnabled(): boolean { + return this.rawSet.rbIsCcdEnabled(this.handle); + } + + /** + * The number of colliders attached to this rigid-body. + */ + public numColliders(): number { + return this.rawSet.rbNumColliders(this.handle); + } + + /** + * Retrieves the `i-th` collider attached to this rigid-body. + * + * @param i - The index of the collider to retrieve. Must be a number in `[0, this.numColliders()[`. + * This index is **not** the same as the unique identifier of the collider. + */ + public collider(i: number): Collider { + return this.colliderSet.get(this.rawSet.rbCollider(this.handle, i)); + } + + /** + * Sets whether this rigid-body is enabled or not. + * + * @param enabled - Set to `false` to disable this rigid-body and all its attached colliders. + */ + public setEnabled(enabled: boolean) { + this.rawSet.rbSetEnabled(this.handle, enabled); + } + + /** + * Is this rigid-body enabled? + */ + public isEnabled(): boolean { + return this.rawSet.rbIsEnabled(this.handle); + } + + /** + * The status of this rigid-body: static, dynamic, or kinematic. + */ + public bodyType(): RigidBodyType { + return this.rawSet.rbBodyType(this.handle) as number as RigidBodyType; + } + + /** + * Set a new status for this rigid-body: static, dynamic, or kinematic. + */ + public setBodyType(type: RigidBodyType, wakeUp: boolean) { + return this.rawSet.rbSetBodyType( + this.handle, + type as number as RawRigidBodyType, + wakeUp, + ); + } + + /** + * Is this rigid-body sleeping? + */ + public isSleeping(): boolean { + return this.rawSet.rbIsSleeping(this.handle); + } + + /** + * Is the velocity of this rigid-body not zero? + */ + public isMoving(): boolean { + return this.rawSet.rbIsMoving(this.handle); + } + + /** + * Is this rigid-body static? + */ + public isFixed(): boolean { + return this.rawSet.rbIsFixed(this.handle); + } + + /** + * Is this rigid-body kinematic? + */ + public isKinematic(): boolean { + return this.rawSet.rbIsKinematic(this.handle); + } + + /** + * Is this rigid-body dynamic? + */ + public isDynamic(): boolean { + return this.rawSet.rbIsDynamic(this.handle); + } + + /** + * The linear damping coefficient of this rigid-body. + */ + public linearDamping(): number { + return this.rawSet.rbLinearDamping(this.handle); + } + + /** + * The angular damping coefficient of this rigid-body. + */ + public angularDamping(): number { + return this.rawSet.rbAngularDamping(this.handle); + } + + /** + * Sets the linear damping factor applied to this rigid-body. + * + * @param factor - The damping factor to set. + */ + public setLinearDamping(factor: number) { + this.rawSet.rbSetLinearDamping(this.handle, factor); + } + + /** + * Recompute the mass-properties of this rigid-bodies based on its currently attached colliders. + */ + public recomputeMassPropertiesFromColliders() { + this.rawSet.rbRecomputeMassPropertiesFromColliders( + this.handle, + this.colliderSet.raw, + ); + } + + /** + * Sets the rigid-body's additional mass. + * + * The total angular inertia of the rigid-body will be scaled automatically based on this additional mass. If this + * scaling effect isn’t desired, use Self::additional_mass_properties instead of this method. + * + * This is only the "additional" mass because the total mass of the rigid-body is equal to the sum of this + * additional mass and the mass computed from the colliders (with non-zero densities) attached to this rigid-body. + * + * That total mass (which includes the attached colliders’ contributions) will be updated at the name physics step, + * or can be updated manually with `this.recomputeMassPropertiesFromColliders`. + * + * This will override any previous additional mass-properties set by `this.setAdditionalMass`, + * `this.setAdditionalMassProperties`, `RigidBodyDesc::setAdditionalMass`, or + * `RigidBodyDesc.setAdditionalMassfProperties` for this rigid-body. + * + * @param mass - The additional mass to set. + * @param wakeUp - If `true` then the rigid-body will be woken up if it was put to sleep because it did not move for a while. + */ + public setAdditionalMass(mass: number, wakeUp: boolean) { + this.rawSet.rbSetAdditionalMass(this.handle, mass, wakeUp); + } + + + /** + * Sets the rigid-body's additional mass-properties. + * + * This is only the "additional" mass-properties because the total mass-properties of the rigid-body is equal to the + * sum of this additional mass-properties and the mass computed from the colliders (with non-zero densities) attached + * to this rigid-body. + * + * That total mass-properties (which include the attached colliders’ contributions) will be updated at the name + * physics step, or can be updated manually with `this.recomputeMassPropertiesFromColliders`. + * + * This will override any previous mass-properties set by `this.setAdditionalMass`, + * `this.setAdditionalMassProperties`, `RigidBodyDesc.setAdditionalMass`, or `RigidBodyDesc.setAdditionalMassProperties` + * for this rigid-body. + * + * If `wake_up` is true then the rigid-body will be woken up if it was put to sleep because it did not move for a while. + */ + public setAdditionalMassProperties( + mass: number, + centerOfMass: Vector, + principalAngularInertia: number, + wakeUp: boolean, + ) { + let rawCom = VectorOps.intoRaw(centerOfMass); + this.rawSet.rbSetAdditionalMassProperties( + this.handle, + mass, + rawCom, + principalAngularInertia, + wakeUp, + ); + rawCom.free(); + } + + + /** + * Sets the linear damping factor applied to this rigid-body. + * + * @param factor - The damping factor to set. + */ + public setAngularDamping(factor: number) { + this.rawSet.rbSetAngularDamping(this.handle, factor); + } + + /** + * Resets to zero the user forces (but not torques) applied to this rigid-body. + * + * @param wakeUp - should the rigid-body be automatically woken-up? + */ + public resetForces(wakeUp: boolean) { + this.rawSet.rbResetForces(this.handle, wakeUp); + } + + /** + * Resets to zero the user torques applied to this rigid-body. + * + * @param wakeUp - should the rigid-body be automatically woken-up? + */ + public resetTorques(wakeUp: boolean) { + this.rawSet.rbResetTorques(this.handle, wakeUp); + } + + /** + * Adds a force at the center-of-mass of this rigid-body. + * + * @param force - the world-space force to add to the rigid-body. + * @param wakeUp - should the rigid-body be automatically woken-up? + */ + public addForce(force: Vector, wakeUp: boolean) { + const rawForce = VectorOps.intoRaw(force); + this.rawSet.rbAddForce(this.handle, rawForce, wakeUp); + rawForce.free(); + } + + /** + * Applies an impulse at the center-of-mass of this rigid-body. + * + * @param impulse - the world-space impulse to apply on the rigid-body. + * @param wakeUp - should the rigid-body be automatically woken-up? + */ + public applyImpulse(impulse: Vector, wakeUp: boolean) { + const rawImpulse = VectorOps.intoRaw(impulse); + this.rawSet.rbApplyImpulse(this.handle, rawImpulse, wakeUp); + rawImpulse.free(); + } + + /** + * Adds a torque at the center-of-mass of this rigid-body. + * + * @param torque - the torque to add to the rigid-body. + * @param wakeUp - should the rigid-body be automatically woken-up? + */ + public addTorque(torque: number, wakeUp: boolean) { + this.rawSet.rbAddTorque(this.handle, torque, wakeUp); + } + + + + /** + * Applies an impulsive torque at the center-of-mass of this rigid-body. + * + * @param torqueImpulse - the torque impulse to apply on the rigid-body. + * @param wakeUp - should the rigid-body be automatically woken-up? + */ + public applyTorqueImpulse(torqueImpulse: number, wakeUp: boolean) { + this.rawSet.rbApplyTorqueImpulse(this.handle, torqueImpulse, wakeUp); + } + + + + /** + * Adds a force at the given world-space point of this rigid-body. + * + * @param force - the world-space force to add to the rigid-body. + * @param point - the world-space point where the impulse is to be applied on the rigid-body. + * @param wakeUp - should the rigid-body be automatically woken-up? + */ + public addForceAtPoint(force: Vector, point: Vector, wakeUp: boolean) { + const rawForce = VectorOps.intoRaw(force); + const rawPoint = VectorOps.intoRaw(point); + this.rawSet.rbAddForceAtPoint(this.handle, rawForce, rawPoint, wakeUp); + rawForce.free(); + rawPoint.free(); + } + + /** + * Applies an impulse at the given world-space point of this rigid-body. + * + * @param impulse - the world-space impulse to apply on the rigid-body. + * @param point - the world-space point where the impulse is to be applied on the rigid-body. + * @param wakeUp - should the rigid-body be automatically woken-up? + */ + public applyImpulseAtPoint( + impulse: Vector, + point: Vector, + wakeUp: boolean, + ) { + const rawImpulse = VectorOps.intoRaw(impulse); + const rawPoint = VectorOps.intoRaw(point); + this.rawSet.rbApplyImpulseAtPoint( + this.handle, + rawImpulse, + rawPoint, + wakeUp, + ); + rawImpulse.free(); + rawPoint.free(); + } + + /** + * Retrieves the constant force(s) the user added to this rigid-body + * Returns zero if the rigid-body is not dynamic. + */ + public userForce(): Vector { + return VectorOps.fromRaw(this.rawSet.rbUserForce(this.handle)); + } + + /** + * Retrieves the constant torque(s) the user added to this rigid-body + * Returns zero if the rigid-body is not dynamic. + */ + public userTorque(): number { + return this.rawSet.rbUserTorque(this.handle); + } + +} + +export class RigidBodyDesc { + enabled: boolean; + translation: Vector; + rotation: Rotation; + gravityScale: number; + mass: number; + massOnly: boolean; + centerOfMass: Vector; + translationsEnabledX: boolean; + translationsEnabledY: boolean; + linvel: Vector; + angvel: number; + principalAngularInertia: number; + rotationsEnabled: boolean; + linearDamping: number; + angularDamping: number; + status: RigidBodyType; + canSleep: boolean; + sleeping: boolean; + ccdEnabled: boolean; + softCcdPrediction: number; + dominanceGroup: number; + additionalSolverIterations: number; + userData?: unknown; + + constructor(status: RigidBodyType) { + this.enabled = true; + this.status = status; + this.translation = VectorOps.zeros(); + this.rotation = RotationOps.identity(); + this.gravityScale = 1.0; + this.linvel = VectorOps.zeros(); + this.mass = 0.0; + this.massOnly = false; + this.centerOfMass = VectorOps.zeros(); + this.translationsEnabledX = true; + this.translationsEnabledY = true; + this.angvel = 0.0; + this.principalAngularInertia = 0.0; + this.rotationsEnabled = true; + this.linearDamping = 0.0; + this.angularDamping = 0.0; + this.canSleep = true; + this.sleeping = false; + this.ccdEnabled = false; + this.softCcdPrediction = 0.0; + this.dominanceGroup = 0; + this.additionalSolverIterations = 0; + } + + /** + * A rigid-body descriptor used to build a dynamic rigid-body. + */ + public static dynamic(): RigidBodyDesc { + return new RigidBodyDesc(RigidBodyType.Dynamic); + } + + /** + * A rigid-body descriptor used to build a position-based kinematic rigid-body. + */ + public static kinematicPositionBased(): RigidBodyDesc { + return new RigidBodyDesc(RigidBodyType.KinematicPositionBased); + } + + /** + * A rigid-body descriptor used to build a velocity-based kinematic rigid-body. + */ + public static kinematicVelocityBased(): RigidBodyDesc { + return new RigidBodyDesc(RigidBodyType.KinematicVelocityBased); + } + + /** + * A rigid-body descriptor used to build a fixed rigid-body. + */ + public static fixed(): RigidBodyDesc { + return new RigidBodyDesc(RigidBodyType.Fixed); + } + + /** + * A rigid-body descriptor used to build a dynamic rigid-body. + * + * @deprecated The method has been renamed to `.dynamic()`. + */ + public static newDynamic(): RigidBodyDesc { + return new RigidBodyDesc(RigidBodyType.Dynamic); + } + + /** + * A rigid-body descriptor used to build a position-based kinematic rigid-body. + * + * @deprecated The method has been renamed to `.kinematicPositionBased()`. + */ + public static newKinematicPositionBased(): RigidBodyDesc { + return new RigidBodyDesc(RigidBodyType.KinematicPositionBased); + } + + /** + * A rigid-body descriptor used to build a velocity-based kinematic rigid-body. + * + * @deprecated The method has been renamed to `.kinematicVelocityBased()`. + */ + public static newKinematicVelocityBased(): RigidBodyDesc { + return new RigidBodyDesc(RigidBodyType.KinematicVelocityBased); + } + + /** + * A rigid-body descriptor used to build a fixed rigid-body. + * + * @deprecated The method has been renamed to `.fixed()`. + */ + public static newStatic(): RigidBodyDesc { + return new RigidBodyDesc(RigidBodyType.Fixed); + } + + public setDominanceGroup(group: number): RigidBodyDesc { + this.dominanceGroup = group; + return this; + } + + /** + * Sets the number of additional solver iterations that will be run for this + * rigid-body and everything that interacts with it directly or indirectly + * through contacts or joints. + * + * Compared to increasing the global `World.numSolverIteration`, setting this + * value lets you increase accuracy on only a subset of the scene, resulting in reduced + * performance loss. + * + * @param iters - The new number of additional solver iterations (default: 0). + */ + public setAdditionalSolverIterations(iters: number): RigidBodyDesc { + this.additionalSolverIterations = iters; + return this; + } + + /** + * Sets whether the created rigid-body will be enabled or disabled. + * @param enabled − If set to `false` the rigid-body will be disabled at creation. + */ + public setEnabled(enabled: boolean): RigidBodyDesc { + this.enabled = enabled; + return this; + } + + /** + * Sets the initial translation of the rigid-body to create. + */ + public setTranslation(x: number, y: number): RigidBodyDesc { + if (typeof x != "number" || typeof y != "number") + throw TypeError("The translation components must be numbers."); + + this.translation = {x: x, y: y}; + return this; + } + + + + /** + * Sets the initial rotation of the rigid-body to create. + * + * @param rot - The rotation to set. + */ + public setRotation(rot: Rotation): RigidBodyDesc { + this.rotation = rot; + return this; + } + + /** + * Sets the scale factor applied to the gravity affecting + * the rigid-body being built. + * + * @param scale - The scale factor. Set this to `0.0` if the rigid-body + * needs to ignore gravity. + */ + public setGravityScale(scale: number): RigidBodyDesc { + this.gravityScale = scale; + return this; + } + + /** + * Sets the initial mass of the rigid-body being built, before adding colliders' contributions. + * + * @param mass − The initial mass of the rigid-body to create. + */ + public setAdditionalMass(mass: number): RigidBodyDesc { + this.mass = mass; + this.massOnly = true; + return this; + } + + /** + * Sets the initial linear velocity of the rigid-body to create. + * + * @param x - The linear velocity to set along the `x` axis. + * @param y - The linear velocity to set along the `y` axis. + */ + public setLinvel(x: number, y: number): RigidBodyDesc { + if (typeof x != "number" || typeof y != "number") + throw TypeError("The linvel components must be numbers."); + + this.linvel = {x: x, y: y}; + return this; + } + + /** + * Sets the initial angular velocity of the rigid-body to create. + * + * @param vel - The angular velocity to set. + */ + public setAngvel(vel: number): RigidBodyDesc { + this.angvel = vel; + return this; + } + + /** + * Sets the mass properties of the rigid-body being built. + * + * Note that the final mass properties of the rigid-bodies depends + * on the initial mass-properties of the rigid-body (set by this method) + * to which is added the contributions of all the colliders with non-zero density + * attached to this rigid-body. + * + * Therefore, if you want your provided mass properties to be the final + * mass properties of your rigid-body, don't attach colliders to it, or + * only attach colliders with densities equal to zero. + * + * @param mass − The initial mass of the rigid-body to create. + * @param centerOfMass − The initial center-of-mass of the rigid-body to create. + * @param principalAngularInertia − The initial principal angular inertia of the rigid-body to create. + */ + public setAdditionalMassProperties( + mass: number, + centerOfMass: Vector, + principalAngularInertia: number, + ): RigidBodyDesc { + this.mass = mass; + VectorOps.copy(this.centerOfMass, centerOfMass); + this.principalAngularInertia = principalAngularInertia; + this.massOnly = false; + return this; + } + + /** + * Allow translation of this rigid-body only along specific axes. + * @param translationsEnabledX - Are translations along the X axis enabled? + * @param translationsEnabledY - Are translations along the y axis enabled? + */ + public enabledTranslations( + translationsEnabledX: boolean, + translationsEnabledY: boolean, + ): RigidBodyDesc { + this.translationsEnabledX = translationsEnabledX; + this.translationsEnabledY = translationsEnabledY; + return this; + } + + /** + * Allow translation of this rigid-body only along specific axes. + * @param translationsEnabledX - Are translations along the X axis enabled? + * @param translationsEnabledY - Are translations along the y axis enabled? + * @deprecated use `this.enabledTranslations` with the same arguments instead. + */ + public restrictTranslations( + translationsEnabledX: boolean, + translationsEnabledY: boolean, + ): RigidBodyDesc { + return this.enabledTranslations( + translationsEnabledX, + translationsEnabledY, + ); + } + + /** + * Locks all translations that would have resulted from forces on + * the created rigid-body. + */ + public lockTranslations(): RigidBodyDesc { + return this.restrictTranslations(false, false); + } + + /** + * Locks all rotations that would have resulted from forces on + * the created rigid-body. + */ + public lockRotations(): RigidBodyDesc { + this.rotationsEnabled = false; + return this; + } + + + + /** + * Sets the linear damping of the rigid-body to create. + * + * This will progressively slowdown the translational movement of the rigid-body. + * + * @param damping - The angular damping coefficient. Should be >= 0. The higher this + * value is, the stronger the translational slowdown will be. + */ + public setLinearDamping(damping: number): RigidBodyDesc { + this.linearDamping = damping; + return this; + } + + /** + * Sets the angular damping of the rigid-body to create. + * + * This will progressively slowdown the rotational movement of the rigid-body. + * + * @param damping - The angular damping coefficient. Should be >= 0. The higher this + * value is, the stronger the rotational slowdown will be. + */ + public setAngularDamping(damping: number): RigidBodyDesc { + this.angularDamping = damping; + return this; + } + + /** + * Sets whether or not the rigid-body to create can sleep. + * + * @param can - true if the rigid-body can sleep, false if it can't. + */ + public setCanSleep(can: boolean): RigidBodyDesc { + this.canSleep = can; + return this; + } + + /** + * Sets whether or not the rigid-body is to be created asleep. + * + * @param can - true if the rigid-body should be in sleep, default false. + */ + setSleeping(sleeping: boolean): RigidBodyDesc { + this.sleeping = sleeping; + return this; + } + + /** + * Sets whether Continuous Collision Detection (CCD) is enabled for this rigid-body. + * + * @param enabled - true if the rigid-body has CCD enabled. + */ + public setCcdEnabled(enabled: boolean): RigidBodyDesc { + this.ccdEnabled = enabled; + return this; + } + + /** + * Sets the maximum prediction distance Soft Continuous Collision-Detection. + * + * When set to 0, soft-CCD is disabled. Soft-CCD helps prevent tunneling especially of + * slow-but-thin to moderately fast objects. The soft CCD prediction distance indicates how + * far in the object’s path the CCD algorithm is allowed to inspect. Large values can impact + * performance badly by increasing the work needed from the broad-phase. + * + * It is a generally cheaper variant of regular CCD (that can be enabled with + * `RigidBodyDesc::setCcdEnabled` since it relies on predictive constraints instead of + * shape-cast and substeps. + */ + public setSoftCcdPrediction(distance: number): RigidBodyDesc { + this.softCcdPrediction = distance; + return this; + } + + /** + * Sets the user-defined object of this rigid-body. + * + * @param userData - The user-defined object to set. + */ + public setUserData(data?: unknown): RigidBodyDesc { + this.userData = data; + return this; + } +} diff --git a/packages/rapier2d/src/dynamics/rigid_body_set.ts b/packages/rapier2d/src/dynamics/rigid_body_set.ts new file mode 100644 index 00000000..25e583c1 --- /dev/null +++ b/packages/rapier2d/src/dynamics/rigid_body_set.ts @@ -0,0 +1,211 @@ +import {RawRigidBodySet, RawRigidBodyType} from "../raw"; +import {Coarena} from "../coarena"; +import {VectorOps, RotationOps} from "../math"; +import { + RigidBody, + RigidBodyDesc, + RigidBodyHandle, + RigidBodyType, +} from "./rigid_body"; +import {ColliderSet} from "../geometry"; +import {ImpulseJointSet} from "./impulse_joint_set"; +import {MultibodyJointSet} from "./multibody_joint_set"; +import {IslandManager} from "./island_manager"; + +/** + * A set of rigid bodies that can be handled by a physics pipeline. + * + * To avoid leaking WASM resources, this MUST be freed manually with `rigidBodySet.free()` + * once you are done using it (and all the rigid-bodies it created). + */ +export class RigidBodySet { + raw: RawRigidBodySet; + private map: Coarena; + + /** + * Release the WASM memory occupied by this rigid-body set. + */ + public free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + + if (!!this.map) { + this.map.clear(); + } + this.map = undefined; + } + + constructor(raw?: RawRigidBodySet) { + this.raw = raw || new RawRigidBodySet(); + this.map = new Coarena(); + // deserialize + if (raw) { + raw.forEachRigidBodyHandle((handle: RigidBodyHandle) => { + this.map.set(handle, new RigidBody(raw, null, handle)); + }); + } + } + + /** + * Internal method, do not call this explicitly. + */ + public finalizeDeserialization(colliderSet: ColliderSet) { + this.map.forEach((rb) => rb.finalizeDeserialization(colliderSet)); + } + + /** + * Creates a new rigid-body and return its integer handle. + * + * @param desc - The description of the rigid-body to create. + */ + public createRigidBody( + colliderSet: ColliderSet, + desc: RigidBodyDesc, + ): RigidBody { + let rawTra = VectorOps.intoRaw(desc.translation); + let rawRot = RotationOps.intoRaw(desc.rotation); + let rawLv = VectorOps.intoRaw(desc.linvel); + let rawCom = VectorOps.intoRaw(desc.centerOfMass); + + + let handle = this.raw.createRigidBody( + desc.enabled, + rawTra, + rawRot, + desc.gravityScale, + desc.mass, + desc.massOnly, + rawCom, + rawLv, + desc.angvel, + desc.principalAngularInertia, + desc.translationsEnabledX, + desc.translationsEnabledY, + desc.rotationsEnabled, + desc.linearDamping, + desc.angularDamping, + desc.status as number as RawRigidBodyType, + desc.canSleep, + desc.sleeping, + desc.softCcdPrediction, + desc.ccdEnabled, + desc.dominanceGroup, + desc.additionalSolverIterations, + ); + + rawTra.free(); + rawRot.free(); + rawLv.free(); + rawCom.free(); + + + const body = new RigidBody(this.raw, colliderSet, handle); + body.userData = desc.userData; + + this.map.set(handle, body); + + return body; + } + + /** + * Removes a rigid-body from this set. + * + * This will also remove all the colliders and joints attached to the rigid-body. + * + * @param handle - The integer handle of the rigid-body to remove. + * @param colliders - The set of colliders that may contain colliders attached to the removed rigid-body. + * @param impulseJoints - The set of impulse joints that may contain joints attached to the removed rigid-body. + * @param multibodyJoints - The set of multibody joints that may contain joints attached to the removed rigid-body. + */ + public remove( + handle: RigidBodyHandle, + islands: IslandManager, + colliders: ColliderSet, + impulseJoints: ImpulseJointSet, + multibodyJoints: MultibodyJointSet, + ) { + // Unmap the entities that will be removed automatically because of the rigid-body removals. + for (let i = 0; i < this.raw.rbNumColliders(handle); i += 1) { + colliders.unmap(this.raw.rbCollider(handle, i)); + } + + impulseJoints.forEachJointHandleAttachedToRigidBody(handle, (handle) => + impulseJoints.unmap(handle), + ); + multibodyJoints.forEachJointHandleAttachedToRigidBody( + handle, + (handle) => multibodyJoints.unmap(handle), + ); + + // Remove the rigid-body. + this.raw.remove( + handle, + islands.raw, + colliders.raw, + impulseJoints.raw, + multibodyJoints.raw, + ); + this.map.delete(handle); + } + + /** + * The number of rigid-bodies on this set. + */ + public len(): number { + return this.map.len(); + } + + /** + * Does this set contain a rigid-body with the given handle? + * + * @param handle - The rigid-body handle to check. + */ + public contains(handle: RigidBodyHandle): boolean { + return this.get(handle) != null; + } + + /** + * Gets the rigid-body with the given handle. + * + * @param handle - The handle of the rigid-body to retrieve. + */ + public get(handle: RigidBodyHandle): RigidBody | null { + return this.map.get(handle); + } + + /** + * Applies the given closure to each rigid-body contained by this set. + * + * @param f - The closure to apply. + */ + public forEach(f: (body: RigidBody) => void) { + this.map.forEach(f); + } + + /** + * Applies the given closure to each active rigid-bodies contained by this set. + * + * A rigid-body is active if it is not sleeping, i.e., if it moved recently. + * + * @param f - The closure to apply. + */ + public forEachActiveRigidBody( + islands: IslandManager, + f: (body: RigidBody) => void, + ) { + islands.forEachActiveRigidBodyHandle((handle) => { + f(this.get(handle)); + }); + } + + /** + * Gets all rigid-bodies in the list. + * + * @returns rigid-bodies list. + */ + public getAll(): RigidBody[] { + return this.map.getAll(); + } +} diff --git a/packages/rapier2d/src/exports.ts b/packages/rapier2d/src/exports.ts new file mode 100644 index 00000000..237bd545 --- /dev/null +++ b/packages/rapier2d/src/exports.ts @@ -0,0 +1,27 @@ +import {version as vers, reserve_memory as reserve} from "./raw"; + +export function version(): string { + return vers(); +} + +/// Reserves additional memory in WASM land. +/// +/// This will grow the internal WASM memory buffer so that it can fit at least +/// the specified amount of extra bytes. This can help reduce future runtime +/// overhead due to dynamic internal memory growth once the limit of the +/// pre-allocated memory is reached. +/// +/// This feature is still experimental. Due to the nature of the internal +/// allocator, there can be situations where the allocator decides to perform +/// additional internal memory growth even though not all `extraBytesCount` +/// are occupied yet. +export function reserveMemory(extraBytesCount: number) { + reserve(extraBytesCount); +} + +export * from "./math"; +export * from "./dynamics"; +export * from "./geometry"; +export * from "./pipeline"; +export * from "./init"; +export * from "./control"; diff --git a/packages/rapier2d/src/geometry/broad_phase.ts b/packages/rapier2d/src/geometry/broad_phase.ts new file mode 100644 index 00000000..addaee92 --- /dev/null +++ b/packages/rapier2d/src/geometry/broad_phase.ts @@ -0,0 +1,520 @@ +import {RawBroadPhase, RawRayColliderIntersection} from "../raw"; +import {RigidBodyHandle, RigidBodySet} from "../dynamics"; +import {ColliderSet} from "./collider_set"; +import {Ray, RayColliderHit, RayColliderIntersection} from "./ray"; +import {InteractionGroups} from "./interaction_groups"; +import {ColliderHandle} from "./collider"; +import {Rotation, RotationOps, Vector, VectorOps} from "../math"; +import {Shape} from "./shape"; +import {PointColliderProjection} from "./point"; +import {ColliderShapeCastHit} from "./toi"; +import {QueryFilterFlags} from "../pipeline"; +import {NarrowPhase} from "./narrow_phase"; + +/** + * The broad-phase used for coarse collision-detection. + * + * To avoid leaking WASM resources, this MUST be freed manually with `broadPhase.free()` + * once you are done using it. + */ +export class BroadPhase { + raw: RawBroadPhase; + + /** + * Release the WASM memory occupied by this broad-phase. + */ + public free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + } + + constructor(raw?: RawBroadPhase) { + this.raw = raw || new RawBroadPhase(); + } + + /** + * Find the closest intersection between a ray and a set of collider. + * + * @param colliders - The set of colliders taking part in this pipeline. + * @param ray - The ray to cast. + * @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively + * limits the length of the ray to `ray.dir.norm() * maxToi`. + * @param solid - If `false` then the ray will attempt to hit the boundary of a shape, even if its + * origin already lies inside of a shape. In other terms, `true` implies that all shapes are plain, + * whereas `false` implies that all shapes are hollow for this ray-cast. + * @param groups - Used to filter the colliders that can or cannot be hit by the ray. + * @param filter - The callback to filter out which collider will be hit. + */ + public castRay( + narrowPhase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + ray: Ray, + maxToi: number, + solid: boolean, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: ColliderHandle, + filterExcludeRigidBody?: RigidBodyHandle, + filterPredicate?: (collider: ColliderHandle) => boolean, + ): RayColliderHit | null { + let rawOrig = VectorOps.intoRaw(ray.origin); + let rawDir = VectorOps.intoRaw(ray.dir); + let result = RayColliderHit.fromRaw( + colliders, + this.raw.castRay( + narrowPhase.raw, + bodies.raw, + colliders.raw, + rawOrig, + rawDir, + maxToi, + solid, + filterFlags, + filterGroups, + filterExcludeCollider, + filterExcludeRigidBody, + filterPredicate, + ), + ); + + rawOrig.free(); + rawDir.free(); + + return result; + } + + /** + * Find the closest intersection between a ray and a set of collider. + * + * This also computes the normal at the hit point. + * @param colliders - The set of colliders taking part in this pipeline. + * @param ray - The ray to cast. + * @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively + * limits the length of the ray to `ray.dir.norm() * maxToi`. + * @param solid - If `false` then the ray will attempt to hit the boundary of a shape, even if its + * origin already lies inside of a shape. In other terms, `true` implies that all shapes are plain, + * whereas `false` implies that all shapes are hollow for this ray-cast. + * @param groups - Used to filter the colliders that can or cannot be hit by the ray. + */ + public castRayAndGetNormal( + narrowPhase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + ray: Ray, + maxToi: number, + solid: boolean, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: ColliderHandle, + filterExcludeRigidBody?: RigidBodyHandle, + filterPredicate?: (collider: ColliderHandle) => boolean, + ): RayColliderIntersection | null { + let rawOrig = VectorOps.intoRaw(ray.origin); + let rawDir = VectorOps.intoRaw(ray.dir); + let result = RayColliderIntersection.fromRaw( + colliders, + this.raw.castRayAndGetNormal( + narrowPhase.raw, + bodies.raw, + colliders.raw, + rawOrig, + rawDir, + maxToi, + solid, + filterFlags, + filterGroups, + filterExcludeCollider, + filterExcludeRigidBody, + filterPredicate, + ), + ); + + rawOrig.free(); + rawDir.free(); + + return result; + } + + /** + * Cast a ray and collects all the intersections between a ray and the scene. + * + * @param colliders - The set of colliders taking part in this pipeline. + * @param ray - The ray to cast. + * @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively + * limits the length of the ray to `ray.dir.norm() * maxToi`. + * @param solid - If `false` then the ray will attempt to hit the boundary of a shape, even if its + * origin already lies inside of a shape. In other terms, `true` implies that all shapes are plain, + * whereas `false` implies that all shapes are hollow for this ray-cast. + * @param groups - Used to filter the colliders that can or cannot be hit by the ray. + * @param callback - The callback called once per hit (in no particular order) between a ray and a collider. + * If this callback returns `false`, then the cast will stop and no further hits will be detected/reported. + */ + public intersectionsWithRay( + narrowPhase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + ray: Ray, + maxToi: number, + solid: boolean, + callback: (intersect: RayColliderIntersection) => boolean, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: ColliderHandle, + filterExcludeRigidBody?: RigidBodyHandle, + filterPredicate?: (collider: ColliderHandle) => boolean, + ) { + let rawOrig = VectorOps.intoRaw(ray.origin); + let rawDir = VectorOps.intoRaw(ray.dir); + let rawCallback = (rawInter: RawRayColliderIntersection) => { + return callback( + RayColliderIntersection.fromRaw(colliders, rawInter), + ); + }; + + this.raw.intersectionsWithRay( + narrowPhase.raw, + bodies.raw, + colliders.raw, + rawOrig, + rawDir, + maxToi, + solid, + rawCallback, + filterFlags, + filterGroups, + filterExcludeCollider, + filterExcludeRigidBody, + filterPredicate, + ); + + rawOrig.free(); + rawDir.free(); + } + + /** + * Gets the handle of up to one collider intersecting the given shape. + * + * @param colliders - The set of colliders taking part in this pipeline. + * @param shapePos - The position of the shape used for the intersection test. + * @param shapeRot - The orientation of the shape used for the intersection test. + * @param shape - The shape used for the intersection test. + * @param groups - The bit groups and filter associated to the ray, in order to only + * hit the colliders with collision groups compatible with the ray's group. + */ + public intersectionWithShape( + narrowPhase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + shapePos: Vector, + shapeRot: Rotation, + shape: Shape, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: ColliderHandle, + filterExcludeRigidBody?: RigidBodyHandle, + filterPredicate?: (collider: ColliderHandle) => boolean, + ): ColliderHandle | null { + let rawPos = VectorOps.intoRaw(shapePos); + let rawRot = RotationOps.intoRaw(shapeRot); + let rawShape = shape.intoRaw(); + let result = this.raw.intersectionWithShape( + narrowPhase.raw, + bodies.raw, + colliders.raw, + rawPos, + rawRot, + rawShape, + filterFlags, + filterGroups, + filterExcludeCollider, + filterExcludeRigidBody, + filterPredicate, + ); + + rawPos.free(); + rawRot.free(); + rawShape.free(); + + return result; + } + + /** + * Find the projection of a point on the closest collider. + * + * @param colliders - The set of colliders taking part in this pipeline. + * @param point - The point to project. + * @param solid - If this is set to `true` then the collider shapes are considered to + * be plain (if the point is located inside of a plain shape, its projection is the point + * itself). If it is set to `false` the collider shapes are considered to be hollow + * (if the point is located inside of an hollow shape, it is projected on the shape's + * boundary). + * @param groups - The bit groups and filter associated to the point to project, in order to only + * project on colliders with collision groups compatible with the ray's group. + */ + public projectPoint( + narrowPhase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + point: Vector, + solid: boolean, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: ColliderHandle, + filterExcludeRigidBody?: RigidBodyHandle, + filterPredicate?: (collider: ColliderHandle) => boolean, + ): PointColliderProjection | null { + let rawPoint = VectorOps.intoRaw(point); + let result = PointColliderProjection.fromRaw( + colliders, + this.raw.projectPoint( + narrowPhase.raw, + bodies.raw, + colliders.raw, + rawPoint, + solid, + filterFlags, + filterGroups, + filterExcludeCollider, + filterExcludeRigidBody, + filterPredicate, + ), + ); + + rawPoint.free(); + + return result; + } + + /** + * Find the projection of a point on the closest collider. + * + * @param colliders - The set of colliders taking part in this pipeline. + * @param point - The point to project. + * @param groups - The bit groups and filter associated to the point to project, in order to only + * project on colliders with collision groups compatible with the ray's group. + */ + public projectPointAndGetFeature( + narrowPhase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + point: Vector, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: ColliderHandle, + filterExcludeRigidBody?: RigidBodyHandle, + filterPredicate?: (collider: ColliderHandle) => boolean, + ): PointColliderProjection | null { + let rawPoint = VectorOps.intoRaw(point); + let result = PointColliderProjection.fromRaw( + colliders, + this.raw.projectPointAndGetFeature( + narrowPhase.raw, + bodies.raw, + colliders.raw, + rawPoint, + filterFlags, + filterGroups, + filterExcludeCollider, + filterExcludeRigidBody, + filterPredicate, + ), + ); + + rawPoint.free(); + + return result; + } + + /** + * Find all the colliders containing the given point. + * + * @param colliders - The set of colliders taking part in this pipeline. + * @param point - The point used for the containment test. + * @param groups - The bit groups and filter associated to the point to test, in order to only + * test on colliders with collision groups compatible with the ray's group. + * @param callback - A function called with the handles of each collider with a shape + * containing the `point`. + */ + public intersectionsWithPoint( + narrowPhase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + point: Vector, + callback: (handle: ColliderHandle) => boolean, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: ColliderHandle, + filterExcludeRigidBody?: RigidBodyHandle, + filterPredicate?: (collider: ColliderHandle) => boolean, + ) { + let rawPoint = VectorOps.intoRaw(point); + + this.raw.intersectionsWithPoint( + narrowPhase.raw, + bodies.raw, + colliders.raw, + rawPoint, + callback, + filterFlags, + filterGroups, + filterExcludeCollider, + filterExcludeRigidBody, + filterPredicate, + ); + + rawPoint.free(); + } + + /** + * Casts a shape at a constant linear velocity and retrieve the first collider it hits. + * This is similar to ray-casting except that we are casting a whole shape instead of + * just a point (the ray origin). + * + * @param colliders - The set of colliders taking part in this pipeline. + * @param shapePos - The initial position of the shape to cast. + * @param shapeRot - The initial rotation of the shape to cast. + * @param shapeVel - The constant velocity of the shape to cast (i.e. the cast direction). + * @param shape - The shape to cast. + * @param targetDistance − If the shape moves closer to this distance from a collider, a hit + * will be returned. + * @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively + * limits the distance traveled by the shape to `shapeVel.norm() * maxToi`. + * @param stopAtPenetration - If set to `false`, the linear shape-cast won’t immediately stop if + * the shape is penetrating another shape at its starting point **and** its trajectory is such + * that it’s on a path to exit that penetration state. + * @param groups - The bit groups and filter associated to the shape to cast, in order to only + * test on colliders with collision groups compatible with this group. + */ + public castShape( + narrowPhase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + shapePos: Vector, + shapeRot: Rotation, + shapeVel: Vector, + shape: Shape, + targetDistance: number, + maxToi: number, + stopAtPenetration: boolean, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: ColliderHandle, + filterExcludeRigidBody?: RigidBodyHandle, + filterPredicate?: (collider: ColliderHandle) => boolean, + ): ColliderShapeCastHit | null { + let rawPos = VectorOps.intoRaw(shapePos); + let rawRot = RotationOps.intoRaw(shapeRot); + let rawVel = VectorOps.intoRaw(shapeVel); + let rawShape = shape.intoRaw(); + + let result = ColliderShapeCastHit.fromRaw( + colliders, + this.raw.castShape( + narrowPhase.raw, + bodies.raw, + colliders.raw, + rawPos, + rawRot, + rawVel, + rawShape, + targetDistance, + maxToi, + stopAtPenetration, + filterFlags, + filterGroups, + filterExcludeCollider, + filterExcludeRigidBody, + filterPredicate, + ), + ); + + rawPos.free(); + rawRot.free(); + rawVel.free(); + rawShape.free(); + + return result; + } + + /** + * Retrieve all the colliders intersecting the given shape. + * + * @param colliders - The set of colliders taking part in this pipeline. + * @param shapePos - The position of the shape to test. + * @param shapeRot - The orientation of the shape to test. + * @param shape - The shape to test. + * @param groups - The bit groups and filter associated to the shape to test, in order to only + * test on colliders with collision groups compatible with this group. + * @param callback - A function called with the handles of each collider intersecting the `shape`. + */ + public intersectionsWithShape( + narrowPhase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + shapePos: Vector, + shapeRot: Rotation, + shape: Shape, + callback: (handle: ColliderHandle) => boolean, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: ColliderHandle, + filterExcludeRigidBody?: RigidBodyHandle, + filterPredicate?: (collider: ColliderHandle) => boolean, + ) { + let rawPos = VectorOps.intoRaw(shapePos); + let rawRot = RotationOps.intoRaw(shapeRot); + let rawShape = shape.intoRaw(); + + this.raw.intersectionsWithShape( + narrowPhase.raw, + bodies.raw, + colliders.raw, + rawPos, + rawRot, + rawShape, + callback, + filterFlags, + filterGroups, + filterExcludeCollider, + filterExcludeRigidBody, + filterPredicate, + ); + + rawPos.free(); + rawRot.free(); + rawShape.free(); + } + + /** + * Finds the handles of all the colliders with an AABB intersecting the given AABB. + * + * @param aabbCenter - The center of the AABB to test. + * @param aabbHalfExtents - The half-extents of the AABB to test. + * @param callback - The callback that will be called with the handles of all the colliders + * currently intersecting the given AABB. + */ + public collidersWithAabbIntersectingAabb( + narrowPhase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + aabbCenter: Vector, + aabbHalfExtents: Vector, + callback: (handle: ColliderHandle) => boolean, + ) { + let rawCenter = VectorOps.intoRaw(aabbCenter); + let rawHalfExtents = VectorOps.intoRaw(aabbHalfExtents); + this.raw.collidersWithAabbIntersectingAabb( + narrowPhase.raw, + bodies.raw, + colliders.raw, + rawCenter, + rawHalfExtents, + callback, + ); + rawCenter.free(); + rawHalfExtents.free(); + } +} diff --git a/packages/rapier2d/src/geometry/collider.ts b/packages/rapier2d/src/geometry/collider.ts new file mode 100644 index 00000000..4cc98ec4 --- /dev/null +++ b/packages/rapier2d/src/geometry/collider.ts @@ -0,0 +1,1603 @@ +import {RawColliderSet} from "../raw"; +import {Rotation, RotationOps, Vector, VectorOps} from "../math"; +import { + CoefficientCombineRule, + RigidBody, + RigidBodyHandle, + RigidBodySet, +} from "../dynamics"; +import {ActiveHooks, ActiveEvents} from "../pipeline"; +import {InteractionGroups} from "./interaction_groups"; +import { + Shape, + Cuboid, + Ball, + ShapeType, + Capsule, + Voxels, + TriMesh, + Polyline, + Heightfield, + Segment, + Triangle, + RoundTriangle, + RoundCuboid, + HalfSpace, + TriMeshFlags, + ConvexPolygon, + RoundConvexPolygon, +} from "./shape"; +import {Ray, RayIntersection} from "./ray"; +import {PointProjection} from "./point"; +import {ColliderShapeCastHit, ShapeCastHit} from "./toi"; +import {ShapeContact} from "./contact"; +import {ColliderSet} from "./collider_set"; + +/** + * Flags affecting whether collision-detection happens between two colliders + * depending on the type of rigid-bodies they are attached to. + */ +export enum ActiveCollisionTypes { + /** + * Enable collision-detection between a collider attached to a dynamic body + * and another collider attached to a dynamic body. + */ + DYNAMIC_DYNAMIC = 0b0000_0000_0000_0001, + /** + * Enable collision-detection between a collider attached to a dynamic body + * and another collider attached to a kinematic body. + */ + DYNAMIC_KINEMATIC = 0b0000_0000_0000_1100, + /** + * Enable collision-detection between a collider attached to a dynamic body + * and another collider attached to a fixed body (or not attached to any body). + */ + DYNAMIC_FIXED = 0b0000_0000_0000_0010, + /** + * Enable collision-detection between a collider attached to a kinematic body + * and another collider attached to a kinematic body. + */ + KINEMATIC_KINEMATIC = 0b1100_1100_0000_0000, + + /** + * Enable collision-detection between a collider attached to a kinematic body + * and another collider attached to a fixed body (or not attached to any body). + */ + KINEMATIC_FIXED = 0b0010_0010_0000_0000, + + /** + * Enable collision-detection between a collider attached to a fixed body (or + * not attached to any body) and another collider attached to a fixed body (or + * not attached to any body). + */ + FIXED_FIXED = 0b0000_0000_0010_0000, + /** + * The default active collision types, enabling collisions between a dynamic body + * and another body of any type, but not enabling collisions between two non-dynamic bodies. + */ + DEFAULT = DYNAMIC_KINEMATIC | DYNAMIC_DYNAMIC | DYNAMIC_FIXED, + /** + * Enable collisions between any kind of rigid-bodies (including between two non-dynamic bodies). + */ + ALL = DYNAMIC_KINEMATIC | + DYNAMIC_DYNAMIC | + DYNAMIC_FIXED | + KINEMATIC_KINEMATIC | + KINEMATIC_FIXED | + KINEMATIC_KINEMATIC, +} + +/** + * The integer identifier of a collider added to a `ColliderSet`. + */ +export type ColliderHandle = number; + +/** + * A geometric entity that can be attached to a body so it can be affected + * by contacts and proximity queries. + */ +export class Collider { + private colliderSet: ColliderSet; // The Collider won't need to free this. + readonly handle: ColliderHandle; + private _shape: Shape; // TODO: deprecate/remove this since it isn’t a reliable way of getting the latest shape properties. + private _parent: RigidBody | null; + + constructor( + colliderSet: ColliderSet, + handle: ColliderHandle, + parent: RigidBody | null, + shape?: Shape, + ) { + this.colliderSet = colliderSet; + this.handle = handle; + this._parent = parent; + this._shape = shape; + } + + /** @internal */ + public finalizeDeserialization(bodies: RigidBodySet) { + if (this.handle != null) { + this._parent = bodies.get( + this.colliderSet.raw.coParent(this.handle), + ); + } + } + + private ensureShapeIsCached() { + if (!this._shape) + this._shape = Shape.fromRaw(this.colliderSet.raw, this.handle); + } + + /** + * The shape of this collider. + */ + public get shape(): Shape { + this.ensureShapeIsCached(); + return this._shape; + } + + /** + * Set the internal cached JS shape to null. + * + * This can be useful if you want to free some memory (assuming you are not + * holding any other references to the shape object), or in order to force + * the recalculation of the JS shape (the next time the `shape` getter is + * accessed) from the WASM source of truth. + */ + public clearShapeCache() { + this._shape = null; + } + + /** + * Checks if this collider is still valid (i.e. that it has + * not been deleted from the collider set yet). + */ + public isValid(): boolean { + return this.colliderSet.raw.contains(this.handle); + } + + /** + * The world-space translation of this collider. + */ + public translation(): Vector { + return VectorOps.fromRaw( + this.colliderSet.raw.coTranslation(this.handle), + ); + } + + /** + * The translation of this collider relative to its parent rigid-body. + * + * Returns `null` if the collider doesn’t have a parent rigid-body. + */ + public translationWrtParent(): Vector | null { + return VectorOps.fromRaw( + this.colliderSet.raw.coTranslationWrtParent(this.handle), + ); + } + + /** + * The world-space orientation of this collider. + */ + public rotation(): Rotation { + return RotationOps.fromRaw( + this.colliderSet.raw.coRotation(this.handle), + ); + } + + /** + * The orientation of this collider relative to its parent rigid-body. + * + * Returns `null` if the collider doesn’t have a parent rigid-body. + */ + public rotationWrtParent(): Rotation | null { + return RotationOps.fromRaw( + this.colliderSet.raw.coRotationWrtParent(this.handle), + ); + } + + /** + * Is this collider a sensor? + */ + public isSensor(): boolean { + return this.colliderSet.raw.coIsSensor(this.handle); + } + + /** + * Sets whether this collider is a sensor. + * @param isSensor - If `true`, the collider will be a sensor. + */ + public setSensor(isSensor: boolean) { + this.colliderSet.raw.coSetSensor(this.handle, isSensor); + } + + /** + * Sets the new shape of the collider. + * @param shape - The collider’s new shape. + */ + public setShape(shape: Shape) { + let rawShape = shape.intoRaw(); + this.colliderSet.raw.coSetShape(this.handle, rawShape); + rawShape.free(); + this._shape = shape; + } + + /** + * Sets whether this collider is enabled or not. + * + * @param enabled - Set to `false` to disable this collider (its parent rigid-body won’t be disabled automatically by this). + */ + public setEnabled(enabled: boolean) { + this.colliderSet.raw.coSetEnabled(this.handle, enabled); + } + + /** + * Is this collider enabled? + */ + public isEnabled(): boolean { + return this.colliderSet.raw.coIsEnabled(this.handle); + } + + /** + * Sets the restitution coefficient of the collider to be created. + * + * @param restitution - The restitution coefficient in `[0, 1]`. A value of 0 (the default) means no bouncing behavior + * while 1 means perfect bouncing (though energy may still be lost due to numerical errors of the + * constraints solver). + */ + public setRestitution(restitution: number) { + this.colliderSet.raw.coSetRestitution(this.handle, restitution); + } + + /** + * Sets the friction coefficient of the collider to be created. + * + * @param friction - The friction coefficient. Must be greater or equal to 0. This is generally smaller than 1. The + * higher the coefficient, the stronger friction forces will be for contacts with the collider + * being built. + */ + public setFriction(friction: number) { + this.colliderSet.raw.coSetFriction(this.handle, friction); + } + + /** + * Gets the rule used to combine the friction coefficients of two colliders + * colliders involved in a contact. + */ + public frictionCombineRule(): CoefficientCombineRule { + return this.colliderSet.raw.coFrictionCombineRule(this.handle); + } + + /** + * Sets the rule used to combine the friction coefficients of two colliders + * colliders involved in a contact. + * + * @param rule − The combine rule to apply. + */ + public setFrictionCombineRule(rule: CoefficientCombineRule) { + this.colliderSet.raw.coSetFrictionCombineRule(this.handle, rule); + } + + /** + * Gets the rule used to combine the restitution coefficients of two colliders + * colliders involved in a contact. + */ + public restitutionCombineRule(): CoefficientCombineRule { + return this.colliderSet.raw.coRestitutionCombineRule(this.handle); + } + + /** + * Sets the rule used to combine the restitution coefficients of two colliders + * colliders involved in a contact. + * + * @param rule − The combine rule to apply. + */ + public setRestitutionCombineRule(rule: CoefficientCombineRule) { + this.colliderSet.raw.coSetRestitutionCombineRule(this.handle, rule); + } + + /** + * Sets the collision groups used by this collider. + * + * Two colliders will interact iff. their collision groups are compatible. + * See the documentation of `InteractionGroups` for details on teh used bit pattern. + * + * @param groups - The collision groups used for the collider being built. + */ + public setCollisionGroups(groups: InteractionGroups) { + this.colliderSet.raw.coSetCollisionGroups(this.handle, groups); + } + + /** + * Sets the solver groups used by this collider. + * + * Forces between two colliders in contact will be computed iff their solver + * groups are compatible. + * See the documentation of `InteractionGroups` for details on the used bit pattern. + * + * @param groups - The solver groups used for the collider being built. + */ + public setSolverGroups(groups: InteractionGroups) { + this.colliderSet.raw.coSetSolverGroups(this.handle, groups); + } + + /** + * Sets the contact skin for this collider. + * + * See the documentation of `ColliderDesc.setContactSkin` for additional details. + */ + public contactSkin(): number { + return this.colliderSet.raw.coContactSkin(this.handle); + } + + /** + * Sets the contact skin for this collider. + * + * See the documentation of `ColliderDesc.setContactSkin` for additional details. + * + * @param thickness - The contact skin thickness. + */ + public setContactSkin(thickness: number) { + return this.colliderSet.raw.coSetContactSkin(this.handle, thickness); + } + + /** + * Get the physics hooks active for this collider. + */ + public activeHooks(): ActiveHooks { + return this.colliderSet.raw.coActiveHooks(this.handle); + } + + /** + * Set the physics hooks active for this collider. + * + * Use this to enable custom filtering rules for contact/intersecstion pairs involving this collider. + * + * @param activeHooks - The hooks active for contact/intersection pairs involving this collider. + */ + public setActiveHooks(activeHooks: ActiveHooks) { + this.colliderSet.raw.coSetActiveHooks(this.handle, activeHooks); + } + + /** + * The events active for this collider. + */ + public activeEvents(): ActiveEvents { + return this.colliderSet.raw.coActiveEvents(this.handle); + } + + /** + * Set the events active for this collider. + * + * Use this to enable contact and/or intersection event reporting for this collider. + * + * @param activeEvents - The events active for contact/intersection pairs involving this collider. + */ + public setActiveEvents(activeEvents: ActiveEvents) { + this.colliderSet.raw.coSetActiveEvents(this.handle, activeEvents); + } + + /** + * Gets the collision types active for this collider. + */ + public activeCollisionTypes(): ActiveCollisionTypes { + return this.colliderSet.raw.coActiveCollisionTypes(this.handle); + } + + /** + * Sets the total force magnitude beyond which a contact force event can be emitted. + * + * @param threshold - The new force threshold. + */ + public setContactForceEventThreshold(threshold: number) { + return this.colliderSet.raw.coSetContactForceEventThreshold( + this.handle, + threshold, + ); + } + + /** + * The total force magnitude beyond which a contact force event can be emitted. + */ + public contactForceEventThreshold(): number { + return this.colliderSet.raw.coContactForceEventThreshold(this.handle); + } + + /** + * Set the collision types active for this collider. + * + * @param activeCollisionTypes - The hooks active for contact/intersection pairs involving this collider. + */ + public setActiveCollisionTypes(activeCollisionTypes: ActiveCollisionTypes) { + this.colliderSet.raw.coSetActiveCollisionTypes( + this.handle, + activeCollisionTypes, + ); + } + + /** + * Sets the uniform density of this collider. + * + * This will override any previous mass-properties set by `this.setDensity`, + * `this.setMass`, `this.setMassProperties`, `ColliderDesc.density`, + * `ColliderDesc.mass`, or `ColliderDesc.massProperties` for this collider. + * + * The mass and angular inertia of this collider will be computed automatically based on its + * shape. + */ + public setDensity(density: number) { + this.colliderSet.raw.coSetDensity(this.handle, density); + } + + /** + * Sets the mass of this collider. + * + * This will override any previous mass-properties set by `this.setDensity`, + * `this.setMass`, `this.setMassProperties`, `ColliderDesc.density`, + * `ColliderDesc.mass`, or `ColliderDesc.massProperties` for this collider. + * + * The angular inertia of this collider will be computed automatically based on its shape + * and this mass value. + */ + public setMass(mass: number) { + this.colliderSet.raw.coSetMass(this.handle, mass); + } + + + /** + * Sets the mass of this collider. + * + * This will override any previous mass-properties set by `this.setDensity`, + * `this.setMass`, `this.setMassProperties`, `ColliderDesc.density`, + * `ColliderDesc.mass`, or `ColliderDesc.massProperties` for this collider. + */ + public setMassProperties( + mass: number, + centerOfMass: Vector, + principalAngularInertia: number, + ) { + let rawCom = VectorOps.intoRaw(centerOfMass); + this.colliderSet.raw.coSetMassProperties( + this.handle, + mass, + rawCom, + principalAngularInertia, + ); + rawCom.free(); + } + + + /** + * Sets the translation of this collider. + * + * @param tra - The world-space position of the collider. + */ + public setTranslation(tra: Vector) { + this.colliderSet.raw.coSetTranslation(this.handle, tra.x, tra.y); + } + + /** + * Sets the translation of this collider relative to its parent rigid-body. + * + * Does nothing if this collider isn't attached to a rigid-body. + * + * @param tra - The new translation of the collider relative to its parent. + */ + public setTranslationWrtParent(tra: Vector) { + this.colliderSet.raw.coSetTranslationWrtParent( + this.handle, + tra.x, + tra.y, + ); + } + + /** + * Sets the rotation angle of this collider. + * + * @param angle - The rotation angle, in radians. + */ + public setRotation(angle: number) { + this.colliderSet.raw.coSetRotation(this.handle, angle); + } + + /** + * Sets the rotation angle of this collider relative to its parent rigid-body. + * + * Does nothing if this collider isn't attached to a rigid-body. + * + * @param angle - The rotation angle, in radians. + */ + public setRotationWrtParent(angle: number) { + this.colliderSet.raw.coSetRotationWrtParent(this.handle, angle); + } + + + /** + * The type of the shape of this collider. + */ + public shapeType(): ShapeType { + return this.colliderSet.raw.coShapeType( + this.handle, + ) as number as ShapeType; + } + + /** + * The half-extents of this collider if it is a cuboid shape. + */ + public halfExtents(): Vector { + return VectorOps.fromRaw( + this.colliderSet.raw.coHalfExtents(this.handle), + ); + } + + /** + * Sets the half-extents of this collider if it is a cuboid shape. + * + * @param newHalfExtents - desired half extents. + */ + public setHalfExtents(newHalfExtents: Vector) { + const rawPoint = VectorOps.intoRaw(newHalfExtents); + this.colliderSet.raw.coSetHalfExtents(this.handle, rawPoint); + } + + /** + * The radius of this collider if it is a ball, cylinder, capsule, or cone shape. + */ + public radius(): number { + return this.colliderSet.raw.coRadius(this.handle); + } + + /** + * Sets the radius of this collider if it is a ball, cylinder, capsule, or cone shape. + * + * @param newRadius - desired radius. + */ + public setRadius(newRadius: number): void { + this.colliderSet.raw.coSetRadius(this.handle, newRadius); + } + + /** + * The radius of the round edges of this collider if it is a round cylinder. + */ + public roundRadius(): number { + return this.colliderSet.raw.coRoundRadius(this.handle); + } + + /** + * Sets the radius of the round edges of this collider if it has round edges. + * + * @param newBorderRadius - desired round edge radius. + */ + public setRoundRadius(newBorderRadius: number) { + this.colliderSet.raw.coSetRoundRadius(this.handle, newBorderRadius); + } + + /** + * The half height of this collider if it is a cylinder, capsule, or cone shape. + */ + public halfHeight(): number { + return this.colliderSet.raw.coHalfHeight(this.handle); + } + + /** + * Sets the half height of this collider if it is a cylinder, capsule, or cone shape. + * + * @param newHalfheight - desired half height. + */ + public setHalfHeight(newHalfheight: number) { + this.colliderSet.raw.coSetHalfHeight(this.handle, newHalfheight); + } + + /** + * If this collider has a Voxels shape, this will mark the voxel at the + * given grid coordinates as filled or empty (depending on the `filled` + * argument). + * + * Each input value is assumed to be an integer. + * + * The operation is O(1), unless the provided coordinates are out of the + * bounds of the currently allocated internal grid in which case the grid + * will be grown automatically. + */ + public setVoxel( + ix: number, + iy: number, + filled: boolean, + ) { + this.colliderSet.raw.coSetVoxel( + this.handle, + ix, + iy, + filled, + ); + // We modified the shape, invalidate it to keep our cache + // up-to-date the next time the user requests the shape data. + // PERF: this isn’t ideal for performances as this adds a + // hidden, non-constant, cost. + this._shape = null; + } + + /** + * If this and `voxels2` are voxel colliders, and a voxel from `this` was + * modified with `setVoxel`, this will ensure that a + * moving object transitioning across the boundaries of these colliders + * won’t suffer from the "internal edges" artifact. + * + * The indices `ix, iy, iz` indicate the integer coordinates of the voxel in + * the local coordinate frame of `this`. + * + * If the voxels in `voxels2` live in a different coordinate space from `this`, + * then the `shift_*` argument indicate the distance, in voxel units, between + * the origin of `this` to the origin of `voxels2`. + * + * This method is intended to be called between `this` and all the other + * voxels colliders with a domain intersecting `this` or sharing a domain + * boundary. This is an incremental maintenance of the effect of + * `combineVoxelStates`. + */ + public propagateVoxelChange( + voxels2: Collider, + ix: number, + iy: number, + shift_x: number, + shift_y: number, + ) { + this.colliderSet.raw.coPropagateVoxelChange( + this.handle, + voxels2.handle, + ix, + iy, + shift_x, + shift_y, + ); + // We modified the shape, invalidate it to keep our cache + // up-to-date the next time the user requests the shape data. + // PERF: this isn’t ideal for performances as this adds a + // hidden, non-constant, cost. + this._shape = null; + } + + /** + * If this and `voxels2` are voxel colliders, this will ensure that a + * moving object transitioning across the boundaries of these colliders + * won’t suffer from the "internal edges" artifact. + * + * If the voxels in `voxels2` live in a different coordinate space from `this`, + * then the `shift_*` argument indicate the distance, in voxel units, between + * the origin of `this` to the origin of `voxels2`. + * + * This method is intended to be called once between all pairs of voxels + * colliders with intersecting domains or shared boundaries. + * + * If either voxels collider is then modified with `setVoxel`, the + * `propagateVoxelChange` method must be called to maintain the coupling + * between the voxels shapes after the modification. + */ + public combineVoxelStates( + voxels2: Collider, + shift_x: number, + shift_y: number, + ) { + this.colliderSet.raw.coCombineVoxelStates( + this.handle, + voxels2.handle, + shift_x, + shift_y, + ); + // We modified the shape, invalidate it to keep our cache + // up-to-date the next time the user requests the shape data. + // PERF: this isn’t ideal for performances as this adds a + // hidden, non-constant, cost. + this._shape = null; + } + + /** + * If this collider has a triangle mesh, polyline, convex polygon, or convex polyhedron shape, + * this returns the vertex buffer of said shape. + */ + public vertices(): Float32Array { + return this.colliderSet.raw.coVertices(this.handle); + } + + /** + * If this collider has a triangle mesh, polyline, or convex polyhedron shape, + * this returns the index buffer of said shape. + */ + public indices(): Uint32Array | undefined { + return this.colliderSet.raw.coIndices(this.handle); + } + + /** + * If this collider has a heightfield shape, this returns the heights buffer of + * the heightfield. + * In 3D, the returned height matrix is provided in column-major order. + */ + public heightfieldHeights(): Float32Array { + return this.colliderSet.raw.coHeightfieldHeights(this.handle); + } + + /** + * If this collider has a heightfield shape, this returns the scale + * applied to it. + */ + public heightfieldScale(): Vector { + let scale = this.colliderSet.raw.coHeightfieldScale(this.handle); + return VectorOps.fromRaw(scale); + } + + + /** + * The rigid-body this collider is attached to. + */ + public parent(): RigidBody | null { + return this._parent; + } + + /** + * The friction coefficient of this collider. + */ + public friction(): number { + return this.colliderSet.raw.coFriction(this.handle); + } + + /** + * The restitution coefficient of this collider. + */ + public restitution(): number { + return this.colliderSet.raw.coRestitution(this.handle); + } + + /** + * The density of this collider. + */ + public density(): number { + return this.colliderSet.raw.coDensity(this.handle); + } + + /** + * The mass of this collider. + */ + public mass(): number { + return this.colliderSet.raw.coMass(this.handle); + } + + /** + * The volume of this collider. + */ + public volume(): number { + return this.colliderSet.raw.coVolume(this.handle); + } + + /** + * The collision groups of this collider. + */ + public collisionGroups(): InteractionGroups { + return this.colliderSet.raw.coCollisionGroups(this.handle); + } + + /** + * The solver groups of this collider. + */ + public solverGroups(): InteractionGroups { + return this.colliderSet.raw.coSolverGroups(this.handle); + } + + /** + * Tests if this collider contains a point. + * + * @param point - The point to test. + */ + public containsPoint(point: Vector): boolean { + let rawPoint = VectorOps.intoRaw(point); + let result = this.colliderSet.raw.coContainsPoint( + this.handle, + rawPoint, + ); + + rawPoint.free(); + + return result; + } + + /** + * Find the projection of a point on this collider. + * + * @param point - The point to project. + * @param solid - If this is set to `true` then the collider shapes are considered to + * be plain (if the point is located inside of a plain shape, its projection is the point + * itself). If it is set to `false` the collider shapes are considered to be hollow + * (if the point is located inside of an hollow shape, it is projected on the shape's + * boundary). + */ + public projectPoint(point: Vector, solid: boolean): PointProjection | null { + let rawPoint = VectorOps.intoRaw(point); + let result = PointProjection.fromRaw( + this.colliderSet.raw.coProjectPoint(this.handle, rawPoint, solid), + ); + + rawPoint.free(); + + return result; + } + + /** + * Tests if this collider intersects the given ray. + * + * @param ray - The ray to cast. + * @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively + * limits the length of the ray to `ray.dir.norm() * maxToi`. + */ + public intersectsRay(ray: Ray, maxToi: number): boolean { + let rawOrig = VectorOps.intoRaw(ray.origin); + let rawDir = VectorOps.intoRaw(ray.dir); + let result = this.colliderSet.raw.coIntersectsRay( + this.handle, + rawOrig, + rawDir, + maxToi, + ); + + rawOrig.free(); + rawDir.free(); + + return result; + } + + /* + * Computes the smallest time between this and the given shape under translational movement are separated by a distance smaller or equal to distance. + * + * @param collider1Vel - The constant velocity of the current shape to cast (i.e. the cast direction). + * @param shape2 - The shape to cast against. + * @param shape2Pos - The position of the second shape. + * @param shape2Rot - The rotation of the second shape. + * @param shape2Vel - The constant velocity of the second shape. + * @param targetDistance − If the shape moves closer to this distance from a collider, a hit + * will be returned. + * @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively + * limits the distance traveled by the shape to `collider1Vel.norm() * maxToi`. + * @param stopAtPenetration - If set to `false`, the linear shape-cast won’t immediately stop if + * the shape is penetrating another shape at its starting point **and** its trajectory is such + * that it’s on a path to exit that penetration state. + */ + public castShape( + collider1Vel: Vector, + shape2: Shape, + shape2Pos: Vector, + shape2Rot: Rotation, + shape2Vel: Vector, + targetDistance: number, + maxToi: number, + stopAtPenetration: boolean, + ): ShapeCastHit | null { + let rawCollider1Vel = VectorOps.intoRaw(collider1Vel); + let rawShape2Pos = VectorOps.intoRaw(shape2Pos); + let rawShape2Rot = RotationOps.intoRaw(shape2Rot); + let rawShape2Vel = VectorOps.intoRaw(shape2Vel); + let rawShape2 = shape2.intoRaw(); + + let result = ShapeCastHit.fromRaw( + this.colliderSet, + this.colliderSet.raw.coCastShape( + this.handle, + rawCollider1Vel, + rawShape2, + rawShape2Pos, + rawShape2Rot, + rawShape2Vel, + targetDistance, + maxToi, + stopAtPenetration, + ), + ); + + rawCollider1Vel.free(); + rawShape2Pos.free(); + rawShape2Rot.free(); + rawShape2Vel.free(); + rawShape2.free(); + + return result; + } + + /* + * Computes the smallest time between this and the given collider under translational movement are separated by a distance smaller or equal to distance. + * + * @param collider1Vel - The constant velocity of the current collider to cast (i.e. the cast direction). + * @param collider2 - The collider to cast against. + * @param collider2Vel - The constant velocity of the second collider. + * @param targetDistance − If the shape moves closer to this distance from a collider, a hit + * will be returned. + * @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively + * limits the distance traveled by the shape to `shapeVel.norm() * maxToi`. + * @param stopAtPenetration - If set to `false`, the linear shape-cast won’t immediately stop if + * the shape is penetrating another shape at its starting point **and** its trajectory is such + * that it’s on a path to exit that penetration state. + */ + public castCollider( + collider1Vel: Vector, + collider2: Collider, + collider2Vel: Vector, + targetDistance: number, + maxToi: number, + stopAtPenetration: boolean, + ): ColliderShapeCastHit | null { + let rawCollider1Vel = VectorOps.intoRaw(collider1Vel); + let rawCollider2Vel = VectorOps.intoRaw(collider2Vel); + + let result = ColliderShapeCastHit.fromRaw( + this.colliderSet, + this.colliderSet.raw.coCastCollider( + this.handle, + rawCollider1Vel, + collider2.handle, + rawCollider2Vel, + targetDistance, + maxToi, + stopAtPenetration, + ), + ); + + rawCollider1Vel.free(); + rawCollider2Vel.free(); + + return result; + } + + public intersectsShape( + shape2: Shape, + shapePos2: Vector, + shapeRot2: Rotation, + ): boolean { + let rawPos2 = VectorOps.intoRaw(shapePos2); + let rawRot2 = RotationOps.intoRaw(shapeRot2); + let rawShape2 = shape2.intoRaw(); + + let result = this.colliderSet.raw.coIntersectsShape( + this.handle, + rawShape2, + rawPos2, + rawRot2, + ); + + rawPos2.free(); + rawRot2.free(); + rawShape2.free(); + + return result; + } + + /** + * Computes one pair of contact points between the shape owned by this collider and the given shape. + * + * @param shape2 - The second shape. + * @param shape2Pos - The initial position of the second shape. + * @param shape2Rot - The rotation of the second shape. + * @param prediction - The prediction value, if the shapes are separated by a distance greater than this value, test will fail. + * @returns `null` if the shapes are separated by a distance greater than prediction, otherwise contact details. The result is given in world-space. + */ + contactShape( + shape2: Shape, + shape2Pos: Vector, + shape2Rot: Rotation, + prediction: number, + ): ShapeContact | null { + let rawPos2 = VectorOps.intoRaw(shape2Pos); + let rawRot2 = RotationOps.intoRaw(shape2Rot); + let rawShape2 = shape2.intoRaw(); + + let result = ShapeContact.fromRaw( + this.colliderSet.raw.coContactShape( + this.handle, + rawShape2, + rawPos2, + rawRot2, + prediction, + ), + ); + + rawPos2.free(); + rawRot2.free(); + rawShape2.free(); + + return result; + } + + /** + * Computes one pair of contact points between the collider and the given collider. + * + * @param collider2 - The second collider. + * @param prediction - The prediction value, if the shapes are separated by a distance greater than this value, test will fail. + * @returns `null` if the shapes are separated by a distance greater than prediction, otherwise contact details. The result is given in world-space. + */ + contactCollider( + collider2: Collider, + prediction: number, + ): ShapeContact | null { + let result = ShapeContact.fromRaw( + this.colliderSet.raw.coContactCollider( + this.handle, + collider2.handle, + prediction, + ), + ); + + return result; + } + + /** + * Find the closest intersection between a ray and this collider. + * + * This also computes the normal at the hit point. + * @param ray - The ray to cast. + * @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively + * limits the length of the ray to `ray.dir.norm() * maxToi`. + * @param solid - If `false` then the ray will attempt to hit the boundary of a shape, even if its + * origin already lies inside of a shape. In other terms, `true` implies that all shapes are plain, + * whereas `false` implies that all shapes are hollow for this ray-cast. + * @returns The time-of-impact between this collider and the ray, or `-1` if there is no intersection. + */ + public castRay(ray: Ray, maxToi: number, solid: boolean): number { + let rawOrig = VectorOps.intoRaw(ray.origin); + let rawDir = VectorOps.intoRaw(ray.dir); + let result = this.colliderSet.raw.coCastRay( + this.handle, + rawOrig, + rawDir, + maxToi, + solid, + ); + + rawOrig.free(); + rawDir.free(); + + return result; + } + + /** + * Find the closest intersection between a ray and this collider. + * + * This also computes the normal at the hit point. + * @param ray - The ray to cast. + * @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively + * limits the length of the ray to `ray.dir.norm() * maxToi`. + * @param solid - If `false` then the ray will attempt to hit the boundary of a shape, even if its + * origin already lies inside of a shape. In other terms, `true` implies that all shapes are plain, + * whereas `false` implies that all shapes are hollow for this ray-cast. + */ + public castRayAndGetNormal( + ray: Ray, + maxToi: number, + solid: boolean, + ): RayIntersection | null { + let rawOrig = VectorOps.intoRaw(ray.origin); + let rawDir = VectorOps.intoRaw(ray.dir); + let result = RayIntersection.fromRaw( + this.colliderSet.raw.coCastRayAndGetNormal( + this.handle, + rawOrig, + rawDir, + maxToi, + solid, + ), + ); + + rawOrig.free(); + rawDir.free(); + + return result; + } +} + +export enum MassPropsMode { + Density, + Mass, + MassProps, +} + +export class ColliderDesc { + enabled: boolean; + shape: Shape; + massPropsMode: MassPropsMode; + mass: number; + centerOfMass: Vector; + principalAngularInertia: number; + rotationsEnabled: boolean; + density: number; + friction: number; + restitution: number; + rotation: Rotation; + translation: Vector; + isSensor: boolean; + collisionGroups: InteractionGroups; + solverGroups: InteractionGroups; + frictionCombineRule: CoefficientCombineRule; + restitutionCombineRule: CoefficientCombineRule; + activeEvents: ActiveEvents; + activeHooks: ActiveHooks; + activeCollisionTypes: ActiveCollisionTypes; + contactForceEventThreshold: number; + contactSkin: number; + + /** + * Initializes a collider descriptor from the collision shape. + * + * @param shape - The shape of the collider being built. + */ + constructor(shape: Shape) { + this.enabled = true; + this.shape = shape; + this.massPropsMode = MassPropsMode.Density; + this.density = 1.0; + this.friction = 0.5; + this.restitution = 0.0; + this.rotation = RotationOps.identity(); + this.translation = VectorOps.zeros(); + this.isSensor = false; + this.collisionGroups = 0xffff_ffff; + this.solverGroups = 0xffff_ffff; + this.frictionCombineRule = CoefficientCombineRule.Average; + this.restitutionCombineRule = CoefficientCombineRule.Average; + this.activeCollisionTypes = ActiveCollisionTypes.DEFAULT; + this.activeEvents = ActiveEvents.NONE; + this.activeHooks = ActiveHooks.NONE; + this.mass = 0.0; + this.centerOfMass = VectorOps.zeros(); + this.contactForceEventThreshold = 0.0; + this.contactSkin = 0.0; + + this.principalAngularInertia = 0.0; + this.rotationsEnabled = true; + } + + /** + * Create a new collider descriptor with a ball shape. + * + * @param radius - The radius of the ball. + */ + public static ball(radius: number): ColliderDesc { + const shape = new Ball(radius); + return new ColliderDesc(shape); + } + + /** + * Create a new collider descriptor with a capsule shape. + * + * @param halfHeight - The half-height of the capsule, along the `y` axis. + * @param radius - The radius of the capsule basis. + */ + public static capsule(halfHeight: number, radius: number): ColliderDesc { + const shape = new Capsule(halfHeight, radius); + return new ColliderDesc(shape); + } + + /** + * Creates a new segment shape. + * + * @param a - The first point of the segment. + * @param b - The second point of the segment. + */ + public static segment(a: Vector, b: Vector): ColliderDesc { + const shape = new Segment(a, b); + return new ColliderDesc(shape); + } + + /** + * Creates a new triangle shape. + * + * @param a - The first point of the triangle. + * @param b - The second point of the triangle. + * @param c - The third point of the triangle. + */ + public static triangle(a: Vector, b: Vector, c: Vector): ColliderDesc { + const shape = new Triangle(a, b, c); + return new ColliderDesc(shape); + } + + /** + * Creates a new triangle shape with round corners. + * + * @param a - The first point of the triangle. + * @param b - The second point of the triangle. + * @param c - The third point of the triangle. + * @param borderRadius - The radius of the borders of this triangle. In 3D, + * this is also equal to half the thickness of the triangle. + */ + public static roundTriangle( + a: Vector, + b: Vector, + c: Vector, + borderRadius: number, + ): ColliderDesc { + const shape = new RoundTriangle(a, b, c, borderRadius); + return new ColliderDesc(shape); + } + + /** + * Creates a new collider descriptor with a polyline shape. + * + * @param vertices - The coordinates of the polyline's vertices. + * @param indices - The indices of the polyline's segments. If this is `undefined` or `null`, + * the vertices are assumed to describe a line strip. + */ + public static polyline( + vertices: Float32Array, + indices?: Uint32Array | null, + ): ColliderDesc { + const shape = new Polyline(vertices, indices); + return new ColliderDesc(shape); + } + + /** + * Creates a new collider descriptor with a shape made of voxels. + * + * @param data - Defines the set of voxels. If this is a `Int32Array` then + * each voxel is defined from its (signed) grid coordinates, + * with 3 (resp 2) contiguous integers per voxel in 3D (resp 2D). + * If this is a `Float32Array`, each voxel will be such that + * they contain at least one point from this array (where each + * point is defined from 3 (resp 2) contiguous numbers per point + * in 3D (resp 2D). + * @param voxelSize - The size of each voxel. + */ + public static voxels( + voxels: Float32Array | Int32Array, + voxelSize: Vector, + ): ColliderDesc { + const shape = new Voxels(voxels, voxelSize); + return new ColliderDesc(shape); + } + + /** + * Creates a new collider descriptor with a triangle mesh shape. + * + * @param vertices - The coordinates of the triangle mesh's vertices. + * @param indices - The indices of the triangle mesh's triangles. + */ + public static trimesh( + vertices: Float32Array, + indices: Uint32Array, + flags?: TriMeshFlags, + ): ColliderDesc { + const shape = new TriMesh(vertices, indices, flags); + return new ColliderDesc(shape); + } + + /** + * Creates a new collider descriptor with a rectangular shape. + * + * @param hx - The half-width of the rectangle along its local `x` axis. + * @param hy - The half-width of the rectangle along its local `y` axis. + */ + public static cuboid(hx: number, hy: number): ColliderDesc { + const shape = new Cuboid(hx, hy); + return new ColliderDesc(shape); + } + + /** + * Creates a new collider descriptor with a rectangular shape with round borders. + * + * @param hx - The half-width of the rectangle along its local `x` axis. + * @param hy - The half-width of the rectangle along its local `y` axis. + * @param borderRadius - The radius of the cuboid's borders. + */ + public static roundCuboid( + hx: number, + hy: number, + borderRadius: number, + ): ColliderDesc { + const shape = new RoundCuboid(hx, hy, borderRadius); + return new ColliderDesc(shape); + } + + /** + * Creates a new collider description with a halfspace (infinite plane) shape. + * + * @param normal - The outward normal of the plane. + */ + public static halfspace(normal: Vector): ColliderDesc { + const shape = new HalfSpace(normal); + return new ColliderDesc(shape); + } + + /** + * Creates a new collider descriptor with a heightfield shape. + * + * @param heights - The heights of the heightfield, along its local `y` axis. + * @param scale - The scale factor applied to the heightfield. + */ + public static heightfield( + heights: Float32Array, + scale: Vector, + ): ColliderDesc { + const shape = new Heightfield(heights, scale); + return new ColliderDesc(shape); + } + + /** + * Computes the convex-hull of the given points and use the resulting + * convex polygon as the shape for this new collider descriptor. + * + * @param points - The point that will be used to compute the convex-hull. + */ + public static convexHull(points: Float32Array): ColliderDesc | null { + const shape = new ConvexPolygon(points, false); + return new ColliderDesc(shape); + } + + /** + * Creates a new collider descriptor that uses the given set of points assumed + * to form a convex polyline (no convex-hull computation will be done). + * + * @param vertices - The vertices of the convex polyline. + */ + public static convexPolyline(vertices: Float32Array): ColliderDesc | null { + const shape = new ConvexPolygon(vertices, true); + return new ColliderDesc(shape); + } + + /** + * Computes the convex-hull of the given points and use the resulting + * convex polygon as the shape for this new collider descriptor. A + * border is added to that convex polygon to give it round corners. + * + * @param points - The point that will be used to compute the convex-hull. + * @param borderRadius - The radius of the round border added to the convex polygon. + */ + public static roundConvexHull( + points: Float32Array, + borderRadius: number, + ): ColliderDesc | null { + const shape = new RoundConvexPolygon(points, borderRadius, false); + return new ColliderDesc(shape); + } + + /** + * Creates a new collider descriptor that uses the given set of points assumed + * to form a round convex polyline (no convex-hull computation will be done). + * + * @param vertices - The vertices of the convex polyline. + * @param borderRadius - The radius of the round border added to the convex polyline. + */ + public static roundConvexPolyline( + vertices: Float32Array, + borderRadius: number, + ): ColliderDesc | null { + const shape = new RoundConvexPolygon(vertices, borderRadius, true); + return new ColliderDesc(shape); + } + + + + /** + * Sets the position of the collider to be created relative to the rigid-body it is attached to. + */ + public setTranslation(x: number, y: number): ColliderDesc { + if (typeof x != "number" || typeof y != "number") + throw TypeError("The translation components must be numbers."); + + this.translation = {x: x, y: y}; + return this; + } + + + + /** + * Sets the rotation of the collider to be created relative to the rigid-body it is attached to. + * + * @param rot - The rotation of the collider to be created relative to the rigid-body it is attached to. + */ + public setRotation(rot: Rotation): ColliderDesc { + this.rotation = rot; + return this; + } + + /** + * Sets whether or not the collider being created is a sensor. + * + * A sensor collider does not take part of the physics simulation, but generates + * proximity events. + * + * @param sensor - Set to `true` of the collider built is to be a sensor. + */ + public setSensor(sensor: boolean): ColliderDesc { + this.isSensor = sensor; + return this; + } + + /** + * Sets whether the created collider will be enabled or disabled. + * @param enabled − If set to `false` the collider will be disabled at creation. + */ + public setEnabled(enabled: boolean): ColliderDesc { + this.enabled = enabled; + return this; + } + + /** + * Sets the contact skin of the collider. + * + * The contact skin acts as if the collider was enlarged with a skin of width `skin_thickness` + * around it, keeping objects further apart when colliding. + * + * A non-zero contact skin can increase performance, and in some cases, stability. However + * it creates a small gap between colliding object (equal to the sum of their skin). If the + * skin is sufficiently small, this might not be visually significant or can be hidden by the + * rendering assets. + */ + public setContactSkin(thickness: number): ColliderDesc { + this.contactSkin = thickness; + return this; + } + + /** + * Sets the density of the collider being built. + * + * The mass and angular inertia tensor will be computed automatically based on this density and the collider’s shape. + * + * @param density - The density to set, must be greater or equal to 0. A density of 0 means that this collider + * will not affect the mass or angular inertia of the rigid-body it is attached to. + */ + public setDensity(density: number): ColliderDesc { + this.massPropsMode = MassPropsMode.Density; + this.density = density; + return this; + } + + /** + * Sets the mass of the collider being built. + * + * The angular inertia tensor will be computed automatically based on this mass and the collider’s shape. + * + * @param mass - The mass to set, must be greater or equal to 0. + */ + public setMass(mass: number): ColliderDesc { + this.massPropsMode = MassPropsMode.Mass; + this.mass = mass; + return this; + } + + /** + * Sets the mass properties of the collider being built. + * + * This replaces the mass-properties automatically computed from the collider's density and shape. + * These mass-properties will be added to the mass-properties of the rigid-body this collider will be attached to. + * + * @param mass − The mass of the collider to create. + * @param centerOfMass − The center-of-mass of the collider to create. + * @param principalAngularInertia − The principal angular inertia of the collider to create. + */ + public setMassProperties( + mass: number, + centerOfMass: Vector, + principalAngularInertia: number, + ): ColliderDesc { + this.massPropsMode = MassPropsMode.MassProps; + this.mass = mass; + VectorOps.copy(this.centerOfMass, centerOfMass); + this.principalAngularInertia = principalAngularInertia; + return this; + } + + + + /** + * Sets the restitution coefficient of the collider to be created. + * + * @param restitution - The restitution coefficient in `[0, 1]`. A value of 0 (the default) means no bouncing behavior + * while 1 means perfect bouncing (though energy may still be lost due to numerical errors of the + * constraints solver). + */ + public setRestitution(restitution: number): ColliderDesc { + this.restitution = restitution; + return this; + } + + /** + * Sets the friction coefficient of the collider to be created. + * + * @param friction - The friction coefficient. Must be greater or equal to 0. This is generally smaller than 1. The + * higher the coefficient, the stronger friction forces will be for contacts with the collider + * being built. + */ + public setFriction(friction: number): ColliderDesc { + this.friction = friction; + return this; + } + + /** + * Sets the rule used to combine the friction coefficients of two colliders + * colliders involved in a contact. + * + * @param rule − The combine rule to apply. + */ + public setFrictionCombineRule(rule: CoefficientCombineRule): ColliderDesc { + this.frictionCombineRule = rule; + return this; + } + + /** + * Sets the rule used to combine the restitution coefficients of two colliders + * colliders involved in a contact. + * + * @param rule − The combine rule to apply. + */ + public setRestitutionCombineRule( + rule: CoefficientCombineRule, + ): ColliderDesc { + this.restitutionCombineRule = rule; + return this; + } + + /** + * Sets the collision groups used by this collider. + * + * Two colliders will interact iff. their collision groups are compatible. + * See the documentation of `InteractionGroups` for details on teh used bit pattern. + * + * @param groups - The collision groups used for the collider being built. + */ + public setCollisionGroups(groups: InteractionGroups): ColliderDesc { + this.collisionGroups = groups; + return this; + } + + /** + * Sets the solver groups used by this collider. + * + * Forces between two colliders in contact will be computed iff their solver + * groups are compatible. + * See the documentation of `InteractionGroups` for details on the used bit pattern. + * + * @param groups - The solver groups used for the collider being built. + */ + public setSolverGroups(groups: InteractionGroups): ColliderDesc { + this.solverGroups = groups; + return this; + } + + /** + * Set the physics hooks active for this collider. + * + * Use this to enable custom filtering rules for contact/intersecstion pairs involving this collider. + * + * @param activeHooks - The hooks active for contact/intersection pairs involving this collider. + */ + public setActiveHooks(activeHooks: ActiveHooks): ColliderDesc { + this.activeHooks = activeHooks; + return this; + } + + /** + * Set the events active for this collider. + * + * Use this to enable contact and/or intersection event reporting for this collider. + * + * @param activeEvents - The events active for contact/intersection pairs involving this collider. + */ + public setActiveEvents(activeEvents: ActiveEvents): ColliderDesc { + this.activeEvents = activeEvents; + return this; + } + + /** + * Set the collision types active for this collider. + * + * @param activeCollisionTypes - The hooks active for contact/intersection pairs involving this collider. + */ + public setActiveCollisionTypes( + activeCollisionTypes: ActiveCollisionTypes, + ): ColliderDesc { + this.activeCollisionTypes = activeCollisionTypes; + return this; + } + + /** + * Sets the total force magnitude beyond which a contact force event can be emitted. + * + * @param threshold - The force threshold to set. + */ + public setContactForceEventThreshold(threshold: number): ColliderDesc { + this.contactForceEventThreshold = threshold; + return this; + } +} diff --git a/packages/rapier2d/src/geometry/collider_set.ts b/packages/rapier2d/src/geometry/collider_set.ts new file mode 100644 index 00000000..912f3c1e --- /dev/null +++ b/packages/rapier2d/src/geometry/collider_set.ts @@ -0,0 +1,195 @@ +import {RawColliderSet} from "../raw"; +import {Coarena} from "../coarena"; +import {RotationOps, VectorOps} from "../math"; +import {Collider, ColliderDesc, ColliderHandle} from "./collider"; +import {ImpulseJointHandle, IslandManager, RigidBodyHandle} from "../dynamics"; +import {RigidBodySet} from "../dynamics"; + +/** + * A set of rigid bodies that can be handled by a physics pipeline. + * + * To avoid leaking WASM resources, this MUST be freed manually with `colliderSet.free()` + * once you are done using it (and all the rigid-bodies it created). + */ +export class ColliderSet { + raw: RawColliderSet; + private map: Coarena; + + /** + * Release the WASM memory occupied by this collider set. + */ + public free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + + if (!!this.map) { + this.map.clear(); + } + this.map = undefined; + } + + constructor(raw?: RawColliderSet) { + this.raw = raw || new RawColliderSet(); + this.map = new Coarena(); + // Initialize the map with the existing elements, if any. + if (raw) { + raw.forEachColliderHandle((handle: ColliderHandle) => { + this.map.set(handle, new Collider(this, handle, null)); + }); + } + } + + /** @internal */ + public castClosure( + f?: (collider: Collider) => Res, + ): (handle: ColliderHandle) => Res | undefined { + return (handle) => { + if (!!f) { + return f(this.get(handle)); + } else { + return undefined; + } + }; + } + + /** @internal */ + public finalizeDeserialization(bodies: RigidBodySet) { + this.map.forEach((collider) => + collider.finalizeDeserialization(bodies), + ); + } + + /** + * Creates a new collider and return its integer handle. + * + * @param bodies - The set of bodies where the collider's parent can be found. + * @param desc - The collider's description. + * @param parentHandle - The integer handle of the rigid-body this collider is attached to. + */ + public createCollider( + bodies: RigidBodySet, + desc: ColliderDesc, + parentHandle: RigidBodyHandle, + ): Collider { + let hasParent = parentHandle != undefined && parentHandle != null; + + if (hasParent && isNaN(parentHandle)) + throw Error( + "Cannot create a collider with a parent rigid-body handle that is not a number.", + ); + + let rawShape = desc.shape.intoRaw(); + let rawTra = VectorOps.intoRaw(desc.translation); + let rawRot = RotationOps.intoRaw(desc.rotation); + let rawCom = VectorOps.intoRaw(desc.centerOfMass); + + + let handle = this.raw.createCollider( + desc.enabled, + rawShape, + rawTra, + rawRot, + desc.massPropsMode, + desc.mass, + rawCom, + desc.principalAngularInertia, + desc.density, + desc.friction, + desc.restitution, + desc.frictionCombineRule, + desc.restitutionCombineRule, + desc.isSensor, + desc.collisionGroups, + desc.solverGroups, + desc.activeCollisionTypes, + desc.activeHooks, + desc.activeEvents, + desc.contactForceEventThreshold, + desc.contactSkin, + hasParent, + hasParent ? parentHandle : 0, + bodies.raw, + ); + + rawShape.free(); + rawTra.free(); + rawRot.free(); + rawCom.free(); + + + let parent = hasParent ? bodies.get(parentHandle) : null; + let collider = new Collider(this, handle, parent, desc.shape); + this.map.set(handle, collider); + return collider; + } + + /** + * Remove a collider from this set. + * + * @param handle - The integer handle of the collider to remove. + * @param bodies - The set of rigid-body containing the rigid-body the collider is attached to. + * @param wakeUp - If `true`, the rigid-body the removed collider is attached to will be woken-up automatically. + */ + public remove( + handle: ColliderHandle, + islands: IslandManager, + bodies: RigidBodySet, + wakeUp: boolean, + ) { + this.raw.remove(handle, islands.raw, bodies.raw, wakeUp); + this.unmap(handle); + } + + /** + * Internal function, do not call directly. + * @param handle + */ + public unmap(handle: ImpulseJointHandle) { + this.map.delete(handle); + } + + /** + * Gets the rigid-body with the given handle. + * + * @param handle - The handle of the rigid-body to retrieve. + */ + public get(handle: ColliderHandle): Collider | null { + return this.map.get(handle); + } + + /** + * The number of colliders on this set. + */ + public len(): number { + return this.map.len(); + } + + /** + * Does this set contain a collider with the given handle? + * + * @param handle - The collider handle to check. + */ + public contains(handle: ColliderHandle): boolean { + return this.get(handle) != null; + } + + /** + * Applies the given closure to each collider contained by this set. + * + * @param f - The closure to apply. + */ + public forEach(f: (collider: Collider) => void) { + this.map.forEach(f); + } + + /** + * Gets all colliders in the list. + * + * @returns collider list. + */ + public getAll(): Collider[] { + return this.map.getAll(); + } +} diff --git a/packages/rapier2d/src/geometry/contact.ts b/packages/rapier2d/src/geometry/contact.ts new file mode 100644 index 00000000..802f57f4 --- /dev/null +++ b/packages/rapier2d/src/geometry/contact.ts @@ -0,0 +1,62 @@ +import {Vector, VectorOps} from "../math"; +import {RawShapeContact} from "../raw"; + +/** + * The contact info between two shapes. + */ +export class ShapeContact { + /** + * Distance between the two contact points. + * If this is negative, this contact represents a penetration. + */ + distance: number; + + /** + * Position of the contact on the first shape. + */ + point1: Vector; + + /** + * Position of the contact on the second shape. + */ + point2: Vector; + + /** + * Contact normal, pointing towards the exterior of the first shape. + */ + normal1: Vector; + + /** + * Contact normal, pointing towards the exterior of the second shape. + * If these contact data are expressed in world-space, this normal is equal to -normal1. + */ + normal2: Vector; + + constructor( + dist: number, + point1: Vector, + point2: Vector, + normal1: Vector, + normal2: Vector, + ) { + this.distance = dist; + this.point1 = point1; + this.point2 = point2; + this.normal1 = normal1; + this.normal2 = normal2; + } + + public static fromRaw(raw: RawShapeContact): ShapeContact { + if (!raw) return null; + + const result = new ShapeContact( + raw.distance(), + VectorOps.fromRaw(raw.point1()), + VectorOps.fromRaw(raw.point2()), + VectorOps.fromRaw(raw.normal1()), + VectorOps.fromRaw(raw.normal2()), + ); + raw.free(); + return result; + } +} diff --git a/packages/rapier2d/src/geometry/feature.ts b/packages/rapier2d/src/geometry/feature.ts new file mode 100644 index 00000000..51752b9c --- /dev/null +++ b/packages/rapier2d/src/geometry/feature.ts @@ -0,0 +1,6 @@ +export enum FeatureType { + Vertex, + Face, + Unknown, +} + diff --git a/packages/rapier2d/src/geometry/index.ts b/packages/rapier2d/src/geometry/index.ts new file mode 100644 index 00000000..1bd19e50 --- /dev/null +++ b/packages/rapier2d/src/geometry/index.ts @@ -0,0 +1,11 @@ +export * from "./broad_phase"; +export * from "./narrow_phase"; +export * from "./shape"; +export * from "./collider"; +export * from "./collider_set"; +export * from "./feature"; +export * from "./ray"; +export * from "./point"; +export * from "./toi"; +export * from "./interaction_groups"; +export * from "./contact"; diff --git a/packages/rapier2d/src/geometry/interaction_groups.ts b/packages/rapier2d/src/geometry/interaction_groups.ts new file mode 100644 index 00000000..4d73fd31 --- /dev/null +++ b/packages/rapier2d/src/geometry/interaction_groups.ts @@ -0,0 +1,18 @@ +/** + * Pairwise filtering using bit masks. + * + * This filtering method is based on two 16-bit values: + * - The interaction groups (the 16 left-most bits of `self.0`). + * - The interaction mask (the 16 right-most bits of `self.0`). + * + * An interaction is allowed between two filters `a` and `b` two conditions + * are met simultaneously: + * - The interaction groups of `a` has at least one bit set to `1` in common with the interaction mask of `b`. + * - The interaction groups of `b` has at least one bit set to `1` in common with the interaction mask of `a`. + * In other words, interactions are allowed between two filter iff. the following condition is met: + * + * ``` + * ((a >> 16) & b) != 0 && ((b >> 16) & a) != 0 + * ``` + */ +export type InteractionGroups = number; diff --git a/packages/rapier2d/src/geometry/narrow_phase.ts b/packages/rapier2d/src/geometry/narrow_phase.ts new file mode 100644 index 00000000..03b14710 --- /dev/null +++ b/packages/rapier2d/src/geometry/narrow_phase.ts @@ -0,0 +1,192 @@ +import {RawNarrowPhase, RawContactManifold} from "../raw"; +import {ColliderHandle} from "./collider"; +import {Vector, VectorOps} from "../math"; + +/** + * The narrow-phase used for precise collision-detection. + * + * To avoid leaking WASM resources, this MUST be freed manually with `narrowPhase.free()` + * once you are done using it. + */ +export class NarrowPhase { + raw: RawNarrowPhase; + tempManifold: TempContactManifold; + + /** + * Release the WASM memory occupied by this narrow-phase. + */ + public free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + } + + constructor(raw?: RawNarrowPhase) { + this.raw = raw || new RawNarrowPhase(); + this.tempManifold = new TempContactManifold(null); + } + + /** + * Enumerates all the colliders potentially in contact with the given collider. + * + * @param collider1 - The second collider involved in the contact. + * @param f - Closure that will be called on each collider that is in contact with `collider1`. + */ + public contactPairsWith( + collider1: ColliderHandle, + f: (collider2: ColliderHandle) => void, + ) { + this.raw.contact_pairs_with(collider1, f); + } + + /** + * Enumerates all the colliders intersecting the given colliders, assuming one of them + * is a sensor. + */ + public intersectionPairsWith( + collider1: ColliderHandle, + f: (collider2: ColliderHandle) => void, + ) { + this.raw.intersection_pairs_with(collider1, f); + } + + /** + * Iterates through all the contact manifolds between the given pair of colliders. + * + * @param collider1 - The first collider involved in the contact. + * @param collider2 - The second collider involved in the contact. + * @param f - Closure that will be called on each contact manifold between the two colliders. If the second argument + * passed to this closure is `true`, then the contact manifold data is flipped, i.e., methods like `localNormal1` + * actually apply to the `collider2` and fields like `localNormal2` apply to the `collider1`. + */ + public contactPair( + collider1: ColliderHandle, + collider2: ColliderHandle, + f: (manifold: TempContactManifold, flipped: boolean) => void, + ) { + const rawPair = this.raw.contact_pair(collider1, collider2); + + if (!!rawPair) { + const flipped = rawPair.collider1() != collider1; + + let i; + for (i = 0; i < rawPair.numContactManifolds(); ++i) { + this.tempManifold.raw = rawPair.contactManifold(i); + if (!!this.tempManifold.raw) { + f(this.tempManifold, flipped); + } + + // SAFETY: The RawContactManifold stores a raw pointer that will be invalidated + // at the next timestep. So we must be sure to free the pair here + // to avoid unsoundness in the Rust code. + this.tempManifold.free(); + } + rawPair.free(); + } + } + + /** + * Returns `true` if `collider1` and `collider2` intersect and at least one of them is a sensor. + * @param collider1 − The first collider involved in the intersection. + * @param collider2 − The second collider involved in the intersection. + */ + public intersectionPair( + collider1: ColliderHandle, + collider2: ColliderHandle, + ): boolean { + return this.raw.intersection_pair(collider1, collider2); + } +} + +export class TempContactManifold { + raw: RawContactManifold; + + public free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + } + + constructor(raw: RawContactManifold) { + this.raw = raw; + } + + public normal(): Vector { + return VectorOps.fromRaw(this.raw.normal()); + } + + public localNormal1(): Vector { + return VectorOps.fromRaw(this.raw.local_n1()); + } + + public localNormal2(): Vector { + return VectorOps.fromRaw(this.raw.local_n2()); + } + + public subshape1(): number { + return this.raw.subshape1(); + } + + public subshape2(): number { + return this.raw.subshape2(); + } + + public numContacts(): number { + return this.raw.num_contacts(); + } + + public localContactPoint1(i: number): Vector | null { + return VectorOps.fromRaw(this.raw.contact_local_p1(i)); + } + + public localContactPoint2(i: number): Vector | null { + return VectorOps.fromRaw(this.raw.contact_local_p2(i)); + } + + public contactDist(i: number): number { + return this.raw.contact_dist(i); + } + + public contactFid1(i: number): number { + return this.raw.contact_fid1(i); + } + + public contactFid2(i: number): number { + return this.raw.contact_fid2(i); + } + + public contactImpulse(i: number): number { + return this.raw.contact_impulse(i); + } + + public contactTangentImpulse(i: number): number { + return this.raw.contact_tangent_impulse(i); + } + + + public numSolverContacts(): number { + return this.raw.num_solver_contacts(); + } + + public solverContactPoint(i: number): Vector { + return VectorOps.fromRaw(this.raw.solver_contact_point(i)); + } + + public solverContactDist(i: number): number { + return this.raw.solver_contact_dist(i); + } + + public solverContactFriction(i: number): number { + return this.raw.solver_contact_friction(i); + } + + public solverContactRestitution(i: number): number { + return this.raw.solver_contact_restitution(i); + } + + public solverContactTangentVelocity(i: number): Vector { + return VectorOps.fromRaw(this.raw.solver_contact_tangent_velocity(i)); + } +} diff --git a/packages/rapier2d/src/geometry/point.ts b/packages/rapier2d/src/geometry/point.ts new file mode 100644 index 00000000..d272b7e8 --- /dev/null +++ b/packages/rapier2d/src/geometry/point.ts @@ -0,0 +1,98 @@ +import {Collider, ColliderHandle} from "./collider"; +import {Vector, VectorOps} from "../math"; +import { + RawFeatureType, + RawPointColliderProjection, + RawPointProjection, +} from "../raw"; +import {FeatureType} from "./feature"; +import {ColliderSet} from "./collider_set"; + +/** + * The projection of a point on a collider. + */ +export class PointProjection { + /** + * The projection of the point on the collider. + */ + point: Vector; + /** + * Is the point inside of the collider? + */ + isInside: boolean; + + constructor(point: Vector, isInside: boolean) { + this.point = point; + this.isInside = isInside; + } + + public static fromRaw(raw: RawPointProjection): PointProjection { + if (!raw) return null; + + const result = new PointProjection( + VectorOps.fromRaw(raw.point()), + raw.isInside(), + ); + raw.free(); + return result; + } +} + +/** + * The projection of a point on a collider (includes the collider handle). + */ +export class PointColliderProjection { + /** + * The collider hit by the ray. + */ + collider: Collider; + /** + * The projection of the point on the collider. + */ + point: Vector; + /** + * Is the point inside of the collider? + */ + isInside: boolean; + + /** + * The type of the geometric feature the point was projected on. + */ + featureType = FeatureType.Unknown; + + /** + * The id of the geometric feature the point was projected on. + */ + featureId: number | undefined = undefined; + + constructor( + collider: Collider, + point: Vector, + isInside: boolean, + featureType?: FeatureType, + featureId?: number, + ) { + this.collider = collider; + this.point = point; + this.isInside = isInside; + if (featureId !== undefined) this.featureId = featureId; + if (featureType !== undefined) this.featureType = featureType; + } + + public static fromRaw( + colliderSet: ColliderSet, + raw: RawPointColliderProjection, + ): PointColliderProjection { + if (!raw) return null; + + const result = new PointColliderProjection( + colliderSet.get(raw.colliderHandle()), + VectorOps.fromRaw(raw.point()), + raw.isInside(), + raw.featureType() as number as FeatureType, + raw.featureId(), + ); + raw.free(); + return result; + } +} diff --git a/packages/rapier2d/src/geometry/ray.ts b/packages/rapier2d/src/geometry/ray.ts new file mode 100644 index 00000000..a743692c --- /dev/null +++ b/packages/rapier2d/src/geometry/ray.ts @@ -0,0 +1,189 @@ +import {Vector, VectorOps} from "../math"; +import { + RawFeatureType, + RawRayColliderIntersection, + RawRayColliderHit, + RawRayIntersection, +} from "../raw"; +import {Collider} from "./collider"; +import {FeatureType} from "./feature"; +import {ColliderSet} from "./collider_set"; + +/** + * A ray. This is a directed half-line. + */ +export class Ray { + /** + * The starting point of the ray. + */ + public origin: Vector; + /** + * The direction of propagation of the ray. + */ + public dir: Vector; + + /** + * Builds a ray from its origin and direction. + * + * @param origin - The ray's starting point. + * @param dir - The ray's direction of propagation. + */ + constructor(origin: Vector, dir: Vector) { + this.origin = origin; + this.dir = dir; + } + + public pointAt(t: number): Vector { + return { + x: this.origin.x + this.dir.x * t, + y: this.origin.y + this.dir.y * t, + }; + } +} + +/** + * The intersection between a ray and a collider. + */ +export class RayIntersection { + /** + * The time-of-impact of the ray with the collider. + * + * The hit point is obtained from the ray's origin and direction: `origin + dir * timeOfImpact`. + */ + timeOfImpact: number; + /** + * The normal of the collider at the hit point. + */ + normal: Vector; + + /** + * The type of the geometric feature the point was projected on. + */ + featureType = FeatureType.Unknown; + + /** + * The id of the geometric feature the point was projected on. + */ + featureId: number | undefined = undefined; + + constructor( + timeOfImpact: number, + normal: Vector, + featureType?: FeatureType, + featureId?: number, + ) { + this.timeOfImpact = timeOfImpact; + this.normal = normal; + if (featureId !== undefined) this.featureId = featureId; + if (featureType !== undefined) this.featureType = featureType; + } + + public static fromRaw(raw: RawRayIntersection): RayIntersection { + if (!raw) return null; + + const result = new RayIntersection( + raw.time_of_impact(), + VectorOps.fromRaw(raw.normal()), + raw.featureType() as number as FeatureType, + raw.featureId(), + ); + raw.free(); + return result; + } +} + +/** + * The intersection between a ray and a collider (includes the collider handle). + */ +export class RayColliderIntersection { + /** + * The collider hit by the ray. + */ + collider: Collider; + /** + * The time-of-impact of the ray with the collider. + * + * The hit point is obtained from the ray's origin and direction: `origin + dir * timeOfImpact`. + */ + timeOfImpact: number; + /** + * The normal of the collider at the hit point. + */ + normal: Vector; + + /** + * The type of the geometric feature the point was projected on. + */ + featureType = FeatureType.Unknown; + + /** + * The id of the geometric feature the point was projected on. + */ + featureId: number | undefined = undefined; + + constructor( + collider: Collider, + timeOfImpact: number, + normal: Vector, + featureType?: FeatureType, + featureId?: number, + ) { + this.collider = collider; + this.timeOfImpact = timeOfImpact; + this.normal = normal; + if (featureId !== undefined) this.featureId = featureId; + if (featureType !== undefined) this.featureType = featureType; + } + + public static fromRaw( + colliderSet: ColliderSet, + raw: RawRayColliderIntersection, + ): RayColliderIntersection { + if (!raw) return null; + + const result = new RayColliderIntersection( + colliderSet.get(raw.colliderHandle()), + raw.time_of_impact(), + VectorOps.fromRaw(raw.normal()), + raw.featureType() as number as FeatureType, + raw.featureId(), + ); + raw.free(); + return result; + } +} + +/** + * The time of impact between a ray and a collider. + */ +export class RayColliderHit { + /** + * The handle of the collider hit by the ray. + */ + collider: Collider; + /** + * The time-of-impact of the ray with the collider. + * + * The hit point is obtained from the ray's origin and direction: `origin + dir * timeOfImpact`. + */ + timeOfImpact: number; + + constructor(collider: Collider, timeOfImpact: number) { + this.collider = collider; + this.timeOfImpact = timeOfImpact; + } + + public static fromRaw( + colliderSet: ColliderSet, + raw: RawRayColliderHit, + ): RayColliderHit { + if (!raw) return null; + + const result = new RayColliderHit( + colliderSet.get(raw.colliderHandle()), + raw.timeOfImpact(), + ); + raw.free(); + return result; + } +} diff --git a/packages/rapier2d/src/geometry/shape.ts b/packages/rapier2d/src/geometry/shape.ts new file mode 100644 index 00000000..4fdf8298 --- /dev/null +++ b/packages/rapier2d/src/geometry/shape.ts @@ -0,0 +1,1060 @@ +import {Vector, VectorOps, Rotation, RotationOps} from "../math"; +import {RawColliderSet, RawShape, RawShapeType} from "../raw"; +import {ShapeContact} from "./contact"; +import {PointProjection} from "./point"; +import {Ray, RayIntersection} from "./ray"; +import {ShapeCastHit} from "./toi"; +import {ColliderHandle} from "./collider"; + +export abstract class Shape { + public abstract intoRaw(): RawShape; + + /** + * The concrete type of this shape. + */ + public abstract get type(): ShapeType; + + /** + * instant mode without cache + */ + public static fromRaw( + rawSet: RawColliderSet, + handle: ColliderHandle, + ): Shape { + const rawType = rawSet.coShapeType(handle); + + let extents: Vector; + let borderRadius: number; + let vs: Float32Array; + let indices: Uint32Array; + let halfHeight: number; + let radius: number; + let normal: Vector; + + switch (rawType) { + case RawShapeType.Ball: + return new Ball(rawSet.coRadius(handle)); + case RawShapeType.Cuboid: + extents = rawSet.coHalfExtents(handle); + return new Cuboid(extents.x, extents.y); + + + case RawShapeType.RoundCuboid: + extents = rawSet.coHalfExtents(handle); + borderRadius = rawSet.coRoundRadius(handle); + + return new RoundCuboid(extents.x, extents.y, borderRadius); + + + case RawShapeType.Capsule: + halfHeight = rawSet.coHalfHeight(handle); + radius = rawSet.coRadius(handle); + return new Capsule(halfHeight, radius); + case RawShapeType.Segment: + vs = rawSet.coVertices(handle); + + return new Segment( + VectorOps.new(vs[0], vs[1]), + VectorOps.new(vs[2], vs[3]), + ); + + + case RawShapeType.Polyline: + vs = rawSet.coVertices(handle); + indices = rawSet.coIndices(handle); + return new Polyline(vs, indices); + case RawShapeType.Triangle: + vs = rawSet.coVertices(handle); + + return new Triangle( + VectorOps.new(vs[0], vs[1]), + VectorOps.new(vs[2], vs[3]), + VectorOps.new(vs[4], vs[5]), + ); + + + case RawShapeType.RoundTriangle: + vs = rawSet.coVertices(handle); + borderRadius = rawSet.coRoundRadius(handle); + + return new RoundTriangle( + VectorOps.new(vs[0], vs[1]), + VectorOps.new(vs[2], vs[3]), + VectorOps.new(vs[4], vs[5]), + borderRadius, + ); + + + case RawShapeType.HalfSpace: + normal = VectorOps.fromRaw(rawSet.coHalfspaceNormal(handle)); + return new HalfSpace(normal); + + case RawShapeType.Voxels: + const vox_data = rawSet.coVoxelData(handle); + const vox_size = rawSet.coVoxelSize(handle); + return new Voxels(vox_data, vox_size); + + case RawShapeType.TriMesh: + vs = rawSet.coVertices(handle); + indices = rawSet.coIndices(handle); + const tri_flags = rawSet.coTriMeshFlags(handle); + return new TriMesh(vs, indices, tri_flags); + + case RawShapeType.HeightField: + const scale = rawSet.coHeightfieldScale(handle); + const heights = rawSet.coHeightfieldHeights(handle); + + return new Heightfield(heights, scale); + + + case RawShapeType.ConvexPolygon: + vs = rawSet.coVertices(handle); + return new ConvexPolygon(vs, false); + case RawShapeType.RoundConvexPolygon: + vs = rawSet.coVertices(handle); + borderRadius = rawSet.coRoundRadius(handle); + return new RoundConvexPolygon(vs, borderRadius, false); + + + default: + throw new Error("unknown shape type: " + rawType); + } + } + + /** + * Computes the time of impact between two moving shapes. + * @param shapePos1 - The initial position of this sahpe. + * @param shapeRot1 - The rotation of this shape. + * @param shapeVel1 - The velocity of this shape. + * @param shape2 - The second moving shape. + * @param shapePos2 - The initial position of the second shape. + * @param shapeRot2 - The rotation of the second shape. + * @param shapeVel2 - The velocity of the second shape. + * @param targetDistance − If the shape moves closer to this distance from a collider, a hit + * will be returned. + * @param maxToi - The maximum time when the impact can happen. + * @param stopAtPenetration - If set to `false`, the linear shape-cast won’t immediately stop if + * the shape is penetrating another shape at its starting point **and** its trajectory is such + * that it’s on a path to exit that penetration state. + * @returns If the two moving shapes collider at some point along their trajectories, this returns the + * time at which the two shape collider as well as the contact information during the impact. Returns + * `null`if the two shapes never collide along their paths. + */ + public castShape( + shapePos1: Vector, + shapeRot1: Rotation, + shapeVel1: Vector, + shape2: Shape, + shapePos2: Vector, + shapeRot2: Rotation, + shapeVel2: Vector, + targetDistance: number, + maxToi: number, + stopAtPenetration: boolean, + ): ShapeCastHit | null { + let rawPos1 = VectorOps.intoRaw(shapePos1); + let rawRot1 = RotationOps.intoRaw(shapeRot1); + let rawVel1 = VectorOps.intoRaw(shapeVel1); + let rawPos2 = VectorOps.intoRaw(shapePos2); + let rawRot2 = RotationOps.intoRaw(shapeRot2); + let rawVel2 = VectorOps.intoRaw(shapeVel2); + + let rawShape1 = this.intoRaw(); + let rawShape2 = shape2.intoRaw(); + + let result = ShapeCastHit.fromRaw( + null, + rawShape1.castShape( + rawPos1, + rawRot1, + rawVel1, + rawShape2, + rawPos2, + rawRot2, + rawVel2, + targetDistance, + maxToi, + stopAtPenetration, + ), + ); + + rawPos1.free(); + rawRot1.free(); + rawVel1.free(); + rawPos2.free(); + rawRot2.free(); + rawVel2.free(); + + rawShape1.free(); + rawShape2.free(); + + return result; + } + + /** + * Tests if this shape intersects another shape. + * + * @param shapePos1 - The position of this shape. + * @param shapeRot1 - The rotation of this shape. + * @param shape2 - The second shape to test. + * @param shapePos2 - The position of the second shape. + * @param shapeRot2 - The rotation of the second shape. + * @returns `true` if the two shapes intersect, `false` if they don’t. + */ + public intersectsShape( + shapePos1: Vector, + shapeRot1: Rotation, + shape2: Shape, + shapePos2: Vector, + shapeRot2: Rotation, + ): boolean { + let rawPos1 = VectorOps.intoRaw(shapePos1); + let rawRot1 = RotationOps.intoRaw(shapeRot1); + let rawPos2 = VectorOps.intoRaw(shapePos2); + let rawRot2 = RotationOps.intoRaw(shapeRot2); + + let rawShape1 = this.intoRaw(); + let rawShape2 = shape2.intoRaw(); + + let result = rawShape1.intersectsShape( + rawPos1, + rawRot1, + rawShape2, + rawPos2, + rawRot2, + ); + + rawPos1.free(); + rawRot1.free(); + rawPos2.free(); + rawRot2.free(); + + rawShape1.free(); + rawShape2.free(); + + return result; + } + + /** + * Computes one pair of contact points between two shapes. + * + * @param shapePos1 - The initial position of this sahpe. + * @param shapeRot1 - The rotation of this shape. + * @param shape2 - The second shape. + * @param shapePos2 - The initial position of the second shape. + * @param shapeRot2 - The rotation of the second shape. + * @param prediction - The prediction value, if the shapes are separated by a distance greater than this value, test will fail. + * @returns `null` if the shapes are separated by a distance greater than prediction, otherwise contact details. The result is given in world-space. + */ + contactShape( + shapePos1: Vector, + shapeRot1: Rotation, + shape2: Shape, + shapePos2: Vector, + shapeRot2: Rotation, + prediction: number, + ): ShapeContact | null { + let rawPos1 = VectorOps.intoRaw(shapePos1); + let rawRot1 = RotationOps.intoRaw(shapeRot1); + let rawPos2 = VectorOps.intoRaw(shapePos2); + let rawRot2 = RotationOps.intoRaw(shapeRot2); + + let rawShape1 = this.intoRaw(); + let rawShape2 = shape2.intoRaw(); + + let result = ShapeContact.fromRaw( + rawShape1.contactShape( + rawPos1, + rawRot1, + rawShape2, + rawPos2, + rawRot2, + prediction, + ), + ); + + rawPos1.free(); + rawRot1.free(); + rawPos2.free(); + rawRot2.free(); + + rawShape1.free(); + rawShape2.free(); + + return result; + } + + containsPoint( + shapePos: Vector, + shapeRot: Rotation, + point: Vector, + ): boolean { + let rawPos = VectorOps.intoRaw(shapePos); + let rawRot = RotationOps.intoRaw(shapeRot); + let rawPoint = VectorOps.intoRaw(point); + let rawShape = this.intoRaw(); + + let result = rawShape.containsPoint(rawPos, rawRot, rawPoint); + + rawPos.free(); + rawRot.free(); + rawPoint.free(); + rawShape.free(); + + return result; + } + + projectPoint( + shapePos: Vector, + shapeRot: Rotation, + point: Vector, + solid: boolean, + ): PointProjection { + let rawPos = VectorOps.intoRaw(shapePos); + let rawRot = RotationOps.intoRaw(shapeRot); + let rawPoint = VectorOps.intoRaw(point); + let rawShape = this.intoRaw(); + + let result = PointProjection.fromRaw( + rawShape.projectPoint(rawPos, rawRot, rawPoint, solid), + ); + + rawPos.free(); + rawRot.free(); + rawPoint.free(); + rawShape.free(); + + return result; + } + + intersectsRay( + ray: Ray, + shapePos: Vector, + shapeRot: Rotation, + maxToi: number, + ): boolean { + let rawPos = VectorOps.intoRaw(shapePos); + let rawRot = RotationOps.intoRaw(shapeRot); + let rawRayOrig = VectorOps.intoRaw(ray.origin); + let rawRayDir = VectorOps.intoRaw(ray.dir); + let rawShape = this.intoRaw(); + + let result = rawShape.intersectsRay( + rawPos, + rawRot, + rawRayOrig, + rawRayDir, + maxToi, + ); + + rawPos.free(); + rawRot.free(); + rawRayOrig.free(); + rawRayDir.free(); + rawShape.free(); + + return result; + } + + castRay( + ray: Ray, + shapePos: Vector, + shapeRot: Rotation, + maxToi: number, + solid: boolean, + ): number { + let rawPos = VectorOps.intoRaw(shapePos); + let rawRot = RotationOps.intoRaw(shapeRot); + let rawRayOrig = VectorOps.intoRaw(ray.origin); + let rawRayDir = VectorOps.intoRaw(ray.dir); + let rawShape = this.intoRaw(); + + let result = rawShape.castRay( + rawPos, + rawRot, + rawRayOrig, + rawRayDir, + maxToi, + solid, + ); + + rawPos.free(); + rawRot.free(); + rawRayOrig.free(); + rawRayDir.free(); + rawShape.free(); + + return result; + } + + castRayAndGetNormal( + ray: Ray, + shapePos: Vector, + shapeRot: Rotation, + maxToi: number, + solid: boolean, + ): RayIntersection { + let rawPos = VectorOps.intoRaw(shapePos); + let rawRot = RotationOps.intoRaw(shapeRot); + let rawRayOrig = VectorOps.intoRaw(ray.origin); + let rawRayDir = VectorOps.intoRaw(ray.dir); + let rawShape = this.intoRaw(); + + let result = RayIntersection.fromRaw( + rawShape.castRayAndGetNormal( + rawPos, + rawRot, + rawRayOrig, + rawRayDir, + maxToi, + solid, + ), + ); + + rawPos.free(); + rawRot.free(); + rawRayOrig.free(); + rawRayDir.free(); + rawShape.free(); + + return result; + } +} + +/** + * An enumeration representing the type of a shape. + */ +export enum ShapeType { + Ball = 0, + Cuboid = 1, + Capsule = 2, + Segment = 3, + Polyline = 4, + Triangle = 5, + TriMesh = 6, + HeightField = 7, + // Compound = 8, + ConvexPolygon = 9, + RoundCuboid = 10, + RoundTriangle = 11, + RoundConvexPolygon = 12, + HalfSpace = 13, + Voxels = 14, +} + + + +// NOTE: this **must** match the TriMeshFlags on the rust side. +/** + * Flags controlling the behavior of the triangle mesh creation and of some + * operations involving triangle meshes. + */ +export enum TriMeshFlags { + // NOTE: these two flags are not really useful in JS. + // + // /** + // * If set, the half-edge topology of the trimesh will be computed if possible. + // */ + // HALF_EDGE_TOPOLOGY = 0b0000_0001, + // /** If set, the half-edge topology and connected components of the trimesh will be computed if possible. + // * + // * Because of the way it is currently implemented, connected components can only be computed on + // * a mesh where the half-edge topology computation succeeds. It will no longer be the case in the + // * future once we decouple the computations. + // */ + // CONNECTED_COMPONENTS = 0b0000_0010, + /** + * If set, any triangle that results in a failing half-hedge topology computation will be deleted. + */ + DELETE_BAD_TOPOLOGY_TRIANGLES = 0b0000_0100, + /** + * If set, the trimesh will be assumed to be oriented (with outward normals). + * + * The pseudo-normals of its vertices and edges will be computed. + */ + ORIENTED = 0b0000_1000, + /** + * If set, the duplicate vertices of the trimesh will be merged. + * + * Two vertices with the exact same coordinates will share the same entry on the + * vertex buffer and the index buffer is adjusted accordingly. + */ + MERGE_DUPLICATE_VERTICES = 0b0001_0000, + /** + * If set, the triangles sharing two vertices with identical index values will be removed. + * + * Because of the way it is currently implemented, this methods implies that duplicate + * vertices will be merged. It will no longer be the case in the future once we decouple + * the computations. + */ + DELETE_DEGENERATE_TRIANGLES = 0b0010_0000, + /** + * If set, two triangles sharing three vertices with identical index values (in any order) + * will be removed. + * + * Because of the way it is currently implemented, this methods implies that duplicate + * vertices will be merged. It will no longer be the case in the future once we decouple + * the computations. + */ + DELETE_DUPLICATE_TRIANGLES = 0b0100_0000, + /** + * If set, a special treatment will be applied to contact manifold calculation to eliminate + * or fix contacts normals that could lead to incorrect bumps in physics simulation + * (especially on flat surfaces). + * + * This is achieved by taking into account adjacent triangle normals when computing contact + * points for a given triangle. + * + * /!\ NOT SUPPORTED IN THE 2D VERSION OF RAPIER. + */ + FIX_INTERNAL_EDGES = 0b1000_0000 | TriMeshFlags.MERGE_DUPLICATE_VERTICES, +} + +/** + * A shape that is a sphere in 3D and a circle in 2D. + */ +export class Ball extends Shape { + readonly type = ShapeType.Ball; + + /** + * The balls radius. + */ + radius: number; + + /** + * Creates a new ball with the given radius. + * @param radius - The balls radius. + */ + constructor(radius: number) { + super(); + this.radius = radius; + } + + public intoRaw(): RawShape { + return RawShape.ball(this.radius); + } +} + +export class HalfSpace extends Shape { + readonly type = ShapeType.HalfSpace; + + /** + * The outward normal of the half-space. + */ + normal: Vector; + + /** + * Creates a new halfspace delimited by an infinite plane. + * + * @param normal - The outward normal of the plane. + */ + constructor(normal: Vector) { + super(); + this.normal = normal; + } + + public intoRaw(): RawShape { + let n = VectorOps.intoRaw(this.normal); + let result = RawShape.halfspace(n); + n.free(); + return result; + } +} + +/** + * A shape that is a box in 3D and a rectangle in 2D. + */ +export class Cuboid extends Shape { + readonly type = ShapeType.Cuboid; + + /** + * The half extent of the cuboid along each coordinate axis. + */ + halfExtents: Vector; + + /** + * Creates a new 2D rectangle. + * @param hx - The half width of the rectangle. + * @param hy - The helf height of the rectangle. + */ + constructor(hx: number, hy: number) { + super(); + this.halfExtents = VectorOps.new(hx, hy); + } + + + + public intoRaw(): RawShape { + return RawShape.cuboid(this.halfExtents.x, this.halfExtents.y); + + } +} + +/** + * A shape that is a box in 3D and a rectangle in 2D, with round corners. + */ +export class RoundCuboid extends Shape { + readonly type = ShapeType.RoundCuboid; + + /** + * The half extent of the cuboid along each coordinate axis. + */ + halfExtents: Vector; + + /** + * The radius of the cuboid's round border. + */ + borderRadius: number; + + /** + * Creates a new 2D rectangle. + * @param hx - The half width of the rectangle. + * @param hy - The helf height of the rectangle. + * @param borderRadius - The radius of the borders of this cuboid. This will + * effectively increase the half-extents of the cuboid by this radius. + */ + constructor(hx: number, hy: number, borderRadius: number) { + super(); + this.halfExtents = VectorOps.new(hx, hy); + this.borderRadius = borderRadius; + } + + + + public intoRaw(): RawShape { + return RawShape.roundCuboid( + this.halfExtents.x, + this.halfExtents.y, + this.borderRadius, + ); + + } +} + +/** + * A shape that is a capsule. + */ +export class Capsule extends Shape { + readonly type = ShapeType.Capsule; + + /** + * The radius of the capsule's basis. + */ + radius: number; + + /** + * The capsule's half height, along the `y` axis. + */ + halfHeight: number; + + /** + * Creates a new capsule with the given radius and half-height. + * @param halfHeight - The balls half-height along the `y` axis. + * @param radius - The balls radius. + */ + constructor(halfHeight: number, radius: number) { + super(); + this.halfHeight = halfHeight; + this.radius = radius; + } + + public intoRaw(): RawShape { + return RawShape.capsule(this.halfHeight, this.radius); + } +} + +/** + * A shape that is a segment. + */ +export class Segment extends Shape { + readonly type = ShapeType.Segment; + + /** + * The first point of the segment. + */ + a: Vector; + + /** + * The second point of the segment. + */ + b: Vector; + + /** + * Creates a new segment shape. + * @param a - The first point of the segment. + * @param b - The second point of the segment. + */ + constructor(a: Vector, b: Vector) { + super(); + this.a = a; + this.b = b; + } + + public intoRaw(): RawShape { + let ra = VectorOps.intoRaw(this.a); + let rb = VectorOps.intoRaw(this.b); + let result = RawShape.segment(ra, rb); + ra.free(); + rb.free(); + return result; + } +} + +/** + * A shape that is a segment. + */ +export class Triangle extends Shape { + readonly type = ShapeType.Triangle; + + /** + * The first point of the triangle. + */ + a: Vector; + + /** + * The second point of the triangle. + */ + b: Vector; + + /** + * The second point of the triangle. + */ + c: Vector; + + /** + * Creates a new triangle shape. + * + * @param a - The first point of the triangle. + * @param b - The second point of the triangle. + * @param c - The third point of the triangle. + */ + constructor(a: Vector, b: Vector, c: Vector) { + super(); + this.a = a; + this.b = b; + this.c = c; + } + + public intoRaw(): RawShape { + let ra = VectorOps.intoRaw(this.a); + let rb = VectorOps.intoRaw(this.b); + let rc = VectorOps.intoRaw(this.c); + let result = RawShape.triangle(ra, rb, rc); + ra.free(); + rb.free(); + rc.free(); + return result; + } +} + +/** + * A shape that is a triangle with round borders and a non-zero thickness. + */ +export class RoundTriangle extends Shape { + readonly type = ShapeType.RoundTriangle; + + /** + * The first point of the triangle. + */ + a: Vector; + + /** + * The second point of the triangle. + */ + b: Vector; + + /** + * The second point of the triangle. + */ + c: Vector; + + /** + * The radius of the triangles's rounded edges and vertices. + * In 3D, this is also equal to half the thickness of the round triangle. + */ + borderRadius: number; + + /** + * Creates a new triangle shape with round corners. + * + * @param a - The first point of the triangle. + * @param b - The second point of the triangle. + * @param c - The third point of the triangle. + * @param borderRadius - The radius of the borders of this triangle. In 3D, + * this is also equal to half the thickness of the triangle. + */ + constructor(a: Vector, b: Vector, c: Vector, borderRadius: number) { + super(); + this.a = a; + this.b = b; + this.c = c; + this.borderRadius = borderRadius; + } + + public intoRaw(): RawShape { + let ra = VectorOps.intoRaw(this.a); + let rb = VectorOps.intoRaw(this.b); + let rc = VectorOps.intoRaw(this.c); + let result = RawShape.roundTriangle(ra, rb, rc, this.borderRadius); + ra.free(); + rb.free(); + rc.free(); + return result; + } +} + +/** + * A shape that is a triangle mesh. + */ +export class Polyline extends Shape { + readonly type = ShapeType.Polyline; + + /** + * The vertices of the polyline. + */ + vertices: Float32Array; + + /** + * The indices of the segments. + */ + indices: Uint32Array; + + /** + * Creates a new polyline shape. + * + * @param vertices - The coordinates of the polyline's vertices. + * @param indices - The indices of the polyline's segments. If this is `null` or not provided, then + * the vertices are assumed to form a line strip. + */ + constructor(vertices: Float32Array, indices?: Uint32Array) { + super(); + this.vertices = vertices; + this.indices = indices ?? new Uint32Array(0); + } + + public intoRaw(): RawShape { + return RawShape.polyline(this.vertices, this.indices); + } +} + +/** + * A shape made of voxels. + */ +export class Voxels extends Shape { + readonly type = ShapeType.Voxels; + + /** + * The points or grid coordinates used to initialize the voxels. + */ + data: Float32Array | Int32Array; + + /** + * The dimensions of each voxel. + */ + voxelSize: Vector; + + /** + * Creates a new shape made of voxels. + * + * @param data - Defines the set of voxels. If this is a `Int32Array` then + * each voxel is defined from its (signed) grid coordinates, + * with 3 (resp 2) contiguous integers per voxel in 3D (resp 2D). + * If this is a `Float32Array`, each voxel will be such that + * they contain at least one point from this array (where each + * point is defined from 3 (resp 2) contiguous numbers per point + * in 3D (resp 2D). + * @param voxelSize - The size of each voxel. + */ + constructor(data: Float32Array | Int32Array, voxelSize: Vector) { + super(); + this.data = data; + this.voxelSize = voxelSize; + } + + public intoRaw(): RawShape { + let voxelSize = VectorOps.intoRaw(this.voxelSize); + + let result; + if (this.data instanceof Int32Array) { + result = RawShape.voxels(voxelSize, this.data); + } else { + result = RawShape.voxelsFromPoints(voxelSize, this.data); + } + + voxelSize.free(); + return result; + } +} + +/** + * A shape that is a triangle mesh. + */ +export class TriMesh extends Shape { + readonly type = ShapeType.TriMesh; + + /** + * The vertices of the triangle mesh. + */ + vertices: Float32Array; + + /** + * The indices of the triangles. + */ + indices: Uint32Array; + + /** + * The triangle mesh flags. + */ + flags: TriMeshFlags; + + /** + * Creates a new triangle mesh shape. + * + * @param vertices - The coordinates of the triangle mesh's vertices. + * @param indices - The indices of the triangle mesh's triangles. + */ + constructor( + vertices: Float32Array, + indices: Uint32Array, + flags?: TriMeshFlags, + ) { + super(); + this.vertices = vertices; + this.indices = indices; + this.flags = flags; + } + + public intoRaw(): RawShape { + return RawShape.trimesh(this.vertices, this.indices, this.flags); + } +} + +/** + * A shape that is a convex polygon. + */ +export class ConvexPolygon extends Shape { + readonly type = ShapeType.ConvexPolygon; + + /** + * The vertices of the convex polygon. + */ + vertices: Float32Array; + + /** + * Do we want to assume the vertices already form a convex hull? + */ + skipConvexHullComputation: boolean; + + /** + * Creates a new convex polygon shape. + * + * @param vertices - The coordinates of the convex polygon's vertices. + * @param skipConvexHullComputation - If set to `true`, the input points will + * be assumed to form a convex polyline and no convex-hull computation will + * be done automatically. + */ + constructor(vertices: Float32Array, skipConvexHullComputation: boolean) { + super(); + this.vertices = vertices; + this.skipConvexHullComputation = !!skipConvexHullComputation; + } + + public intoRaw(): RawShape { + if (this.skipConvexHullComputation) { + return RawShape.convexPolyline(this.vertices); + } else { + return RawShape.convexHull(this.vertices); + } + } +} + +/** + * A shape that is a convex polygon. + */ +export class RoundConvexPolygon extends Shape { + readonly type = ShapeType.RoundConvexPolygon; + + /** + * The vertices of the convex polygon. + */ + vertices: Float32Array; + + /** + * Do we want to assume the vertices already form a convex hull? + */ + skipConvexHullComputation: boolean; + + /** + * The radius of the convex polygon's rounded edges and vertices. + */ + borderRadius: number; + + /** + * Creates a new convex polygon shape. + * + * @param vertices - The coordinates of the convex polygon's vertices. + * @param borderRadius - The radius of the borders of this convex polygon. + * @param skipConvexHullComputation - If set to `true`, the input points will + * be assumed to form a convex polyline and no convex-hull computation will + * be done automatically. + */ + constructor( + vertices: Float32Array, + borderRadius: number, + skipConvexHullComputation: boolean, + ) { + super(); + this.vertices = vertices; + this.borderRadius = borderRadius; + this.skipConvexHullComputation = !!skipConvexHullComputation; + } + + public intoRaw(): RawShape { + if (this.skipConvexHullComputation) { + return RawShape.roundConvexPolyline( + this.vertices, + this.borderRadius, + ); + } else { + return RawShape.roundConvexHull(this.vertices, this.borderRadius); + } + } +} + +/** + * A shape that is a heightfield. + */ +export class Heightfield extends Shape { + readonly type = ShapeType.HeightField; + + /** + * The heights of the heightfield, along its local `y` axis. + */ + heights: Float32Array; + + /** + * The heightfield's length along its local `x` axis. + */ + scale: Vector; + + /** + * Creates a new heightfield shape. + * + * @param heights - The heights of the heightfield, along its local `y` axis. + * @param scale - The scale factor applied to the heightfield. + */ + constructor(heights: Float32Array, scale: Vector) { + super(); + this.heights = heights; + this.scale = scale; + } + + public intoRaw(): RawShape { + let rawScale = VectorOps.intoRaw(this.scale); + let rawShape = RawShape.heightfield(this.heights, rawScale); + rawScale.free(); + return rawShape; + } +} + + diff --git a/packages/rapier2d/src/geometry/toi.ts b/packages/rapier2d/src/geometry/toi.ts new file mode 100644 index 00000000..ade13f17 --- /dev/null +++ b/packages/rapier2d/src/geometry/toi.ts @@ -0,0 +1,105 @@ +import {Collider} from "./collider"; +import {Vector, VectorOps} from "../math"; +import {RawShapeCastHit, RawColliderShapeCastHit} from "../raw"; +import {ColliderSet} from "./collider_set"; + +/** + * The intersection between a ray and a collider. + */ +export class ShapeCastHit { + /** + * The time of impact of the two shapes. + */ + time_of_impact: number; + /** + * The local-space contact point on the first shape, at + * the time of impact. + */ + witness1: Vector; + /** + * The local-space contact point on the second shape, at + * the time of impact. + */ + witness2: Vector; + /** + * The local-space normal on the first shape, at + * the time of impact. + */ + normal1: Vector; + /** + * The local-space normal on the second shape, at + * the time of impact. + */ + normal2: Vector; + + constructor( + time_of_impact: number, + witness1: Vector, + witness2: Vector, + normal1: Vector, + normal2: Vector, + ) { + this.time_of_impact = time_of_impact; + this.witness1 = witness1; + this.witness2 = witness2; + this.normal1 = normal1; + this.normal2 = normal2; + } + + public static fromRaw( + colliderSet: ColliderSet, + raw: RawShapeCastHit, + ): ShapeCastHit { + if (!raw) return null; + + const result = new ShapeCastHit( + raw.time_of_impact(), + VectorOps.fromRaw(raw.witness1()), + VectorOps.fromRaw(raw.witness2()), + VectorOps.fromRaw(raw.normal1()), + VectorOps.fromRaw(raw.normal2()), + ); + raw.free(); + return result; + } +} + +/** + * The intersection between a ray and a collider. + */ +export class ColliderShapeCastHit extends ShapeCastHit { + /** + * The handle of the collider hit by the ray. + */ + collider: Collider; + + constructor( + collider: Collider, + time_of_impact: number, + witness1: Vector, + witness2: Vector, + normal1: Vector, + normal2: Vector, + ) { + super(time_of_impact, witness1, witness2, normal1, normal2); + this.collider = collider; + } + + public static fromRaw( + colliderSet: ColliderSet, + raw: RawColliderShapeCastHit, + ): ColliderShapeCastHit { + if (!raw) return null; + + const result = new ColliderShapeCastHit( + colliderSet.get(raw.colliderHandle()), + raw.time_of_impact(), + VectorOps.fromRaw(raw.witness1()), + VectorOps.fromRaw(raw.witness2()), + VectorOps.fromRaw(raw.normal1()), + VectorOps.fromRaw(raw.normal2()), + ); + raw.free(); + return result; + } +} diff --git a/packages/rapier2d/src/index.ts b/packages/rapier2d/src/index.ts new file mode 100644 index 00000000..dbf4d053 --- /dev/null +++ b/packages/rapier2d/src/index.ts @@ -0,0 +1,3 @@ +import * as RAPIER from "./exports"; +export * from "./exports"; +export default RAPIER; diff --git a/packages/rapier2d/src/init.ts b/packages/rapier2d/src/init.ts new file mode 100644 index 00000000..c1f2b3f2 --- /dev/null +++ b/packages/rapier2d/src/init.ts @@ -0,0 +1,60 @@ +/** + * RAPIER initialization module with dynamic WASM loading support. + * RAPIER 初始化模块,支持动态 WASM 加载。 + */ + +import wasmInit from "../pkg/rapier_wasm2d"; + +/** + * Input types for WASM initialization. + * WASM 初始化的输入类型。 + */ +export type InitInput = + | RequestInfo // URL string or Request object + | URL // URL object + | Response // Fetch Response object + | BufferSource // ArrayBuffer or TypedArray + | WebAssembly.Module; // Pre-compiled module + +let initialized = false; + +/** + * Initializes RAPIER. + * Has to be called and awaited before using any library methods. + * + * 初始化 RAPIER。 + * 必须在使用任何库方法之前调用并等待。 + * + * @param input - WASM source (required). Can be URL, Response, ArrayBuffer, etc. + * WASM 源(必需)。可以是 URL、Response、ArrayBuffer 等。 + * + * @example + * // Load from URL | 从 URL 加载 + * await RAPIER.init('wasm/rapier_wasm2d_bg.wasm'); + * + * @example + * // Load from fetch response | 从 fetch 响应加载 + * const response = await fetch('wasm/rapier_wasm2d_bg.wasm'); + * await RAPIER.init(response); + * + * @example + * // Load from ArrayBuffer | 从 ArrayBuffer 加载 + * const buffer = await fetch('wasm/rapier_wasm2d_bg.wasm').then(r => r.arrayBuffer()); + * await RAPIER.init(buffer); + */ +export async function init(input?: InitInput): Promise { + if (initialized) { + return; + } + + await wasmInit(input); + initialized = true; +} + +/** + * Check if RAPIER is already initialized. + * 检查 RAPIER 是否已初始化。 + */ +export function isInitialized(): boolean { + return initialized; +} diff --git a/packages/rapier2d/src/math.ts b/packages/rapier2d/src/math.ts new file mode 100644 index 00000000..04e543cd --- /dev/null +++ b/packages/rapier2d/src/math.ts @@ -0,0 +1,72 @@ +import {RawVector, RawRotation} from "./raw"; + +export interface Vector { + x: number; + y: number; +} + +/** + * A 2D vector. + */ +export class Vector2 implements Vector { + x: number; + y: number; + + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } +} + +export class VectorOps { + public static new(x: number, y: number): Vector { + return new Vector2(x, y); + } + + public static zeros(): Vector { + return VectorOps.new(0.0, 0.0); + } + + // FIXME: type ram: RawVector? + public static fromRaw(raw: RawVector): Vector | null { + if (!raw) return null; + + let res = VectorOps.new(raw.x, raw.y); + raw.free(); + return res; + } + + public static intoRaw(v: Vector): RawVector { + return new RawVector(v.x, v.y); + } + + public static copy(out: Vector, input: Vector) { + out.x = input.x; + out.y = input.y; + } +} + +/** + * A rotation angle in radians. + */ +export type Rotation = number; + +export class RotationOps { + public static identity(): number { + return 0.0; + } + + public static fromRaw(raw: RawRotation): Rotation | null { + if (!raw) return null; + + let res = raw.angle; + raw.free(); + return res; + } + + public static intoRaw(angle: Rotation): RawRotation { + return RawRotation.fromAngle(angle); + } +} + + diff --git a/packages/rapier2d/src/pipeline/debug_render_pipeline.ts b/packages/rapier2d/src/pipeline/debug_render_pipeline.ts new file mode 100644 index 00000000..bf0d7057 --- /dev/null +++ b/packages/rapier2d/src/pipeline/debug_render_pipeline.ts @@ -0,0 +1,85 @@ +import {RawDebugRenderPipeline} from "../raw"; +import {Vector, VectorOps} from "../math"; +import { + IntegrationParameters, + IslandManager, + ImpulseJointSet, + MultibodyJointSet, + RigidBodySet, +} from "../dynamics"; +import {BroadPhase, Collider, ColliderSet, NarrowPhase} from "../geometry"; +import {QueryFilterFlags} from "./query_pipeline"; + +/** + * The vertex and color buffers for debug-redering the physics scene. + */ +export class DebugRenderBuffers { + /** + * The lines to render. This is a flat array containing all the lines + * to render. Each line is described as two consecutive point. Each + * point is described as two (in 2D) or three (in 3D) consecutive + * floats. For example, in 2D, the array: `[1, 2, 3, 4, 5, 6, 7, 8]` + * describes the two segments `[[1, 2], [3, 4]]` and `[[5, 6], [7, 8]]`. + */ + public vertices: Float32Array; + /** + * The color buffer. There is one color per vertex, and each color + * has four consecutive components (in RGBA format). + */ + public colors: Float32Array; + + constructor(vertices: Float32Array, colors: Float32Array) { + this.vertices = vertices; + this.colors = colors; + } +} + +/** + * A pipeline for rendering the physics scene. + * + * To avoid leaking WASM resources, this MUST be freed manually with `debugRenderPipeline.free()` + * once you are done using it (and all the rigid-bodies it created). + */ +export class DebugRenderPipeline { + raw: RawDebugRenderPipeline; + public vertices: Float32Array; + public colors: Float32Array; + + /** + * Release the WASM memory occupied by this serialization pipeline. + */ + free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + this.vertices = undefined; + this.colors = undefined; + } + + constructor(raw?: RawDebugRenderPipeline) { + this.raw = raw || new RawDebugRenderPipeline(); + } + + public render( + bodies: RigidBodySet, + colliders: ColliderSet, + impulse_joints: ImpulseJointSet, + multibody_joints: MultibodyJointSet, + narrow_phase: NarrowPhase, + filterFlags?: QueryFilterFlags, + filterPredicate?: (collider: Collider) => boolean, + ) { + this.raw.render( + bodies.raw, + colliders.raw, + impulse_joints.raw, + multibody_joints.raw, + narrow_phase.raw, + filterFlags, + colliders.castClosure(filterPredicate), + ); + this.vertices = this.raw.vertices(); + this.colors = this.raw.colors(); + } +} diff --git a/packages/rapier2d/src/pipeline/event_queue.ts b/packages/rapier2d/src/pipeline/event_queue.ts new file mode 100644 index 00000000..e39c1582 --- /dev/null +++ b/packages/rapier2d/src/pipeline/event_queue.ts @@ -0,0 +1,158 @@ +import {RawContactForceEvent, RawEventQueue} from "../raw"; +import {RigidBodyHandle} from "../dynamics"; +import {Collider, ColliderHandle} from "../geometry"; +import {Vector, VectorOps} from "../math"; + +/** + * Flags indicating what events are enabled for colliders. + */ +export enum ActiveEvents { + NONE = 0, + /** + * Enable collision events. + */ + COLLISION_EVENTS = 0b0001, + /** + * Enable contact force events. + */ + CONTACT_FORCE_EVENTS = 0b0010, +} + +/** + * Event occurring when the sum of the magnitudes of the + * contact forces between two colliders exceed a threshold. + * + * This object should **not** be stored anywhere. Its properties can only be + * read from within the closure given to `EventHandler.drainContactForceEvents`. + */ +export class TempContactForceEvent { + raw: RawContactForceEvent; + + public free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + } + + /** + * The first collider involved in the contact. + */ + public collider1(): ColliderHandle { + return this.raw.collider1(); + } + + /** + * The second collider involved in the contact. + */ + public collider2(): ColliderHandle { + return this.raw.collider2(); + } + + /** + * The sum of all the forces between the two colliders. + */ + public totalForce(): Vector { + return VectorOps.fromRaw(this.raw.total_force()); + } + + /** + * The sum of the magnitudes of each force between the two colliders. + * + * Note that this is **not** the same as the magnitude of `self.total_force`. + * Here we are summing the magnitude of all the forces, instead of taking + * the magnitude of their sum. + */ + public totalForceMagnitude(): number { + return this.raw.total_force_magnitude(); + } + + /** + * The world-space (unit) direction of the force with strongest magnitude. + */ + public maxForceDirection(): Vector { + return VectorOps.fromRaw(this.raw.max_force_direction()); + } + + /** + * The magnitude of the largest force at a contact point of this contact pair. + */ + public maxForceMagnitude(): number { + return this.raw.max_force_magnitude(); + } +} + +/** + * A structure responsible for collecting events generated + * by the physics engine. + * + * To avoid leaking WASM resources, this MUST be freed manually with `eventQueue.free()` + * once you are done using it. + */ +export class EventQueue { + raw: RawEventQueue; + + /** + * Creates a new event collector. + * + * @param autoDrain -setting this to `true` is strongly recommended. If true, the collector will + * be automatically drained before each `world.step(collector)`. If false, the collector will + * keep all events in memory unless it is manually drained/cleared; this may lead to unbounded use of + * RAM if no drain is performed. + */ + constructor(autoDrain: boolean, raw?: RawEventQueue) { + this.raw = raw || new RawEventQueue(autoDrain); + } + + /** + * Release the WASM memory occupied by this event-queue. + */ + public free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + } + + /** + * Applies the given javascript closure on each collision event of this collector, then clear + * the internal collision event buffer. + * + * @param f - JavaScript closure applied to each collision event. The + * closure must take three arguments: two integers representing the handles of the colliders + * involved in the collision, and a boolean indicating if the collision started (true) or stopped + * (false). + */ + public drainCollisionEvents( + f: ( + handle1: ColliderHandle, + handle2: ColliderHandle, + started: boolean, + ) => void, + ) { + this.raw.drainCollisionEvents(f); + } + + /** + * Applies the given javascript closure on each contact force event of this collector, then clear + * the internal collision event buffer. + * + * @param f - JavaScript closure applied to each collision event. The + * closure must take one `TempContactForceEvent` argument. + */ + public drainContactForceEvents(f: (event: TempContactForceEvent) => void) { + let event = new TempContactForceEvent(); + this.raw.drainContactForceEvents((raw: RawContactForceEvent) => { + event.raw = raw; + f(event); + event.free(); + }); + } + + /** + * Removes all events contained by this collector + */ + public clear() { + this.raw.clear(); + } +} diff --git a/packages/rapier2d/src/pipeline/index.ts b/packages/rapier2d/src/pipeline/index.ts new file mode 100644 index 00000000..092c1a59 --- /dev/null +++ b/packages/rapier2d/src/pipeline/index.ts @@ -0,0 +1,7 @@ +export * from "./world"; +export * from "./physics_pipeline"; +export * from "./serialization_pipeline"; +export * from "./event_queue"; +export * from "./physics_hooks"; +export * from "./debug_render_pipeline"; +export * from "./query_pipeline"; diff --git a/packages/rapier2d/src/pipeline/physics_hooks.ts b/packages/rapier2d/src/pipeline/physics_hooks.ts new file mode 100644 index 00000000..20e12ad9 --- /dev/null +++ b/packages/rapier2d/src/pipeline/physics_hooks.ts @@ -0,0 +1,54 @@ +import {RigidBodyHandle} from "../dynamics"; +import {ColliderHandle} from "../geometry"; + +export enum ActiveHooks { + NONE = 0, + FILTER_CONTACT_PAIRS = 0b0001, + FILTER_INTERSECTION_PAIRS = 0b0010, + // MODIFY_SOLVER_CONTACTS = 0b0100, /* Not supported yet in JS. */ +} + +export enum SolverFlags { + EMPTY = 0b000, + COMPUTE_IMPULSE = 0b001, +} + +export interface PhysicsHooks { + /** + * Function that determines if contacts computation should happen between two colliders, and how the + * constraints solver should behave for these contacts. + * + * This will only be executed and taken into account if at least one of the involved colliders contains the + * `ActiveHooks.FILTER_CONTACT_PAIR` flag in its active hooks. + * + * @param collider1 − Handle of the first collider involved in the potential contact. + * @param collider2 − Handle of the second collider involved in the potential contact. + * @param body1 − Handle of the first body involved in the potential contact. + * @param body2 − Handle of the second body involved in the potential contact. + */ + filterContactPair( + collider1: ColliderHandle, + collider2: ColliderHandle, + body1: RigidBodyHandle, + body2: RigidBodyHandle, + ): SolverFlags | null; + + /** + * Function that determines if intersection computation should happen between two colliders (where at least + * one is a sensor). + * + * This will only be executed and taken into account if `one of the involved colliders contains the + * `ActiveHooks.FILTER_INTERSECTION_PAIR` flag in its active hooks. + * + * @param collider1 − Handle of the first collider involved in the potential contact. + * @param collider2 − Handle of the second collider involved in the potential contact. + * @param body1 − Handle of the first body involved in the potential contact. + * @param body2 − Handle of the second body involved in the potential contact. + */ + filterIntersectionPair( + collider1: ColliderHandle, + collider2: ColliderHandle, + body1: RigidBodyHandle, + body2: RigidBodyHandle, + ): boolean; +} diff --git a/packages/rapier2d/src/pipeline/physics_pipeline.ts b/packages/rapier2d/src/pipeline/physics_pipeline.ts new file mode 100644 index 00000000..a3319205 --- /dev/null +++ b/packages/rapier2d/src/pipeline/physics_pipeline.ts @@ -0,0 +1,85 @@ +import {RawPhysicsPipeline} from "../raw"; +import {Vector, VectorOps} from "../math"; +import { + IntegrationParameters, + ImpulseJointSet, + MultibodyJointSet, + RigidBodyHandle, + RigidBodySet, + CCDSolver, + IslandManager, +} from "../dynamics"; +import { + BroadPhase, + ColliderHandle, + ColliderSet, + NarrowPhase, +} from "../geometry"; +import {EventQueue} from "./event_queue"; +import {PhysicsHooks} from "./physics_hooks"; + +export class PhysicsPipeline { + raw: RawPhysicsPipeline; + + public free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + } + + constructor(raw?: RawPhysicsPipeline) { + this.raw = raw || new RawPhysicsPipeline(); + } + + public step( + gravity: Vector, + integrationParameters: IntegrationParameters, + islands: IslandManager, + broadPhase: BroadPhase, + narrowPhase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + impulseJoints: ImpulseJointSet, + multibodyJoints: MultibodyJointSet, + ccdSolver: CCDSolver, + eventQueue?: EventQueue, + hooks?: PhysicsHooks, + ) { + let rawG = VectorOps.intoRaw(gravity); + + if (!!eventQueue) { + this.raw.stepWithEvents( + rawG, + integrationParameters.raw, + islands.raw, + broadPhase.raw, + narrowPhase.raw, + bodies.raw, + colliders.raw, + impulseJoints.raw, + multibodyJoints.raw, + ccdSolver.raw, + eventQueue.raw, + hooks, + !!hooks ? hooks.filterContactPair : null, + !!hooks ? hooks.filterIntersectionPair : null, + ); + } else { + this.raw.step( + rawG, + integrationParameters.raw, + islands.raw, + broadPhase.raw, + narrowPhase.raw, + bodies.raw, + colliders.raw, + impulseJoints.raw, + multibodyJoints.raw, + ccdSolver.raw, + ); + } + + rawG.free(); + } +} diff --git a/packages/rapier2d/src/pipeline/query_pipeline.ts b/packages/rapier2d/src/pipeline/query_pipeline.ts new file mode 100644 index 00000000..5610ff31 --- /dev/null +++ b/packages/rapier2d/src/pipeline/query_pipeline.ts @@ -0,0 +1,57 @@ +import {RawRayColliderIntersection} from "../raw"; +import { + ColliderHandle, + ColliderSet, + InteractionGroups, + PointColliderProjection, + Ray, + RayColliderIntersection, + RayColliderHit, + Shape, + ColliderShapeCastHit, +} from "../geometry"; +import {IslandManager, RigidBodyHandle, RigidBodySet} from "../dynamics"; +import {Rotation, RotationOps, Vector, VectorOps} from "../math"; + +// NOTE: must match the bits in the QueryFilterFlags on the Rust side. +/** + * Flags for excluding whole sets of colliders from a scene query. + */ +export enum QueryFilterFlags { + /** + * Exclude from the query any collider attached to a fixed rigid-body and colliders with no rigid-body attached. + */ + EXCLUDE_FIXED = 0b0000_0001, + /** + * Exclude from the query any collider attached to a dynamic rigid-body. + */ + EXCLUDE_KINEMATIC = 0b0000_0010, + /** + * Exclude from the query any collider attached to a kinematic rigid-body. + */ + EXCLUDE_DYNAMIC = 0b0000_0100, + /** + * Exclude from the query any collider that is a sensor. + */ + EXCLUDE_SENSORS = 0b0000_1000, + /** + * Exclude from the query any collider that is not a sensor. + */ + EXCLUDE_SOLIDS = 0b0001_0000, + /** + * Excludes all colliders not attached to a dynamic rigid-body. + */ + ONLY_DYNAMIC = QueryFilterFlags.EXCLUDE_FIXED | + QueryFilterFlags.EXCLUDE_KINEMATIC, + /** + * Excludes all colliders not attached to a kinematic rigid-body. + */ + ONLY_KINEMATIC = QueryFilterFlags.EXCLUDE_DYNAMIC | + QueryFilterFlags.EXCLUDE_FIXED, + /** + * Exclude all colliders attached to a non-fixed rigid-body + * (this will not exclude colliders not attached to any rigid-body). + */ + ONLY_FIXED = QueryFilterFlags.EXCLUDE_DYNAMIC | + QueryFilterFlags.EXCLUDE_KINEMATIC, +} diff --git a/packages/rapier2d/src/pipeline/serialization_pipeline.ts b/packages/rapier2d/src/pipeline/serialization_pipeline.ts new file mode 100644 index 00000000..87fde398 --- /dev/null +++ b/packages/rapier2d/src/pipeline/serialization_pipeline.ts @@ -0,0 +1,84 @@ +import {RawSerializationPipeline} from "../raw"; +import {Vector, VectorOps} from "../math"; +import { + IntegrationParameters, + IslandManager, + ImpulseJointSet, + MultibodyJointSet, + RigidBodySet, +} from "../dynamics"; +import {BroadPhase, ColliderSet, NarrowPhase} from "../geometry"; +import {World} from "./world"; + +/** + * A pipeline for serializing the physics scene. + * + * To avoid leaking WASM resources, this MUST be freed manually with `serializationPipeline.free()` + * once you are done using it (and all the rigid-bodies it created). + */ +export class SerializationPipeline { + raw: RawSerializationPipeline; + + /** + * Release the WASM memory occupied by this serialization pipeline. + */ + free() { + if (!!this.raw) { + this.raw.free(); + } + this.raw = undefined; + } + + constructor(raw?: RawSerializationPipeline) { + this.raw = raw || new RawSerializationPipeline(); + } + + /** + * Serialize a complete physics state into a single byte array. + * @param gravity - The current gravity affecting the simulation. + * @param integrationParameters - The integration parameters of the simulation. + * @param broadPhase - The broad-phase of the simulation. + * @param narrowPhase - The narrow-phase of the simulation. + * @param bodies - The rigid-bodies taking part into the simulation. + * @param colliders - The colliders taking part into the simulation. + * @param impulseJoints - The impulse joints taking part into the simulation. + * @param multibodyJoints - The multibody joints taking part into the simulation. + */ + public serializeAll( + gravity: Vector, + integrationParameters: IntegrationParameters, + islands: IslandManager, + broadPhase: BroadPhase, + narrowPhase: NarrowPhase, + bodies: RigidBodySet, + colliders: ColliderSet, + impulseJoints: ImpulseJointSet, + multibodyJoints: MultibodyJointSet, + ): Uint8Array { + let rawGra = VectorOps.intoRaw(gravity); + + const res = this.raw.serializeAll( + rawGra, + integrationParameters.raw, + islands.raw, + broadPhase.raw, + narrowPhase.raw, + bodies.raw, + colliders.raw, + impulseJoints.raw, + multibodyJoints.raw, + ); + rawGra.free(); + + return res; + } + + /** + * Deserialize the complete physics state from a single byte array. + * + * @param data - The byte array to deserialize. + */ + public deserializeAll(data: Uint8Array): World { + return World.fromRaw(this.raw.deserializeAll(data)); + } +} diff --git a/packages/rapier2d/src/pipeline/world.ts b/packages/rapier2d/src/pipeline/world.ts new file mode 100644 index 00000000..40c96dbf --- /dev/null +++ b/packages/rapier2d/src/pipeline/world.ts @@ -0,0 +1,1260 @@ +import { + RawBroadPhase, + RawCCDSolver, + RawColliderSet, + RawDeserializedWorld, + RawIntegrationParameters, + RawIslandManager, + RawImpulseJointSet, + RawMultibodyJointSet, + RawNarrowPhase, + RawPhysicsPipeline, + RawRigidBodySet, + RawSerializationPipeline, + RawDebugRenderPipeline, +} from "../raw"; + +import { + BroadPhase, + Collider, + ColliderDesc, + ColliderHandle, + ColliderSet, + InteractionGroups, + NarrowPhase, + PointColliderProjection, + Ray, + RayColliderIntersection, + RayColliderHit, + Shape, + ColliderShapeCastHit, + TempContactManifold, +} from "../geometry"; +import { + CCDSolver, + IntegrationParameters, + IslandManager, + ImpulseJoint, + ImpulseJointHandle, + MultibodyJoint, + MultibodyJointHandle, + JointData, + ImpulseJointSet, + MultibodyJointSet, + RigidBody, + RigidBodyDesc, + RigidBodyHandle, + RigidBodySet, +} from "../dynamics"; +import {Rotation, Vector, VectorOps} from "../math"; +import {PhysicsPipeline} from "./physics_pipeline"; +import {QueryFilterFlags} from "./query_pipeline"; +import {SerializationPipeline} from "./serialization_pipeline"; +import {EventQueue} from "./event_queue"; +import {PhysicsHooks} from "./physics_hooks"; +import {DebugRenderBuffers, DebugRenderPipeline} from "./debug_render_pipeline"; +import { + KinematicCharacterController, + PidAxesMask, + PidController, +} from "../control"; +import {Coarena} from "../coarena"; + + +/** + * The physics world. + * + * This contains all the data-structures necessary for creating and simulating + * bodies with contacts, joints, and external forces. + */ +export class World { + public gravity: Vector; + integrationParameters: IntegrationParameters; + islands: IslandManager; + broadPhase: BroadPhase; + narrowPhase: NarrowPhase; + bodies: RigidBodySet; + colliders: ColliderSet; + impulseJoints: ImpulseJointSet; + multibodyJoints: MultibodyJointSet; + ccdSolver: CCDSolver; + physicsPipeline: PhysicsPipeline; + serializationPipeline: SerializationPipeline; + debugRenderPipeline: DebugRenderPipeline; + characterControllers: Set; + pidControllers: Set; + + + /** + * Release the WASM memory occupied by this physics world. + * + * All the fields of this physics world will be freed as well, + * so there is no need to call their `.free()` methods individually. + */ + public free() { + this.integrationParameters.free(); + this.islands.free(); + this.broadPhase.free(); + this.narrowPhase.free(); + this.bodies.free(); + this.colliders.free(); + this.impulseJoints.free(); + this.multibodyJoints.free(); + this.ccdSolver.free(); + this.physicsPipeline.free(); + this.serializationPipeline.free(); + this.debugRenderPipeline.free(); + this.characterControllers.forEach((controller) => controller.free()); + this.pidControllers.forEach((controller) => controller.free()); + + + this.integrationParameters = undefined; + this.islands = undefined; + this.broadPhase = undefined; + this.narrowPhase = undefined; + this.bodies = undefined; + this.colliders = undefined; + this.ccdSolver = undefined; + this.impulseJoints = undefined; + this.multibodyJoints = undefined; + this.physicsPipeline = undefined; + this.serializationPipeline = undefined; + this.debugRenderPipeline = undefined; + this.characterControllers = undefined; + this.pidControllers = undefined; + + } + + constructor( + gravity: Vector, + rawIntegrationParameters?: RawIntegrationParameters, + rawIslands?: RawIslandManager, + rawBroadPhase?: RawBroadPhase, + rawNarrowPhase?: RawNarrowPhase, + rawBodies?: RawRigidBodySet, + rawColliders?: RawColliderSet, + rawImpulseJoints?: RawImpulseJointSet, + rawMultibodyJoints?: RawMultibodyJointSet, + rawCCDSolver?: RawCCDSolver, + rawPhysicsPipeline?: RawPhysicsPipeline, + rawSerializationPipeline?: RawSerializationPipeline, + rawDebugRenderPipeline?: RawDebugRenderPipeline, + ) { + this.gravity = gravity; + this.integrationParameters = new IntegrationParameters( + rawIntegrationParameters, + ); + this.islands = new IslandManager(rawIslands); + this.broadPhase = new BroadPhase(rawBroadPhase); + this.narrowPhase = new NarrowPhase(rawNarrowPhase); + this.bodies = new RigidBodySet(rawBodies); + this.colliders = new ColliderSet(rawColliders); + this.impulseJoints = new ImpulseJointSet(rawImpulseJoints); + this.multibodyJoints = new MultibodyJointSet(rawMultibodyJoints); + this.ccdSolver = new CCDSolver(rawCCDSolver); + this.physicsPipeline = new PhysicsPipeline(rawPhysicsPipeline); + this.serializationPipeline = new SerializationPipeline( + rawSerializationPipeline, + ); + this.debugRenderPipeline = new DebugRenderPipeline( + rawDebugRenderPipeline, + ); + this.characterControllers = new Set(); + this.pidControllers = new Set(); + + + this.impulseJoints.finalizeDeserialization(this.bodies); + this.bodies.finalizeDeserialization(this.colliders); + this.colliders.finalizeDeserialization(this.bodies); + } + + public static fromRaw(raw: RawDeserializedWorld): World { + if (!raw) return null; + + return new World( + VectorOps.fromRaw(raw.takeGravity()), + raw.takeIntegrationParameters(), + raw.takeIslandManager(), + raw.takeBroadPhase(), + raw.takeNarrowPhase(), + raw.takeBodies(), + raw.takeColliders(), + raw.takeImpulseJoints(), + raw.takeMultibodyJoints(), + ); + } + + /** + * Takes a snapshot of this world. + * + * Use `World.restoreSnapshot` to create a new physics world with a state identical to + * the state when `.takeSnapshot()` is called. + */ + public takeSnapshot(): Uint8Array { + return this.serializationPipeline.serializeAll( + this.gravity, + this.integrationParameters, + this.islands, + this.broadPhase, + this.narrowPhase, + this.bodies, + this.colliders, + this.impulseJoints, + this.multibodyJoints, + ); + } + + /** + * Creates a new physics world from a snapshot. + * + * This new physics world will be an identical copy of the snapshoted physics world. + */ + public static restoreSnapshot(data: Uint8Array): World { + let deser = new SerializationPipeline(); + return deser.deserializeAll(data); + } + + /** + * Computes all the lines (and their colors) needed to render the scene. + * + * @param filterFlags - Flags for excluding whole subsets of colliders from rendering. + * @param filterPredicate - Any collider for which this closure returns `false` will be excluded from the + * debug rendering. + */ + public debugRender( + filterFlags?: QueryFilterFlags, + filterPredicate?: (collider: Collider) => boolean, + ): DebugRenderBuffers { + this.debugRenderPipeline.render( + this.bodies, + this.colliders, + this.impulseJoints, + this.multibodyJoints, + this.narrowPhase, + filterFlags, + filterPredicate, + ); + return new DebugRenderBuffers( + this.debugRenderPipeline.vertices, + this.debugRenderPipeline.colors, + ); + } + + /** + * Advance the simulation by one time step. + * + * All events generated by the physics engine are ignored. + * + * @param EventQueue - (optional) structure responsible for collecting + * events generated by the physics engine. + */ + public step(eventQueue?: EventQueue, hooks?: PhysicsHooks) { + this.physicsPipeline.step( + this.gravity, + this.integrationParameters, + this.islands, + this.broadPhase, + this.narrowPhase, + this.bodies, + this.colliders, + this.impulseJoints, + this.multibodyJoints, + this.ccdSolver, + eventQueue, + hooks, + ); + } + + /** + * Update colliders positions after rigid-bodies moved. + * + * When a rigid-body moves, the positions of the colliders attached to it need to be updated. This update is + * generally automatically done at the beginning and the end of each simulation step with World.step. + * If the positions need to be updated without running a simulation step this method can be called manually. + */ + public propagateModifiedBodyPositionsToColliders() { + this.bodies.raw.propagateModifiedBodyPositionsToColliders( + this.colliders.raw, + ); + } + + // TODO: This needs to trigger a broad-phase update but without emitting collision events? + // /** + // * Ensure subsequent scene queries take into account the collider positions set before this method is called. + // * + // * This does not step the physics simulation forward. + // */ + // public updateSceneQueries() { + // this.propagateModifiedBodyPositionsToColliders(); + // this.queryPipeline.update(this.colliders); + // } + + /** + * The current simulation timestep. + */ + get timestep(): number { + return this.integrationParameters.dt; + } + + /** + * Sets the new simulation timestep. + * + * The simulation timestep governs by how much the physics state of the world will + * be integrated. A simulation timestep should: + * - be as small as possible. Typical values evolve around 0.016 (assuming the chosen unit is milliseconds, + * corresponds to the time between two frames of a game running at 60FPS). + * - not vary too much during the course of the simulation. A timestep with large variations may + * cause instabilities in the simulation. + * + * @param dt - The timestep length, in seconds. + */ + set timestep(dt: number) { + this.integrationParameters.dt = dt; + } + + /** + * The approximate size of most dynamic objects in the scene. + * + * See the documentation of the `World.lengthUnit` setter for further details. + */ + get lengthUnit(): number { + return this.integrationParameters.lengthUnit; + } + + /** + * The approximate size of most dynamic objects in the scene. + * + * This value is used internally to estimate some length-based tolerance. In particular, the + * values `IntegrationParameters.allowedLinearError`, + * `IntegrationParameters.maxPenetrationCorrection`, + * `IntegrationParameters.predictionDistance`, `RigidBodyActivation.linearThreshold` + * are scaled by this value implicitly. + * + * This value can be understood as the number of units-per-meter in your physical world compared + * to a human-sized world in meter. For example, in a 2d game, if your typical object size is 100 + * pixels, set the `[`Self::length_unit`]` parameter to 100.0. The physics engine will interpret + * it as if 100 pixels is equivalent to 1 meter in its various internal threshold. + * (default `1.0`). + */ + set lengthUnit(unitsPerMeter: number) { + this.integrationParameters.lengthUnit = unitsPerMeter; + } + + /** + * The number of solver iterations run by the constraints solver for calculating forces (default: `4`). + */ + get numSolverIterations(): number { + return this.integrationParameters.numSolverIterations; + } + + /** + * Sets the number of solver iterations run by the constraints solver for calculating forces (default: `4`). + * + * The greater this value is, the most rigid and realistic the physics simulation will be. + * However a greater number of iterations is more computationally intensive. + * + * @param niter - The new number of solver iterations. + */ + set numSolverIterations(niter: number) { + this.integrationParameters.numSolverIterations = niter; + } + + /** + * Number of internal Project Gauss Seidel (PGS) iterations run at each solver iteration (default: `1`). + */ + get numInternalPgsIterations(): number { + return this.integrationParameters.numInternalPgsIterations; + } + + /** + * Sets the Number of internal Project Gauss Seidel (PGS) iterations run at each solver iteration (default: `1`). + * + * Increasing this parameter will improve stability of the simulation. It will have a lesser effect than + * increasing `numSolverIterations` but is also less computationally expensive. + * + * @param niter - The new number of internal PGS iterations. + */ + set numInternalPgsIterations(niter: number) { + this.integrationParameters.numInternalPgsIterations = niter; + } + + /** + * The number of substeps continuous collision-detection can run (default: `1`). + */ + get maxCcdSubsteps(): number { + return this.integrationParameters.maxCcdSubsteps; + } + + /** + * Sets the number of substeps continuous collision-detection can run (default: `1`). + * + * CCD operates using a "motion clamping" mechanism where all fast-moving object trajectories will + * be truncated to their first impact on their path. The number of CCD substeps beyond 1 indicate how + * many times that trajectory will be updated and continued after a hit. This can results in smoother + * paths, but at a significant computational cost. + * + * @param niter - The new maximum number of CCD substeps. Setting to `0` disables CCD entirely. + */ + set maxCcdSubsteps(substeps: number) { + this.integrationParameters.maxCcdSubsteps = substeps; + } + + /** + * Creates a new rigid-body from the given rigid-body descriptor. + * + * @param body - The description of the rigid-body to create. + */ + public createRigidBody(body: RigidBodyDesc): RigidBody { + return this.bodies.createRigidBody(this.colliders, body); + } + + /** + * Creates a new character controller. + * + * @param offset - The artificial gap added between the character’s chape and its environment. + */ + public createCharacterController( + offset: number, + ): KinematicCharacterController { + let controller = new KinematicCharacterController( + offset, + this.integrationParameters, + this.broadPhase, + this.narrowPhase, + this.bodies, + this.colliders, + ); + this.characterControllers.add(controller); + return controller; + } + + /** + * Removes a character controller from this world. + * + * @param controller - The character controller to remove. + */ + public removeCharacterController(controller: KinematicCharacterController) { + this.characterControllers.delete(controller); + controller.free(); + } + + /** + * Creates a new PID (Proportional-Integral-Derivative) controller. + * + * @param kp - The Proportional gain applied to the instantaneous linear position errors. + * This is usually set to a multiple of the inverse of simulation step time + * (e.g. `60` if the delta-time is `1.0 / 60.0`). + * @param ki - The linear gain applied to the Integral part of the PID controller. + * @param kd - The Derivative gain applied to the instantaneous linear velocity errors. + * This is usually set to a value in `[0.0, 1.0]` where `0.0` implies no damping + * (no correction of velocity errors) and `1.0` implies complete damping (velocity errors + * are corrected in a single simulation step). + * @param axes - The axes affected by this controller. + * Only coordinate axes with a bit flags set to `true` will be taken into + * account when calculating the errors and corrections. + */ + public createPidController( + kp: number, + ki: number, + kd: number, + axes: PidAxesMask, + ): PidController { + let controller = new PidController( + this.integrationParameters, + this.bodies, + kp, + ki, + kd, + axes, + ); + this.pidControllers.add(controller); + return controller; + } + + /** + * Removes a PID controller from this world. + * + * @param controller - The PID controller to remove. + */ + public removePidController(controller: PidController) { + this.pidControllers.delete(controller); + controller.free(); + } + + + /** + * Creates a new collider. + * + * @param desc - The description of the collider. + * @param parent - The rigid-body this collider is attached to. + */ + public createCollider(desc: ColliderDesc, parent?: RigidBody): Collider { + let parentHandle = parent ? parent.handle : undefined; + return this.colliders.createCollider(this.bodies, desc, parentHandle); + } + + /** + * Creates a new impulse joint from the given joint descriptor. + * + * @param params - The description of the joint to create. + * @param parent1 - The first rigid-body attached to this joint. + * @param parent2 - The second rigid-body attached to this joint. + * @param wakeUp - Should the attached rigid-bodies be awakened? + */ + public createImpulseJoint( + params: JointData, + parent1: RigidBody, + parent2: RigidBody, + wakeUp: boolean, + ): ImpulseJoint { + return this.impulseJoints.createJoint( + this.bodies, + params, + parent1.handle, + parent2.handle, + wakeUp, + ); + } + + /** + * Creates a new multibody joint from the given joint descriptor. + * + * @param params - The description of the joint to create. + * @param parent1 - The first rigid-body attached to this joint. + * @param parent2 - The second rigid-body attached to this joint. + * @param wakeUp - Should the attached rigid-bodies be awakened? + */ + public createMultibodyJoint( + params: JointData, + parent1: RigidBody, + parent2: RigidBody, + wakeUp: boolean, + ): MultibodyJoint { + return this.multibodyJoints.createJoint( + params, + parent1.handle, + parent2.handle, + wakeUp, + ); + } + + /** + * Retrieves a rigid-body from its handle. + * + * @param handle - The integer handle of the rigid-body to retrieve. + */ + public getRigidBody(handle: RigidBodyHandle): RigidBody { + return this.bodies.get(handle); + } + + /** + * Retrieves a collider from its handle. + * + * @param handle - The integer handle of the collider to retrieve. + */ + public getCollider(handle: ColliderHandle): Collider { + return this.colliders.get(handle); + } + + /** + * Retrieves an impulse joint from its handle. + * + * @param handle - The integer handle of the impulse joint to retrieve. + */ + public getImpulseJoint(handle: ImpulseJointHandle): ImpulseJoint { + return this.impulseJoints.get(handle); + } + + /** + * Retrieves an multibody joint from its handle. + * + * @param handle - The integer handle of the multibody joint to retrieve. + */ + public getMultibodyJoint(handle: MultibodyJointHandle): MultibodyJoint { + return this.multibodyJoints.get(handle); + } + + /** + * Removes the given rigid-body from this physics world. + * + * This will remove this rigid-body as well as all its attached colliders and joints. + * Every other bodies touching or attached by joints to this rigid-body will be woken-up. + * + * @param body - The rigid-body to remove. + */ + public removeRigidBody(body: RigidBody) { + if (this.bodies) { + this.bodies.remove( + body.handle, + this.islands, + this.colliders, + this.impulseJoints, + this.multibodyJoints, + ); + } + } + + /** + * Removes the given collider from this physics world. + * + * @param collider - The collider to remove. + * @param wakeUp - If set to `true`, the rigid-body this collider is attached to will be awaken. + */ + public removeCollider(collider: Collider, wakeUp: boolean) { + if (this.colliders) { + this.colliders.remove( + collider.handle, + this.islands, + this.bodies, + wakeUp, + ); + } + } + + /** + * Removes the given impulse joint from this physics world. + * + * @param joint - The impulse joint to remove. + * @param wakeUp - If set to `true`, the rigid-bodies attached by this joint will be awaken. + */ + public removeImpulseJoint(joint: ImpulseJoint, wakeUp: boolean) { + if (this.impulseJoints) { + this.impulseJoints.remove(joint.handle, wakeUp); + } + } + + /** + * Removes the given multibody joint from this physics world. + * + * @param joint - The multibody joint to remove. + * @param wakeUp - If set to `true`, the rigid-bodies attached by this joint will be awaken. + */ + public removeMultibodyJoint(joint: MultibodyJoint, wakeUp: boolean) { + if (this.impulseJoints) { + this.multibodyJoints.remove(joint.handle, wakeUp); + } + } + + /** + * Applies the given closure to each collider managed by this physics world. + * + * @param f(collider) - The function to apply to each collider managed by this physics world. Called as `f(collider)`. + */ + public forEachCollider(f: (collider: Collider) => void) { + this.colliders.forEach(f); + } + + /** + * Applies the given closure to each rigid-body managed by this physics world. + * + * @param f(body) - The function to apply to each rigid-body managed by this physics world. Called as `f(collider)`. + */ + public forEachRigidBody(f: (body: RigidBody) => void) { + this.bodies.forEach(f); + } + + /** + * Applies the given closure to each active rigid-body managed by this physics world. + * + * After a short time of inactivity, a rigid-body is automatically deactivated ("asleep") by + * the physics engine in order to save computational power. A sleeping rigid-body never moves + * unless it is moved manually by the user. + * + * @param f - The function to apply to each active rigid-body managed by this physics world. Called as `f(collider)`. + */ + public forEachActiveRigidBody(f: (body: RigidBody) => void) { + this.bodies.forEachActiveRigidBody(this.islands, f); + } + + /** + * Find the closest intersection between a ray and the physics world. + * + * @param ray - The ray to cast. + * @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively + * limits the length of the ray to `ray.dir.norm() * maxToi`. + * @param solid - If `false` then the ray will attempt to hit the boundary of a shape, even if its + * origin already lies inside of a shape. In other terms, `true` implies that all shapes are plain, + * whereas `false` implies that all shapes are hollow for this ray-cast. + * @param groups - Used to filter the colliders that can or cannot be hit by the ray. + * @param filter - The callback to filter out which collider will be hit. + */ + public castRay( + ray: Ray, + maxToi: number, + solid: boolean, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: Collider, + filterExcludeRigidBody?: RigidBody, + filterPredicate?: (collider: Collider) => boolean, + ): RayColliderHit | null { + return this.broadPhase.castRay( + this.narrowPhase, + this.bodies, + this.colliders, + ray, + maxToi, + solid, + filterFlags, + filterGroups, + filterExcludeCollider ? filterExcludeCollider.handle : null, + filterExcludeRigidBody ? filterExcludeRigidBody.handle : null, + this.colliders.castClosure(filterPredicate), + ); + } + + /** + * Find the closest intersection between a ray and the physics world. + * + * This also computes the normal at the hit point. + * @param ray - The ray to cast. + * @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively + * limits the length of the ray to `ray.dir.norm() * maxToi`. + * @param solid - If `false` then the ray will attempt to hit the boundary of a shape, even if its + * origin already lies inside of a shape. In other terms, `true` implies that all shapes are plain, + * whereas `false` implies that all shapes are hollow for this ray-cast. + * @param groups - Used to filter the colliders that can or cannot be hit by the ray. + */ + public castRayAndGetNormal( + ray: Ray, + maxToi: number, + solid: boolean, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: Collider, + filterExcludeRigidBody?: RigidBody, + filterPredicate?: (collider: Collider) => boolean, + ): RayColliderIntersection | null { + return this.broadPhase.castRayAndGetNormal( + this.narrowPhase, + this.bodies, + this.colliders, + ray, + maxToi, + solid, + filterFlags, + filterGroups, + filterExcludeCollider ? filterExcludeCollider.handle : null, + filterExcludeRigidBody ? filterExcludeRigidBody.handle : null, + this.colliders.castClosure(filterPredicate), + ); + } + + /** + * Cast a ray and collects all the intersections between a ray and the scene. + * + * @param ray - The ray to cast. + * @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively + * limits the length of the ray to `ray.dir.norm() * maxToi`. + * @param solid - If `false` then the ray will attempt to hit the boundary of a shape, even if its + * origin already lies inside of a shape. In other terms, `true` implies that all shapes are plain, + * whereas `false` implies that all shapes are hollow for this ray-cast. + * @param groups - Used to filter the colliders that can or cannot be hit by the ray. + * @param callback - The callback called once per hit (in no particular order) between a ray and a collider. + * If this callback returns `false`, then the cast will stop and no further hits will be detected/reported. + */ + public intersectionsWithRay( + ray: Ray, + maxToi: number, + solid: boolean, + callback: (intersect: RayColliderIntersection) => boolean, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: Collider, + filterExcludeRigidBody?: RigidBody, + filterPredicate?: (collider: Collider) => boolean, + ) { + this.broadPhase.intersectionsWithRay( + this.narrowPhase, + this.bodies, + this.colliders, + ray, + maxToi, + solid, + callback, + filterFlags, + filterGroups, + filterExcludeCollider ? filterExcludeCollider.handle : null, + filterExcludeRigidBody ? filterExcludeRigidBody.handle : null, + this.colliders.castClosure(filterPredicate), + ); + } + + /** + * Gets the handle of up to one collider intersecting the given shape. + * + * @param shapePos - The position of the shape used for the intersection test. + * @param shapeRot - The orientation of the shape used for the intersection test. + * @param shape - The shape used for the intersection test. + * @param groups - The bit groups and filter associated to the ray, in order to only + * hit the colliders with collision groups compatible with the ray's group. + */ + public intersectionWithShape( + shapePos: Vector, + shapeRot: Rotation, + shape: Shape, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: Collider, + filterExcludeRigidBody?: RigidBody, + filterPredicate?: (collider: Collider) => boolean, + ): Collider | null { + let handle = this.broadPhase.intersectionWithShape( + this.narrowPhase, + this.bodies, + this.colliders, + shapePos, + shapeRot, + shape, + filterFlags, + filterGroups, + filterExcludeCollider ? filterExcludeCollider.handle : null, + filterExcludeRigidBody ? filterExcludeRigidBody.handle : null, + this.colliders.castClosure(filterPredicate), + ); + return handle != null ? this.colliders.get(handle) : null; + } + + /** + * Find the projection of a point on the closest collider. + * + * @param point - The point to project. + * @param solid - If this is set to `true` then the collider shapes are considered to + * be plain (if the point is located inside of a plain shape, its projection is the point + * itself). If it is set to `false` the collider shapes are considered to be hollow + * (if the point is located inside of an hollow shape, it is projected on the shape's + * boundary). + * @param groups - The bit groups and filter associated to the point to project, in order to only + * project on colliders with collision groups compatible with the ray's group. + */ + public projectPoint( + point: Vector, + solid: boolean, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: Collider, + filterExcludeRigidBody?: RigidBody, + filterPredicate?: (collider: Collider) => boolean, + ): PointColliderProjection | null { + return this.broadPhase.projectPoint( + this.narrowPhase, + this.bodies, + this.colliders, + point, + solid, + filterFlags, + filterGroups, + filterExcludeCollider ? filterExcludeCollider.handle : null, + filterExcludeRigidBody ? filterExcludeRigidBody.handle : null, + this.colliders.castClosure(filterPredicate), + ); + } + + /** + * Find the projection of a point on the closest collider. + * + * @param point - The point to project. + * @param groups - The bit groups and filter associated to the point to project, in order to only + * project on colliders with collision groups compatible with the ray's group. + */ + public projectPointAndGetFeature( + point: Vector, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: Collider, + filterExcludeRigidBody?: RigidBody, + filterPredicate?: (collider: Collider) => boolean, + ): PointColliderProjection | null { + return this.broadPhase.projectPointAndGetFeature( + this.narrowPhase, + this.bodies, + this.colliders, + point, + filterFlags, + filterGroups, + filterExcludeCollider ? filterExcludeCollider.handle : null, + filterExcludeRigidBody ? filterExcludeRigidBody.handle : null, + this.colliders.castClosure(filterPredicate), + ); + } + + /** + * Find all the colliders containing the given point. + * + * @param point - The point used for the containment test. + * @param groups - The bit groups and filter associated to the point to test, in order to only + * test on colliders with collision groups compatible with the ray's group. + * @param callback - A function called with the handles of each collider with a shape + * containing the `point`. + */ + public intersectionsWithPoint( + point: Vector, + callback: (handle: Collider) => boolean, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: Collider, + filterExcludeRigidBody?: RigidBody, + filterPredicate?: (collider: Collider) => boolean, + ) { + this.broadPhase.intersectionsWithPoint( + this.narrowPhase, + this.bodies, + this.colliders, + point, + this.colliders.castClosure(callback), + filterFlags, + filterGroups, + filterExcludeCollider ? filterExcludeCollider.handle : null, + filterExcludeRigidBody ? filterExcludeRigidBody.handle : null, + this.colliders.castClosure(filterPredicate), + ); + } + + /** + * Casts a shape at a constant linear velocity and retrieve the first collider it hits. + * This is similar to ray-casting except that we are casting a whole shape instead of + * just a point (the ray origin). + * + * @param shapePos - The initial position of the shape to cast. + * @param shapeRot - The initial rotation of the shape to cast. + * @param shapeVel - The constant velocity of the shape to cast (i.e. the cast direction). + * @param shape - The shape to cast. + * @param targetDistance − If the shape moves closer to this distance from a collider, a hit + * will be returned. + * @param maxToi - The maximum time-of-impact that can be reported by this cast. This effectively + * limits the distance traveled by the shape to `shapeVel.norm() * maxToi`. + * @param stopAtPenetration - If set to `false`, the linear shape-cast won’t immediately stop if + * the shape is penetrating another shape at its starting point **and** its trajectory is such + * that it’s on a path to exit that penetration state. + * @param groups - The bit groups and filter associated to the shape to cast, in order to only + * test on colliders with collision groups compatible with this group. + */ + public castShape( + shapePos: Vector, + shapeRot: Rotation, + shapeVel: Vector, + shape: Shape, + targetDistance: number, + maxToi: number, + stopAtPenetration: boolean, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: Collider, + filterExcludeRigidBody?: RigidBody, + filterPredicate?: (collider: Collider) => boolean, + ): ColliderShapeCastHit | null { + return this.broadPhase.castShape( + this.narrowPhase, + this.bodies, + this.colliders, + shapePos, + shapeRot, + shapeVel, + shape, + targetDistance, + maxToi, + stopAtPenetration, + filterFlags, + filterGroups, + filterExcludeCollider ? filterExcludeCollider.handle : null, + filterExcludeRigidBody ? filterExcludeRigidBody.handle : null, + this.colliders.castClosure(filterPredicate), + ); + } + + /** + * Retrieve all the colliders intersecting the given shape. + * + * @param shapePos - The position of the shape to test. + * @param shapeRot - The orientation of the shape to test. + * @param shape - The shape to test. + * @param groups - The bit groups and filter associated to the shape to test, in order to only + * test on colliders with collision groups compatible with this group. + * @param callback - A function called with the handles of each collider intersecting the `shape`. + */ + public intersectionsWithShape( + shapePos: Vector, + shapeRot: Rotation, + shape: Shape, + callback: (collider: Collider) => boolean, + filterFlags?: QueryFilterFlags, + filterGroups?: InteractionGroups, + filterExcludeCollider?: Collider, + filterExcludeRigidBody?: RigidBody, + filterPredicate?: (collider: Collider) => boolean, + ) { + this.broadPhase.intersectionsWithShape( + this.narrowPhase, + this.bodies, + this.colliders, + shapePos, + shapeRot, + shape, + this.colliders.castClosure(callback), + filterFlags, + filterGroups, + filterExcludeCollider ? filterExcludeCollider.handle : null, + filterExcludeRigidBody ? filterExcludeRigidBody.handle : null, + this.colliders.castClosure(filterPredicate), + ); + } + + /** + * Finds the handles of all the colliders with an AABB intersecting the given AABB. + * + * @param aabbCenter - The center of the AABB to test. + * @param aabbHalfExtents - The half-extents of the AABB to test. + * @param callback - The callback that will be called with the handles of all the colliders + * currently intersecting the given AABB. + */ + public collidersWithAabbIntersectingAabb( + aabbCenter: Vector, + aabbHalfExtents: Vector, + callback: (handle: Collider) => boolean, + ) { + this.broadPhase.collidersWithAabbIntersectingAabb( + this.narrowPhase, + this.bodies, + this.colliders, + aabbCenter, + aabbHalfExtents, + this.colliders.castClosure(callback), + ); + } + + /** + * Enumerates all the colliders potentially in contact with the given collider. + * + * @param collider1 - The second collider involved in the contact. + * @param f - Closure that will be called on each collider that is in contact with `collider1`. + */ + public contactPairsWith( + collider1: Collider, + f: (collider2: Collider) => void, + ) { + this.narrowPhase.contactPairsWith( + collider1.handle, + this.colliders.castClosure(f), + ); + } + + /** + * Enumerates all the colliders intersecting the given colliders, assuming one of them + * is a sensor. + */ + public intersectionPairsWith( + collider1: Collider, + f: (collider2: Collider) => void, + ) { + this.narrowPhase.intersectionPairsWith( + collider1.handle, + this.colliders.castClosure(f), + ); + } + + /** + * Iterates through all the contact manifolds between the given pair of colliders. + * + * @param collider1 - The first collider involved in the contact. + * @param collider2 - The second collider involved in the contact. + * @param f - Closure that will be called on each contact manifold between the two colliders. If the second argument + * passed to this closure is `true`, then the contact manifold data is flipped, i.e., methods like `localNormal1` + * actually apply to the `collider2` and fields like `localNormal2` apply to the `collider1`. + */ + public contactPair( + collider1: Collider, + collider2: Collider, + f: (manifold: TempContactManifold, flipped: boolean) => void, + ) { + this.narrowPhase.contactPair(collider1.handle, collider2.handle, f); + } + + /** + * Returns `true` if `collider1` and `collider2` intersect and at least one of them is a sensor. + * @param collider1 − The first collider involved in the intersection. + * @param collider2 − The second collider involved in the intersection. + */ + public intersectionPair(collider1: Collider, collider2: Collider): boolean { + return this.narrowPhase.intersectionPair( + collider1.handle, + collider2.handle, + ); + } + + /** + * Sets whether internal performance profiling is enabled (default: false). + * + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + set profilerEnabled(enabled: boolean) { + this.physicsPipeline.raw.set_profiler_enabled(enabled); + } + + /** + * Indicates if the internal performance profiling is enabled. + * + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + get profilerEnabled(): boolean { + return this.physicsPipeline.raw.is_profiler_enabled(); + } + + /** + * The time spent in milliseconds by the last step to run the entire simulation step. + * + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingStep(): number { + return this.physicsPipeline.raw.timing_step(); + } + + /** + * The time spent in milliseconds by the last step to run the collision-detection + * (broad-phase + narrow-phase). + * + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingCollisionDetection(): number { + return this.physicsPipeline.raw.timing_collision_detection(); + } + + /** + * The time spent in milliseconds by the last step to run the broad-phase. + * + * This timing is included in `timingCollisionDetection`. + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingBroadPhase(): number { + return this.physicsPipeline.raw.timing_broad_phase(); + } + + /** + * The time spent in milliseconds by the last step to run the narrow-phase. + * + * This timing is included in `timingCollisionDetection`. + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingNarrowPhase(): number { + return this.physicsPipeline.raw.timing_narrow_phase(); + } + + /** + * The time spent in milliseconds by the last step to run the constraint solver. + * + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingSolver(): number { + return this.physicsPipeline.raw.timing_solver(); + } + + /** + * The time spent in milliseconds by the last step to run the constraint + * initialization. + * + * This timing is included in `timingSolver`. + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingVelocityAssembly(): number { + return this.physicsPipeline.raw.timing_velocity_assembly(); + } + + /** + * The time spent in milliseconds by the last step to run the constraint + * resolution. + * + * This timing is included in `timingSolver`. + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingVelocityResolution(): number { + return this.physicsPipeline.raw.timing_velocity_resolution(); + } + + /** + * The time spent in milliseconds by the last step to run the rigid-body + * velocity update. + * + * This timing is included in `timingSolver`. + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingVelocityUpdate(): number { + return this.physicsPipeline.raw.timing_velocity_update(); + } + + /** + * The time spent in milliseconds by writing rigid-body velocities + * calculated by the solver back into the rigid-bodies. + * + * This timing is included in `timingSolver`. + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingVelocityWriteback(): number { + return this.physicsPipeline.raw.timing_velocity_writeback(); + } + + /** + * The total time spent in CCD detection and resolution. + * + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingCcd(): number { + return this.physicsPipeline.raw.timing_ccd(); + } + + /** + * The total time spent searching for the continuous hits during CCD. + * + * This timing is included in `timingCcd`. + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingCcdToiComputation(): number { + return this.physicsPipeline.raw.timing_ccd_toi_computation(); + } + + /** + * The total time spent in the broad-phase during CCD. + * + * This timing is included in `timingCcd`. + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingCcdBroadPhase(): number { + return this.physicsPipeline.raw.timing_ccd_broad_phase(); + } + + /** + * The total time spent in the narrow-phase during CCD. + * + * This timing is included in `timingCcd`. + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingCcdNarrowPhase(): number { + return this.physicsPipeline.raw.timing_ccd_narrow_phase(); + } + + /** + * The total time spent in the constraints resolution during CCD. + * + * This timing is included in `timingCcd`. + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingCcdSolver(): number { + return this.physicsPipeline.raw.timing_ccd_solver(); + } + + /** + * The total time spent in the islands calculation during CCD. + * + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingIslandConstruction(): number { + return this.physicsPipeline.raw.timing_island_construction(); + } + + /** + * The total time spent propagating detected user changes. + * + * Only works if the internal profiler is enabled with `World.profilerEnabled = true`. + */ + public timingUserChanges(): number { + return this.physicsPipeline.raw.timing_user_changes(); + } +} diff --git a/packages/rapier2d/src/raw.ts b/packages/rapier2d/src/raw.ts new file mode 100644 index 00000000..4eadaffa --- /dev/null +++ b/packages/rapier2d/src/raw.ts @@ -0,0 +1 @@ +export * from "../pkg/rapier_wasm2d"; diff --git a/packages/rapier2d/tsconfig.json b/packages/rapier2d/tsconfig.json new file mode 100644 index 00000000..17c8bb3f --- /dev/null +++ b/packages/rapier2d/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "strict": false, + "strictNullChecks": false + }, + "include": ["src/**/*", "pkg/*.d.ts"] +} diff --git a/packages/rapier2d/tsup.config.ts b/packages/rapier2d/tsup.config.ts new file mode 100644 index 00000000..933ffe82 --- /dev/null +++ b/packages/rapier2d/tsup.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm"], + dts: true, + sourcemap: true, + clean: true, + external: ["../pkg/rapier_wasm2d.js"], +});