@forgeax/engine-physics 0.1.25 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -212,6 +212,7 @@ not kinematic), `body-not-found` (no Rapier body for the entity), or
212
212
  ## Architecture Notes
213
213
 
214
214
  - **Not a backend**: this package defines interfaces and schemas only. Runtime simulation requires a backend (`@forgeax/engine-physics-rapier3d` or `@forgeax/engine-physics-rapier2d`).
215
+ - **Backend selection**: the Rapier backends are optional peer dependencies because the preset is loaded dynamically; install the backend selected by `physicsPlugin('rapier-3d')` or `physicsPlugin('rapier-2d')` (the umbrella Engine package already carries both).
215
216
  - **ECS bridge**: backend packages call `registerPhysicsSystems(world, Transform)` to wire the three-phase tick pipeline into the ECS schedule.
216
217
  - **Fire-and-forget**: WASM backends load asynchronously; entities spawned before load are picked up once the `PhysicsWorld` resource appears.
217
218
  - **Component schemas SSOT**: `packages/physics/src/components.ts` is the authoritative definition for all physics component fields, defaults, and types.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/collision-event.ts","../src/components.ts","../src/load-rapier-backend.mjs","../src/plugin-factory.ts","../src/system-set.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["// @forgeax/engine-physics — CollisionEvent ECS Event token placeholder.\n//\n// Emitted by physics tick systems during the Writeback phase.\n// Two states only: 'started' (new contact) and 'stopped' (separated).\n// No 'continued' event — use CollidingEntities component for ongoing contacts\n// (plan-strategy D-3).\n\nimport type { Vec3 } from '@forgeax/engine-math';\n\n/**\n * Collision event payload — per-contact-pair event emitted during Writeback.\n *\n * `type: 'started'` — two colliders just began touching.\n * `type: 'stopped'` — two colliders just separated.\n */\nexport interface CollisionEventPayload {\n type: 'started' | 'stopped';\n entityA: number;\n entityB: number;\n contactPoint: Vec3;\n contactNormal: Vec3;\n}\n\n/**\n * CollisionEvent constant — identifies the collision event type.\n * Backend systems push CollisionEventPayload instances into the event queue\n * during the Writeback phase; user systems drain via query.\n *\n * The backing ECS event infrastructure (Event<T> generic + world.drainEvent)\n * is deferred to a future feat. For M1, this is a type-only contract.\n */\nexport const CollisionEvent = '__CollisionEvent__' as const;\n\n/** Type-level identifier for the CollisionEvent event channel. */\nexport type CollisionEvent = typeof CollisionEvent;\n","// @forgeax/engine-physics — ECS Component schemas.\n//\n// RigidBody and Collider are the two user-facing entry points; AI users\n// spawn entities with these components to opt into physics simulation.\n// CollidingEntities is the runtime set-query component for continuous\n// collision status (started/stopped model, no 'continued' event).\n\nimport { type Component, defineComponent, type World } from '@forgeax/engine-ecs';\n\n/**\n * RigidBody motion type — 3-state discriminant mirroring Rapier's\n * Dynamic / Fixed / KinematicPositionBased triplet.\n *\n * `'static'`: infinite mass, never moves (Rapier Fixed).\n * `'dynamic'`: driven by forces, gravity, collisions (Rapier Dynamic).\n * `'kinematic'`: user-controlled position, velocity derived by engine\n * (Rapier KinematicPositionBased).\n */\nexport type RigidBodyType = 'static' | 'dynamic' | 'kinematic';\n\n/**\n * Collider shape discriminant — 3 AI-friendly shape names.\n *\n * `'cuboid'`: box shape defined by half-extents (x, y, z).\n * `'sphere'`: sphere defined by radius.\n * `'capsule'`: capsule defined by half-height + radius.\n *\n * Named `'sphere'` not `'ball'` per plan-strategy D-5: AI users see\n * the familiar geometric term; backend maps to Rapier `ColliderDesc.ball()`.\n */\nexport type ColliderShape = 'cuboid' | 'sphere' | 'capsule';\n\n// ─── D-3: numeric enum constants + narrowing helpers ─────────────────────\n//\n// Aligned with `packages/runtime/src/components/camera.ts:41-53`\n// `cameraProjectionFromF32` pattern. The ECS `enum` field maps to `number`\n// (Uint32Array column); these constants let AI users write\n// `{ type: RigidBodyTypeValue.dynamic }` instead of bare magic numbers,\n// and the narrowing helpers let backends switch cleanly on the string union.\n//\n// Declared BEFORE the RigidBody / Collider components so each component's enum\n// field descriptor can reference the SAME `*Value` map as its `labels`\n// (Derive, don't Duplicate — one object is both the AI-facing const AND the\n// schema-projected label map that `describeComponent` surfaces).\n\n/** Numeric value for static rigid body (Rapier Fixed). */\nexport const RIGID_BODY_TYPE_STATIC = 0;\n/** Numeric value for dynamic rigid body (Rapier Dynamic). */\nexport const RIGID_BODY_TYPE_DYNAMIC = 1;\n/** Numeric value for kinematic rigid body (Rapier KinematicPositionBased). */\nexport const RIGID_BODY_TYPE_KINEMATIC = 2;\n\nexport const RigidBodyTypeValue = {\n static: RIGID_BODY_TYPE_STATIC,\n dynamic: RIGID_BODY_TYPE_DYNAMIC,\n kinematic: RIGID_BODY_TYPE_KINEMATIC,\n} as const;\n\nexport function rigidBodyTypeFromF32(n: number): RigidBodyType {\n if (n === RIGID_BODY_TYPE_DYNAMIC) return 'dynamic';\n if (n === RIGID_BODY_TYPE_KINEMATIC) return 'kinematic';\n return 'static';\n}\n\n/** Numeric value for cuboid collider shape. */\nexport const COLLIDER_SHAPE_CUBOID = 0;\n/** Numeric value for sphere collider shape. */\nexport const COLLIDER_SHAPE_SPHERE = 1;\n/** Numeric value for capsule collider shape. */\nexport const COLLIDER_SHAPE_CAPSULE = 2;\n\nexport const ColliderShapeValue = {\n cuboid: COLLIDER_SHAPE_CUBOID,\n sphere: COLLIDER_SHAPE_SPHERE,\n capsule: COLLIDER_SHAPE_CAPSULE,\n} as const;\n\nexport function colliderShapeFromF32(n: number): ColliderShape {\n if (n === COLLIDER_SHAPE_SPHERE) return 'sphere';\n if (n === COLLIDER_SHAPE_CAPSULE) return 'capsule';\n return 'cuboid';\n}\n\n/**\n * ECS Component: rigid body physics properties.\n *\n * AI user entry point — spawn with `world.spawn(RigidBody({ type: 'dynamic' }))`\n * to opt an entity into physics simulation.\n *\n * | Field | Type | Default | Purpose |\n * |:--|:--|:--|:--|\n * | `type` | `RigidBodyType` | `'dynamic'` | Motion type discriminant |\n * | `mass` | `number` | `1.0` | Linear mass (> 0 for dynamic) |\n * | `linearDamping` | `number` | `0.0` | Velocity damping factor [0, 1] |\n * | `angularDamping` | `number` | `0.0` | Angular velocity damping [0, 1] |\n * | `gravityScale` | `number` | `1.0` | Per-body gravity multiplier |\n * | `ccdEnabled` | `boolean` | `false` | Continuous collision detection |\n *\n * `type` declares `labels: RigidBodyTypeValue` so `describeComponent` projects\n * the `static=0 / dynamic=1 / kinematic=2` map through the front door — an AI\n * learns the legal variants from the schema, not from engine source.\n */\nexport const RigidBody = defineComponent('RigidBody', {\n type: { type: 'enum', default: RIGID_BODY_TYPE_DYNAMIC, labels: RigidBodyTypeValue },\n mass: { type: 'f32', default: 1 },\n linearDamping: { type: 'f32', default: 0 },\n angularDamping: { type: 'f32', default: 0 },\n gravityScale: { type: 'f32', default: 1 },\n ccdEnabled: { type: 'bool', default: false },\n});\n\n/**\n * ECS Component: collision geometry.\n *\n * Spawn alongside RigidBody to give an entity a collision shape. Entities\n * with Collider but no RigidBody are treated as static colliders (Rapier\n * native behavior — collider without parent body is fixed): `physicsSyncBackend`\n * synthesizes an implicit static body for them, so a bare-Collider floor/wall is\n * simulated as immovable level geometry — the natural way to author static\n * scenery without a redundant `RigidBody{static}`.\n *\n * | Field | Type | Default | Purpose |\n * |:--|:--|:--|:--|\n * | `shape` | `ColliderShape` | — | Shape discriminant |\n * | `halfExtents` | `[number, number, number]` | `[0.5, 0.5, 0.5]` | Cuboid half-width/height/depth |\n * | `radius` | `number` | `0.5` | Sphere / capsule radius |\n * | `halfHeight` | `number` | `0.5` | Capsule half-height |\n * | `friction` | `number` | `0.5` | Coulomb friction coefficient |\n * | `restitution` | `number` | `0.0` | Elasticity (1.0 = perfect bounce) |\n * | `density` | `number` | `1.0` | Mass density (alternative to mass) |\n * | `isSensor` | `bool` | `false` | Sensor mode (detect, no physical response) |\n * | `collisionGroups` | `u32` | `0x0001_FFFF` | 32-bit packed membership/filter |\n * | `solverGroups` | `u32` | `0xFFFF_FFFF` | 32-bit packed constraint groups |\n */\nexport const Collider = defineComponent('Collider', {\n shape: { type: 'enum', default: COLLIDER_SHAPE_CUBOID, labels: ColliderShapeValue },\n // feat-20260709 M4: cuboid half-extents collapsed from 3 per-axis scalar\n // columns into one inline array<f32,3> column. Explicit layer-2 default\n // (the array layer-3 fallback is all-zero, which would give a degenerate\n // zero-size box). radius/halfHeight stay scalar (OOS-1: independent\n // sphere/capsule params, not part of the cuboid vec).\n halfExtents: { type: 'array<f32, 3>', default: new Float32Array([0.5, 0.5, 0.5]) },\n radius: { type: 'f32', default: 0.5 },\n halfHeight: { type: 'f32', default: 0.5 },\n friction: { type: 'f32', default: 0.5 },\n restitution: { type: 'f32', default: 0 },\n density: { type: 'f32', default: 1 },\n isSensor: { type: 'bool', default: false },\n collisionGroups: { type: 'u32', default: 0x0001_ffff },\n solverGroups: { type: 'u32', default: 0xffff_ffff },\n});\n\n/**\n * ECS Component: kinematic character controller tuning + output state.\n *\n * Spawn alongside a `RigidBody({ type: 'kinematic' })` + `Collider` to opt an\n * entity into collision-aware movement via `PhysicsWorld.moveAndSlide`. The\n * tuning fields are stable character properties; `grounded` is written back by\n * the engine after each `moveAndSlide` (game code reads it, never writes it).\n *\n * All fields are flat scalars in engine units (degrees, world-space distance) —\n * no Rapier types leak through. Slope angles use the `Deg` suffix to make the\n * unit explicit; the backend translates degrees to radians. A single field\n * carries both the on/off switch and the value: `autoStepMaxHeight === 0`\n * disables auto-step, `snapToGroundDist === 0` disables snap-to-ground.\n *\n * 2D and 3D reuse this same component (plan-strategy D-9): every field is a\n * dimension-agnostic scalar, so no separate 2D component is needed.\n *\n * | Field | Type | Default | Purpose |\n * |:--|:--|:--|:--|\n * | `offset` | `f32` | `0.01` | Skin thickness, prevents penetration |\n * | `maxSlopeClimbDeg` | `f32` | `45` | Max climbable slope angle (degrees) |\n * | `minSlopeSlideDeg` | `f32` | `30` | Slope angle past which sliding starts (degrees) |\n * | `autoStepMaxHeight` | `f32` | `0.3` | Max auto-step height (0 = off) |\n * | `autoStepMinWidth` | `f32` | `0.2` | Min step width to be steppable |\n * | `snapToGroundDist` | `f32` | `0.2` | Downhill ground-snap distance (0 = off) |\n * | `grounded` | `bool` | `false` | Engine-written: grounded after last move |\n */\nexport const CharacterController = defineComponent('CharacterController', {\n offset: { type: 'f32', default: 0.01 },\n maxSlopeClimbDeg: { type: 'f32', default: 45 },\n minSlopeSlideDeg: { type: 'f32', default: 30 },\n autoStepMaxHeight: { type: 'f32', default: 0.3 },\n autoStepMinWidth: { type: 'f32', default: 0.2 },\n snapToGroundDist: { type: 'f32', default: 0.2 },\n grounded: { type: 'bool', default: false, transient: true },\n});\n\n/**\n * ECS Component: set of entities currently colliding with the holder entity.\n *\n * Maintained by the physics tick systems — entities are added on collision\n * start (`CollisionEvent.started`) and removed on collision stop\n * (`CollisionEvent.stopped`). AI users query this component to know whose\n * colliders overlap right now without consuming per-frame events.\n *\n * This is the `'continued'` equivalent — no repeated per-frame events,\n * one component query per frame exposes the full active contact set\n * (plan-strategy D-3: CollidingEntities set-query mode).\n */\nexport const CollidingEntities = defineComponent(\n 'CollidingEntities',\n {\n entities: { type: 'array<entity>' },\n },\n { transient: true },\n);\n\nconst PHYSICS_COMPONENTS: readonly Component[] = [\n CharacterController,\n Collider,\n CollidingEntities,\n RigidBody,\n];\n\n/** Install the physics component vocabulary in a World and release its leases on teardown. */\nexport function registerPhysicsComponents(world: World): () => void {\n const leases = PHYSICS_COMPONENTS.map((component) =>\n world.components.register(component).unwrap(),\n );\n return () => {\n for (let index = leases.length - 1; index >= 0; index -= 1) leases[index]?.dispose();\n };\n}\n","// Keep literal specifiers visible to bundlers without pulling backend declarations into the physics TypeScript graph.\nexport function loadRapier3DBackend() {\n return import('@forgeax/engine-physics-rapier3d');\n}\n\nexport function loadRapier2DBackend() {\n return import('@forgeax/engine-physics-rapier2d');\n}\n","// @forgeax/engine-physics -- physicsPlugin(backend) factory (M2 / w10, plan-strategy D-5 / D-7).\n//\n// physicsPlugin lives in @forgeax/engine-physics (the interface package, C-9)\n// and accepts an interface->backend dependency inversion: its async apply\n// dynamic-imports the rapier 2D / 3D backend on demand. The backends are\n// declared as devDependencies in this package's package.json (a regular\n// dependency would form a physics <-> rapier cycle since the backends depend on\n// the interface package); the consuming app declares the real runtime dep.\n//\n// charter awareness:\n// P3 explicit failure: WASM load failure rejects plugin activation and the\n// App boundary preserves the cause; it is never a silent skip.\n// P4 consistent abstraction: physicsPlugin shares the same Plugin shape as\n// transform / audio -- one mental model covers every wiring.\n\nimport type { Plugin } from '@forgeax/engine-plugin';\nimport { registerPhysicsComponents } from './components';\nimport { PhysicsError } from './errors';\nimport { loadRapier2DBackend, loadRapier3DBackend } from './load-rapier-backend.mjs';\nimport type { PhysicsWorld, PhysicsWorld2D } from './physics-world';\n\ninterface Rapier3DBackendModule {\n loadRapier3D(): Promise<unknown>;\n createRapier3DPhysicsWorld(rapier: unknown): PhysicsWorld;\n registerPhysicsSystems(world: import('@forgeax/engine-ecs').World): () => void;\n}\n\ninterface Rapier2DBackendModule {\n loadRapier2D(): Promise<unknown>;\n createRapier2DPhysicsWorld(rapier: unknown): PhysicsWorld2D;\n registerPhysicsSystems2D(world: import('@forgeax/engine-ecs').World): () => void;\n}\n\n/** Rapier backend selector. */\nexport type PhysicsBackend = 'rapier-2d' | 'rapier-3d';\n\nfunction normalizeWasmLoadFailure(backend: PhysicsBackend, cause: unknown): PhysicsError {\n if (cause instanceof PhysicsError && cause.code === 'wasm-load-failed') return cause;\n const reason = cause instanceof Error ? cause.message : String(cause);\n return new PhysicsError({\n code: 'wasm-load-failed',\n expected: `successful import and WASM initialization for ${backend}`,\n hint: `Rapier backend activation failed: ${reason}`,\n detail: { code: 'wasm-load-failed', reason },\n });\n}\n\ndeclare module '@forgeax/engine-plugin' {\n interface EngineContextServices {\n physics?: PhysicsWorld | PhysicsWorld2D;\n }\n}\n\n/**\n * physicsPlugin(backend) dynamically imports the Rapier backend,\n * loads the WASM module, creates the PhysicsWorld, inserts it as the\n * 'PhysicsWorld' world resource, and registers the three-phase tick systems.\n *\n * The resource is inserted before registering systems so moveAndSlide resolves\n * `PhysicsWorld` on the first tick. Cordis owns rollback if any later effect\n * fails.\n *\n * @param backend 'rapier-2d' or 'rapier-3d'\n */\nexport function physicsPlugin(backend: PhysicsBackend): Plugin {\n return {\n name: 'physics',\n inject: ['world'],\n provide: 'physics',\n async apply(ctx) {\n const world = ctx.world;\n let physics: PhysicsWorld | PhysicsWorld2D;\n let registerSystems: () => () => void;\n if (backend === 'rapier-3d') {\n let module: Rapier3DBackendModule;\n let rapier: unknown;\n try {\n module = (await loadRapier3DBackend()) as Rapier3DBackendModule;\n rapier = await module.loadRapier3D();\n } catch (cause) {\n throw normalizeWasmLoadFailure(backend, cause);\n }\n if (rapier instanceof PhysicsError) throw normalizeWasmLoadFailure(backend, rapier);\n const { createRapier3DPhysicsWorld, registerPhysicsSystems } = module;\n physics = createRapier3DPhysicsWorld(rapier);\n registerSystems = () => registerPhysicsSystems(world);\n } else {\n let module: Rapier2DBackendModule;\n let rapier: unknown;\n try {\n module = (await loadRapier2DBackend()) as Rapier2DBackendModule;\n rapier = await module.loadRapier2D();\n } catch (cause) {\n throw normalizeWasmLoadFailure(backend, cause);\n }\n if (rapier instanceof PhysicsError) throw normalizeWasmLoadFailure(backend, rapier);\n const { createRapier2DPhysicsWorld, registerPhysicsSystems2D } = module;\n physics = createRapier2DPhysicsWorld(rapier);\n registerSystems = () => registerPhysicsSystems2D(world);\n }\n ctx.effect(() => registerPhysicsComponents(world), 'physics/components');\n ctx.effect(() => {\n world.insertResource('PhysicsWorld', physics);\n return () => {\n world.removeResource('PhysicsWorld');\n physics.dispose();\n };\n }, 'physics/resource');\n ctx.effect(() => {\n const unregister = registerSystems();\n return () => unregister();\n }, 'physics/systems');\n ctx.provide('physics', physics);\n },\n };\n}\n","import { defineSystemSet } from '@forgeax/engine-ecs';\n\nexport const PhysicsSet = defineSystemSet({ name: 'physics' });\n"]}
1
+ {"version":3,"sources":["../src/collision-event.ts","../src/components.ts","../src/load-rapier-backend.mjs","../src/plugin-factory.ts","../src/system-set.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["// @forgeax/engine-physics — CollisionEvent ECS Event token placeholder.\n//\n// Emitted by physics tick systems during the Writeback phase.\n// Two states only: 'started' (new contact) and 'stopped' (separated).\n// No 'continued' event — use CollidingEntities component for ongoing contacts\n// (plan-strategy D-3).\n\nimport type { Vec3 } from '@forgeax/engine-math';\n\n/**\n * Collision event payload — per-contact-pair event emitted during Writeback.\n *\n * `type: 'started'` — two colliders just began touching.\n * `type: 'stopped'` — two colliders just separated.\n */\nexport interface CollisionEventPayload {\n type: 'started' | 'stopped';\n entityA: number;\n entityB: number;\n contactPoint: Vec3;\n contactNormal: Vec3;\n}\n\n/**\n * CollisionEvent constant — identifies the collision event type.\n * Backend systems push CollisionEventPayload instances into the event queue\n * during the Writeback phase; user systems drain via query.\n *\n * The backing ECS event infrastructure (Event<T> generic + world.drainEvent)\n * is deferred to a future feat. For M1, this is a type-only contract.\n */\nexport const CollisionEvent = '__CollisionEvent__' as const;\n\n/** Type-level identifier for the CollisionEvent event channel. */\nexport type CollisionEvent = typeof CollisionEvent;\n","// @forgeax/engine-physics — ECS Component schemas.\n//\n// RigidBody and Collider are the two user-facing entry points; AI users\n// spawn entities with these components to opt into physics simulation.\n// CollidingEntities is the runtime set-query component for continuous\n// collision status (started/stopped model, no 'continued' event).\n\nimport { type Component, defineComponent, type World } from '@forgeax/engine-ecs';\n\n/**\n * RigidBody motion type — 3-state discriminant mirroring Rapier's\n * Dynamic / Fixed / KinematicPositionBased triplet.\n *\n * `'static'`: infinite mass, never moves (Rapier Fixed).\n * `'dynamic'`: driven by forces, gravity, collisions (Rapier Dynamic).\n * `'kinematic'`: user-controlled position, velocity derived by engine\n * (Rapier KinematicPositionBased).\n */\nexport type RigidBodyType = 'static' | 'dynamic' | 'kinematic';\n\n/**\n * Collider shape discriminant — 3 AI-friendly shape names.\n *\n * `'cuboid'`: box shape defined by half-extents (x, y, z).\n * `'sphere'`: sphere defined by radius.\n * `'capsule'`: capsule defined by half-height + radius.\n *\n * Named `'sphere'` not `'ball'` per plan-strategy D-5: AI users see\n * the familiar geometric term; backend maps to Rapier `ColliderDesc.ball()`.\n */\nexport type ColliderShape = 'cuboid' | 'sphere' | 'capsule';\n\n// ─── D-3: numeric enum constants + narrowing helpers ─────────────────────\n//\n// Aligned with `packages/runtime/src/components/camera.ts:41-53`\n// `cameraProjectionFromF32` pattern. The ECS `enum` field maps to `number`\n// (Uint32Array column); these constants let AI users write\n// `{ type: RigidBodyTypeValue.dynamic }` instead of bare magic numbers,\n// and the narrowing helpers let backends switch cleanly on the string union.\n//\n// Declared BEFORE the RigidBody / Collider components so each component's enum\n// field descriptor can reference the SAME `*Value` map as its `labels`\n// (Derive, don't Duplicate — one object is both the AI-facing const AND the\n// schema-projected label map that `describeComponent` surfaces).\n\n/** Numeric value for static rigid body (Rapier Fixed). */\nexport const RIGID_BODY_TYPE_STATIC = 0;\n/** Numeric value for dynamic rigid body (Rapier Dynamic). */\nexport const RIGID_BODY_TYPE_DYNAMIC = 1;\n/** Numeric value for kinematic rigid body (Rapier KinematicPositionBased). */\nexport const RIGID_BODY_TYPE_KINEMATIC = 2;\n\nexport const RigidBodyTypeValue = {\n static: RIGID_BODY_TYPE_STATIC,\n dynamic: RIGID_BODY_TYPE_DYNAMIC,\n kinematic: RIGID_BODY_TYPE_KINEMATIC,\n} as const;\n\nexport function rigidBodyTypeFromF32(n: number): RigidBodyType {\n if (n === RIGID_BODY_TYPE_DYNAMIC) return 'dynamic';\n if (n === RIGID_BODY_TYPE_KINEMATIC) return 'kinematic';\n return 'static';\n}\n\n/** Numeric value for cuboid collider shape. */\nexport const COLLIDER_SHAPE_CUBOID = 0;\n/** Numeric value for sphere collider shape. */\nexport const COLLIDER_SHAPE_SPHERE = 1;\n/** Numeric value for capsule collider shape. */\nexport const COLLIDER_SHAPE_CAPSULE = 2;\n\nexport const ColliderShapeValue = {\n cuboid: COLLIDER_SHAPE_CUBOID,\n sphere: COLLIDER_SHAPE_SPHERE,\n capsule: COLLIDER_SHAPE_CAPSULE,\n} as const;\n\nexport function colliderShapeFromF32(n: number): ColliderShape {\n if (n === COLLIDER_SHAPE_SPHERE) return 'sphere';\n if (n === COLLIDER_SHAPE_CAPSULE) return 'capsule';\n return 'cuboid';\n}\n\n/**\n * ECS Component: rigid body physics properties.\n *\n * AI user entry point — spawn with `world.spawn(RigidBody({ type: 'dynamic' }))`\n * to opt an entity into physics simulation.\n *\n * | Field | Type | Default | Purpose |\n * |:--|:--|:--|:--|\n * | `type` | `RigidBodyType` | `'dynamic'` | Motion type discriminant |\n * | `mass` | `number` | `1.0` | Linear mass (> 0 for dynamic) |\n * | `linearDamping` | `number` | `0.0` | Velocity damping factor [0, 1] |\n * | `angularDamping` | `number` | `0.0` | Angular velocity damping [0, 1] |\n * | `gravityScale` | `number` | `1.0` | Per-body gravity multiplier |\n * | `ccdEnabled` | `boolean` | `false` | Continuous collision detection |\n *\n * `type` declares `labels: RigidBodyTypeValue` so `describeComponent` projects\n * the `static=0 / dynamic=1 / kinematic=2` map through the front door — an AI\n * learns the legal variants from the schema, not from engine source.\n */\nexport const RigidBody = defineComponent('RigidBody', {\n type: { type: 'enum', default: RIGID_BODY_TYPE_DYNAMIC, labels: RigidBodyTypeValue },\n mass: { type: 'f32', default: 1 },\n linearDamping: { type: 'f32', default: 0 },\n angularDamping: { type: 'f32', default: 0 },\n gravityScale: { type: 'f32', default: 1 },\n ccdEnabled: { type: 'bool', default: false },\n});\n\n/**\n * ECS Component: collision geometry.\n *\n * Spawn alongside RigidBody to give an entity a collision shape. Entities\n * with Collider but no RigidBody are treated as static colliders (Rapier\n * native behavior — collider without parent body is fixed): `physicsSyncBackend`\n * synthesizes an implicit static body for them, so a bare-Collider floor/wall is\n * simulated as immovable level geometry — the natural way to author static\n * scenery without a redundant `RigidBody{static}`.\n *\n * | Field | Type | Default | Purpose |\n * |:--|:--|:--|:--|\n * | `shape` | `ColliderShape` | — | Shape discriminant |\n * | `halfExtents` | `[number, number, number]` | `[0.5, 0.5, 0.5]` | Cuboid half-width/height/depth |\n * | `radius` | `number` | `0.5` | Sphere / capsule radius |\n * | `halfHeight` | `number` | `0.5` | Capsule half-height |\n * | `friction` | `number` | `0.5` | Coulomb friction coefficient |\n * | `restitution` | `number` | `0.0` | Elasticity (1.0 = perfect bounce) |\n * | `density` | `number` | `1.0` | Mass density (alternative to mass) |\n * | `isSensor` | `bool` | `false` | Sensor mode (detect, no physical response) |\n * | `collisionGroups` | `u32` | `0x0001_FFFF` | 32-bit packed membership/filter |\n * | `solverGroups` | `u32` | `0xFFFF_FFFF` | 32-bit packed constraint groups |\n */\nexport const Collider = defineComponent('Collider', {\n shape: { type: 'enum', default: COLLIDER_SHAPE_CUBOID, labels: ColliderShapeValue },\n // feat-20260709 M4: cuboid half-extents collapsed from 3 per-axis scalar\n // columns into one inline array<f32,3> column. Explicit layer-2 default\n // (the array layer-3 fallback is all-zero, which would give a degenerate\n // zero-size box). radius/halfHeight stay scalar (OOS-1: independent\n // sphere/capsule params, not part of the cuboid vec).\n halfExtents: { type: 'array<f32, 3>', default: new Float32Array([0.5, 0.5, 0.5]) },\n radius: { type: 'f32', default: 0.5 },\n halfHeight: { type: 'f32', default: 0.5 },\n friction: { type: 'f32', default: 0.5 },\n restitution: { type: 'f32', default: 0 },\n density: { type: 'f32', default: 1 },\n isSensor: { type: 'bool', default: false },\n collisionGroups: { type: 'u32', default: 0x0001_ffff },\n solverGroups: { type: 'u32', default: 0xffff_ffff },\n});\n\n/**\n * ECS Component: kinematic character controller tuning + output state.\n *\n * Spawn alongside a `RigidBody({ type: 'kinematic' })` + `Collider` to opt an\n * entity into collision-aware movement via `PhysicsWorld.moveAndSlide`. The\n * tuning fields are stable character properties; `grounded` is written back by\n * the engine after each `moveAndSlide` (game code reads it, never writes it).\n *\n * All fields are flat scalars in engine units (degrees, world-space distance) —\n * no Rapier types leak through. Slope angles use the `Deg` suffix to make the\n * unit explicit; the backend translates degrees to radians. A single field\n * carries both the on/off switch and the value: `autoStepMaxHeight === 0`\n * disables auto-step, `snapToGroundDist === 0` disables snap-to-ground.\n *\n * 2D and 3D reuse this same component (plan-strategy D-9): every field is a\n * dimension-agnostic scalar, so no separate 2D component is needed.\n *\n * | Field | Type | Default | Purpose |\n * |:--|:--|:--|:--|\n * | `offset` | `f32` | `0.01` | Skin thickness, prevents penetration |\n * | `maxSlopeClimbDeg` | `f32` | `45` | Max climbable slope angle (degrees) |\n * | `minSlopeSlideDeg` | `f32` | `30` | Slope angle past which sliding starts (degrees) |\n * | `autoStepMaxHeight` | `f32` | `0.3` | Max auto-step height (0 = off) |\n * | `autoStepMinWidth` | `f32` | `0.2` | Min step width to be steppable |\n * | `snapToGroundDist` | `f32` | `0.2` | Downhill ground-snap distance (0 = off) |\n * | `grounded` | `bool` | `false` | Engine-written: grounded after last move |\n */\nexport const CharacterController = defineComponent('CharacterController', {\n offset: { type: 'f32', default: 0.01 },\n maxSlopeClimbDeg: { type: 'f32', default: 45 },\n minSlopeSlideDeg: { type: 'f32', default: 30 },\n autoStepMaxHeight: { type: 'f32', default: 0.3 },\n autoStepMinWidth: { type: 'f32', default: 0.2 },\n snapToGroundDist: { type: 'f32', default: 0.2 },\n grounded: { type: 'bool', default: false, transient: true },\n});\n\n/**\n * ECS Component: set of entities currently colliding with the holder entity.\n *\n * Maintained by the physics tick systems — entities are added on collision\n * start (`CollisionEvent.started`) and removed on collision stop\n * (`CollisionEvent.stopped`). AI users query this component to know whose\n * colliders overlap right now without consuming per-frame events.\n *\n * This is the `'continued'` equivalent — no repeated per-frame events,\n * one component query per frame exposes the full active contact set\n * (plan-strategy D-3: CollidingEntities set-query mode).\n */\nexport const CollidingEntities = defineComponent(\n 'CollidingEntities',\n {\n entities: { type: 'array<entity>' },\n },\n { transient: true },\n);\n\nconst PHYSICS_COMPONENTS: readonly Component[] = [\n CharacterController,\n Collider,\n CollidingEntities,\n RigidBody,\n];\n\n/** Install the physics component vocabulary in a World and release its leases on teardown. */\nexport function registerPhysicsComponents(world: World): () => void {\n const leases = PHYSICS_COMPONENTS.map((component) =>\n world.components.register(component).unwrap(),\n );\n return () => {\n for (let index = leases.length - 1; index >= 0; index -= 1) leases[index]?.dispose();\n };\n}\n","// Keep literal specifiers visible to bundlers without pulling backend declarations into the physics TypeScript graph.\nexport function loadRapier3DBackend() {\n return import('@forgeax/engine-physics-rapier3d');\n}\n\nexport function loadRapier2DBackend() {\n return import('@forgeax/engine-physics-rapier2d');\n}\n","// @forgeax/engine-physics -- physicsPlugin(backend) factory (M2 / w10, plan-strategy D-5 / D-7).\n//\n// physicsPlugin lives in @forgeax/engine-physics (the interface package, C-9)\n// and accepts an interface->backend dependency inversion: its async apply\n// dynamic-imports the rapier 2D / 3D backend on demand. The backends are\n// optional peerDependencies in this package's package.json (a regular\n// dependency would form a physics <-> rapier cycle since the backends depend on\n// the interface package); the consuming app declares the selected runtime dep.\n//\n// charter awareness:\n// P3 explicit failure: WASM load failure rejects plugin activation and the\n// App boundary preserves the cause; it is never a silent skip.\n// P4 consistent abstraction: physicsPlugin shares the same Plugin shape as\n// transform / audio -- one mental model covers every wiring.\n\nimport type { Plugin } from '@forgeax/engine-plugin';\nimport { registerPhysicsComponents } from './components';\nimport { PhysicsError } from './errors';\nimport { loadRapier2DBackend, loadRapier3DBackend } from './load-rapier-backend.mjs';\nimport type { PhysicsWorld, PhysicsWorld2D } from './physics-world';\n\ninterface Rapier3DBackendModule {\n loadRapier3D(): Promise<unknown>;\n createRapier3DPhysicsWorld(rapier: unknown): PhysicsWorld;\n registerPhysicsSystems(world: import('@forgeax/engine-ecs').World): () => void;\n}\n\ninterface Rapier2DBackendModule {\n loadRapier2D(): Promise<unknown>;\n createRapier2DPhysicsWorld(rapier: unknown): PhysicsWorld2D;\n registerPhysicsSystems2D(world: import('@forgeax/engine-ecs').World): () => void;\n}\n\n/** Rapier backend selector. */\nexport type PhysicsBackend = 'rapier-2d' | 'rapier-3d';\n\nfunction normalizeWasmLoadFailure(backend: PhysicsBackend, cause: unknown): PhysicsError {\n if (cause instanceof PhysicsError && cause.code === 'wasm-load-failed') return cause;\n const reason = cause instanceof Error ? cause.message : String(cause);\n return new PhysicsError({\n code: 'wasm-load-failed',\n expected: `successful import and WASM initialization for ${backend}`,\n hint: `Rapier backend activation failed: ${reason}`,\n detail: { code: 'wasm-load-failed', reason },\n });\n}\n\ndeclare module '@forgeax/engine-plugin' {\n interface EngineContextServices {\n physics?: PhysicsWorld | PhysicsWorld2D;\n }\n}\n\n/**\n * physicsPlugin(backend) dynamically imports the Rapier backend,\n * loads the WASM module, creates the PhysicsWorld, inserts it as the\n * 'PhysicsWorld' world resource, and registers the three-phase tick systems.\n *\n * The resource is inserted before registering systems so moveAndSlide resolves\n * `PhysicsWorld` on the first tick. Cordis owns rollback if any later effect\n * fails.\n *\n * @param backend 'rapier-2d' or 'rapier-3d'\n */\nexport function physicsPlugin(backend: PhysicsBackend): Plugin {\n return {\n name: 'physics',\n inject: ['world'],\n provide: 'physics',\n async apply(ctx) {\n const world = ctx.world;\n let physics: PhysicsWorld | PhysicsWorld2D;\n let registerSystems: () => () => void;\n if (backend === 'rapier-3d') {\n let module: Rapier3DBackendModule;\n let rapier: unknown;\n try {\n module = (await loadRapier3DBackend()) as Rapier3DBackendModule;\n rapier = await module.loadRapier3D();\n } catch (cause) {\n throw normalizeWasmLoadFailure(backend, cause);\n }\n if (rapier instanceof PhysicsError) throw normalizeWasmLoadFailure(backend, rapier);\n const { createRapier3DPhysicsWorld, registerPhysicsSystems } = module;\n physics = createRapier3DPhysicsWorld(rapier);\n registerSystems = () => registerPhysicsSystems(world);\n } else {\n let module: Rapier2DBackendModule;\n let rapier: unknown;\n try {\n module = (await loadRapier2DBackend()) as Rapier2DBackendModule;\n rapier = await module.loadRapier2D();\n } catch (cause) {\n throw normalizeWasmLoadFailure(backend, cause);\n }\n if (rapier instanceof PhysicsError) throw normalizeWasmLoadFailure(backend, rapier);\n const { createRapier2DPhysicsWorld, registerPhysicsSystems2D } = module;\n physics = createRapier2DPhysicsWorld(rapier);\n registerSystems = () => registerPhysicsSystems2D(world);\n }\n ctx.effect(() => registerPhysicsComponents(world), 'physics/components');\n ctx.effect(() => {\n world.insertResource('PhysicsWorld', physics);\n return () => {\n world.removeResource('PhysicsWorld');\n physics.dispose();\n };\n }, 'physics/resource');\n ctx.effect(() => {\n const unregister = registerSystems();\n return () => unregister();\n }, 'physics/systems');\n ctx.provide('physics', physics);\n },\n };\n}\n","import { defineSystemSet } from '@forgeax/engine-ecs';\n\nexport const PhysicsSet = defineSystemSet({ name: 'physics' });\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeax/engine-physics",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -26,17 +26,29 @@
26
26
  "LICENSE"
27
27
  ],
28
28
  "dependencies": {
29
- "@forgeax/engine-ecs": "0.1.25",
30
- "@forgeax/engine-math": "0.1.25",
31
- "@forgeax/engine-plugin": "0.1.25",
32
- "@forgeax/engine-types": "0.1.25"
29
+ "@forgeax/engine-ecs": "0.1.27",
30
+ "@forgeax/engine-math": "0.1.27",
31
+ "@forgeax/engine-plugin": "0.1.27",
32
+ "@forgeax/engine-types": "0.1.27"
33
33
  },
34
34
  "devDependencies": {
35
- "@forgeax/engine-assets-runtime": "0.1.25",
36
- "@forgeax/engine-physics-rapier2d": "0.1.25",
37
- "@forgeax/engine-physics-rapier3d": "0.1.25",
38
- "@forgeax/engine-runtime": "0.1.25",
39
- "@forgeax/engine-shader": "0.1.25"
35
+ "@forgeax/engine-assets-runtime": "0.1.27",
36
+ "@forgeax/engine-physics-rapier2d": "0.1.27",
37
+ "@forgeax/engine-physics-rapier3d": "0.1.27",
38
+ "@forgeax/engine-runtime": "0.1.27",
39
+ "@forgeax/engine-shader": "0.1.27"
40
+ },
41
+ "peerDependencies": {
42
+ "@forgeax/engine-physics-rapier2d": "0.1.27",
43
+ "@forgeax/engine-physics-rapier3d": "0.1.27"
44
+ },
45
+ "peerDependenciesMeta": {
46
+ "@forgeax/engine-physics-rapier2d": {
47
+ "optional": true
48
+ },
49
+ "@forgeax/engine-physics-rapier3d": {
50
+ "optional": true
51
+ }
40
52
  },
41
53
  "forgeax": {
42
54
  "metrics": {
@@ -3,9 +3,9 @@
3
3
  // physicsPlugin lives in @forgeax/engine-physics (the interface package, C-9)
4
4
  // and accepts an interface->backend dependency inversion: its async apply
5
5
  // dynamic-imports the rapier 2D / 3D backend on demand. The backends are
6
- // declared as devDependencies in this package's package.json (a regular
6
+ // optional peerDependencies in this package's package.json (a regular
7
7
  // dependency would form a physics <-> rapier cycle since the backends depend on
8
- // the interface package); the consuming app declares the real runtime dep.
8
+ // the interface package); the consuming app declares the selected runtime dep.
9
9
  //
10
10
  // charter awareness:
11
11
  // P3 explicit failure: WASM load failure rejects plugin activation and the