@vworlds/vecs-physics 1.0.16

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.
Files changed (94) hide show
  1. package/adapter/disposal.d.ts +8 -0
  2. package/adapter/disposal.js +14 -0
  3. package/adapter/disposal.js.map +1 -0
  4. package/adapter/runtime.d.ts +4 -0
  5. package/adapter/runtime.js +27 -0
  6. package/adapter/runtime.js.map +1 -0
  7. package/adapter/state.d.ts +79 -0
  8. package/adapter/state.js +89 -0
  9. package/adapter/state.js.map +1 -0
  10. package/components/body.d.ts +34 -0
  11. package/components/body.js +53 -0
  12. package/components/body.js.map +1 -0
  13. package/components/body_tuning.d.ts +21 -0
  14. package/components/body_tuning.js +33 -0
  15. package/components/body_tuning.js.map +1 -0
  16. package/components/events.d.ts +32 -0
  17. package/components/events.js +25 -0
  18. package/components/events.js.map +1 -0
  19. package/components/force.d.ts +22 -0
  20. package/components/force.js +35 -0
  21. package/components/force.js.map +1 -0
  22. package/components/geometry.d.ts +55 -0
  23. package/components/geometry.js +74 -0
  24. package/components/geometry.js.map +1 -0
  25. package/components/material.d.ts +18 -0
  26. package/components/material.js +32 -0
  27. package/components/material.js.map +1 -0
  28. package/components/shape_pose.d.ts +9 -0
  29. package/components/shape_pose.js +16 -0
  30. package/components/shape_pose.js.map +1 -0
  31. package/components/target.d.ts +13 -0
  32. package/components/target.js +21 -0
  33. package/components/target.js.map +1 -0
  34. package/index.d.ts +10 -0
  35. package/index.js +23 -0
  36. package/index.js.map +1 -0
  37. package/install.d.ts +30 -0
  38. package/install.js +139 -0
  39. package/install.js.map +1 -0
  40. package/package.json +22 -0
  41. package/systems/debug.d.ts +5 -0
  42. package/systems/debug.js +266 -0
  43. package/systems/debug.js.map +1 -0
  44. package/systems/event_opt_in_sync.d.ts +3 -0
  45. package/systems/event_opt_in_sync.js +41 -0
  46. package/systems/event_opt_in_sync.js.map +1 -0
  47. package/systems/events.d.ts +3 -0
  48. package/systems/events.js +217 -0
  49. package/systems/events.js.map +1 -0
  50. package/systems/forces.d.ts +3 -0
  51. package/systems/forces.js +26 -0
  52. package/systems/forces.js.map +1 -0
  53. package/systems/impulse_accumulate.d.ts +4 -0
  54. package/systems/impulse_accumulate.js +70 -0
  55. package/systems/impulse_accumulate.js.map +1 -0
  56. package/systems/impulse_zero.d.ts +3 -0
  57. package/systems/impulse_zero.js +25 -0
  58. package/systems/impulse_zero.js.map +1 -0
  59. package/systems/index.d.ts +16 -0
  60. package/systems/index.js +81 -0
  61. package/systems/index.js.map +1 -0
  62. package/systems/kinematic.d.ts +3 -0
  63. package/systems/kinematic.js +88 -0
  64. package/systems/kinematic.js.map +1 -0
  65. package/systems/lifecycle.d.ts +3 -0
  66. package/systems/lifecycle.js +99 -0
  67. package/systems/lifecycle.js.map +1 -0
  68. package/systems/mass_recompute.d.ts +3 -0
  69. package/systems/mass_recompute.js +36 -0
  70. package/systems/mass_recompute.js.map +1 -0
  71. package/systems/material_filter_sync.d.ts +3 -0
  72. package/systems/material_filter_sync.js +64 -0
  73. package/systems/material_filter_sync.js.map +1 -0
  74. package/systems/pose_sync_in.d.ts +3 -0
  75. package/systems/pose_sync_in.js +52 -0
  76. package/systems/pose_sync_in.js.map +1 -0
  77. package/systems/pose_sync_out.d.ts +3 -0
  78. package/systems/pose_sync_out.js +50 -0
  79. package/systems/pose_sync_out.js.map +1 -0
  80. package/systems/shape_lifecycle.d.ts +4 -0
  81. package/systems/shape_lifecycle.js +281 -0
  82. package/systems/shape_lifecycle.js.map +1 -0
  83. package/systems/shape_pose_sync.d.ts +3 -0
  84. package/systems/shape_pose_sync.js +18 -0
  85. package/systems/shape_pose_sync.js.map +1 -0
  86. package/systems/step.d.ts +3 -0
  87. package/systems/step.js +13 -0
  88. package/systems/step.js.map +1 -0
  89. package/util/resolve_body.d.ts +6 -0
  90. package/util/resolve_body.js +16 -0
  91. package/util/resolve_body.js.map +1 -0
  92. package/util/warn.d.ts +21 -0
  93. package/util/warn.js +42 -0
  94. package/util/warn.js.map +1 -0
@@ -0,0 +1,18 @@
1
+ /** Physical surface material for a shape (bundles density + surface props). */
2
+ export declare class Material {
3
+ density: number;
4
+ friction: number;
5
+ restitution: number;
6
+ rollingResistance: number;
7
+ tangentSpeed: number;
8
+ userMaterialId: number;
9
+ }
10
+ /** Collision category/mask/group. Missing means category 1, collides with all. */
11
+ export declare class CollisionFilter {
12
+ categoryBits: number;
13
+ maskBits: number;
14
+ groupIndex: number;
15
+ }
16
+ /** Marks a shape as an overlap-only sensor (no collision response). */
17
+ export declare class Sensor {
18
+ }
@@ -0,0 +1,32 @@
1
+ // Material, collision filtering, and the sensor marker. Defaults match
2
+ // `b2DefaultShapeDef()` / `b2DefaultSurfaceMaterial()` / `b2DefaultFilter()`
3
+ // (see `box2d/src/types.c`). Public API — see `docs/components.md`.
4
+ //
5
+ // Divergence from Box2D: `categoryBits`/`maskBits` are 32-bit and stored as
6
+ // plain `number`, not Box2D's `uint64_t`. 32 categories is plenty, and JS
7
+ // bitwise ops (`&`, `|`) are native 32-bit, so filtering — `(catA & maskB) !==
8
+ // 0` — stays fast and bigint-free. `userMaterialId` is an opaque id (not a
9
+ // bitfield), also a `number`. See PLAN → Settled Decisions.
10
+ /** Physical surface material for a shape (bundles density + surface props). */
11
+ export class Material {
12
+ constructor() {
13
+ this.density = 1;
14
+ this.friction = 0.6;
15
+ this.restitution = 0;
16
+ this.rollingResistance = 0;
17
+ this.tangentSpeed = 0;
18
+ this.userMaterialId = 0;
19
+ }
20
+ }
21
+ /** Collision category/mask/group. Missing means category 1, collides with all. */
22
+ export class CollisionFilter {
23
+ constructor() {
24
+ this.categoryBits = 1;
25
+ this.maskBits = 0xffffffff;
26
+ this.groupIndex = 0;
27
+ }
28
+ }
29
+ /** Marks a shape as an overlap-only sensor (no collision response). */
30
+ export class Sensor {
31
+ }
32
+ //# sourceMappingURL=material.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"material.js","sourceRoot":"","sources":["../../../../src/components/material.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,6EAA6E;AAC7E,oEAAoE;AACpE,EAAE;AACF,4EAA4E;AAC5E,0EAA0E;AAC1E,+EAA+E;AAC/E,2EAA2E;AAC3E,4DAA4D;AAE5D,+EAA+E;AAC/E,MAAM,OAAO,QAAQ;IAArB;QACE,YAAO,GAAG,CAAC,CAAC;QACZ,aAAQ,GAAG,GAAG,CAAC;QACf,gBAAW,GAAG,CAAC,CAAC;QAChB,sBAAiB,GAAG,CAAC,CAAC;QACtB,iBAAY,GAAG,CAAC,CAAC;QACjB,mBAAc,GAAG,CAAC,CAAC;IACrB,CAAC;CAAA;AAED,kFAAkF;AAClF,MAAM,OAAO,eAAe;IAA5B;QACE,iBAAY,GAAG,CAAC,CAAC;QACjB,aAAQ,GAAG,UAAU,CAAC;QACtB,eAAU,GAAG,CAAC,CAAC;IACjB,CAAC;CAAA;AAED,uEAAuE;AACvE,MAAM,OAAO,MAAM;CAAG"}
@@ -0,0 +1,9 @@
1
+ /** Local shape offset from the parent body origin. */
2
+ export declare class ShapePosition {
3
+ x: number;
4
+ y: number;
5
+ }
6
+ /** Local shape rotation in radians. */
7
+ export declare class ShapeRotation {
8
+ angle: number;
9
+ }
@@ -0,0 +1,16 @@
1
+ // Shape-local pose components, relative to the immediate parent body. Optional:
2
+ // missing means the identity offset. Public API — see `docs/components.md`.
3
+ /** Local shape offset from the parent body origin. */
4
+ export class ShapePosition {
5
+ constructor() {
6
+ this.x = 0;
7
+ this.y = 0;
8
+ }
9
+ }
10
+ /** Local shape rotation in radians. */
11
+ export class ShapeRotation {
12
+ constructor() {
13
+ this.angle = 0;
14
+ }
15
+ }
16
+ //# sourceMappingURL=shape_pose.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shape_pose.js","sourceRoot":"","sources":["../../../../src/components/shape_pose.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,4EAA4E;AAE5E,sDAAsD;AACtD,MAAM,OAAO,aAAa;IAA1B;QACE,MAAC,GAAG,CAAC,CAAC;QACN,MAAC,GAAG,CAAC,CAAC;IACR,CAAC;CAAA;AAED,uCAAuC;AACvC,MAAM,OAAO,aAAa;IAA1B;QACE,UAAK,GAAG,CAAC,CAAC;IACZ,CAAC;CAAA"}
@@ -0,0 +1,13 @@
1
+ /** Target world-space position for a kinematic body. */
2
+ export declare class KinematicTargetPosition {
3
+ x: number;
4
+ y: number;
5
+ tolerance: number;
6
+ wake: boolean;
7
+ }
8
+ /** Target world-space rotation (radians) for a kinematic body. */
9
+ export declare class KinematicTargetRotation {
10
+ angle: number;
11
+ tolerance: number;
12
+ wake: boolean;
13
+ }
@@ -0,0 +1,21 @@
1
+ // Kinematic target components. Drive a kinematic body toward a target pose; once
2
+ // within `tolerance` physics leaves the component in place and stops driving.
3
+ // Public API — see `docs/components.md`.
4
+ /** Target world-space position for a kinematic body. */
5
+ export class KinematicTargetPosition {
6
+ constructor() {
7
+ this.x = 0;
8
+ this.y = 0;
9
+ this.tolerance = 0.001;
10
+ this.wake = true;
11
+ }
12
+ }
13
+ /** Target world-space rotation (radians) for a kinematic body. */
14
+ export class KinematicTargetRotation {
15
+ constructor() {
16
+ this.angle = 0;
17
+ this.tolerance = 0.001;
18
+ this.wake = true;
19
+ }
20
+ }
21
+ //# sourceMappingURL=target.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"target.js","sourceRoot":"","sources":["../../../../src/components/target.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,8EAA8E;AAC9E,yCAAyC;AAEzC,wDAAwD;AACxD,MAAM,OAAO,uBAAuB;IAApC;QACE,MAAC,GAAG,CAAC,CAAC;QACN,MAAC,GAAG,CAAC,CAAC;QACN,cAAS,GAAG,KAAK,CAAC;QAClB,SAAI,GAAG,IAAI,CAAC;IACd,CAAC;CAAA;AAED,kEAAkE;AAClE,MAAM,OAAO,uBAAuB;IAApC;QACE,UAAK,GAAG,CAAC,CAAC;QACV,cAAS,GAAG,KAAK,CAAC;QAClB,SAAI,GAAG,IAAI,CAAC;IACd,CAAC;CAAA"}
package/index.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ export { installPhysics, type PhysicsOptions } from "./install.js";
2
+ export { preloadPhysics } from "./adapter/runtime.js";
3
+ export { Body, BodyType, Position, Rotation, LinearVelocity, AngularVelocity, } from "./components/body.js";
4
+ export { Damping, GravityScale, MotionLocks, Sleep } from "./components/body_tuning.js";
5
+ export { ShapePosition, ShapeRotation } from "./components/shape_pose.js";
6
+ export { Box, Circle, Capsule, Segment, Polygon, Chain, type Vec2 } from "./components/geometry.js";
7
+ export { Material, CollisionFilter, Sensor } from "./components/material.js";
8
+ export { Force, Torque, Impulse, AngularImpulse } from "./components/force.js";
9
+ export { KinematicTargetPosition, KinematicTargetRotation } from "./components/target.js";
10
+ export { ContactEvents, type ContactEvent, SensorEvents, type SensorEvent, HitEvents, type HitEvent, } from "./components/events.js";
package/index.js ADDED
@@ -0,0 +1,23 @@
1
+ // Public API surface for @vworlds/vecs-physics. Only re-exports — no logic.
2
+ // The stable contract is documented in docs/ and architecture/PLAN.md →
3
+ // Public API Contract.
4
+ // Entry point
5
+ export { installPhysics } from "./install.js";
6
+ export { preloadPhysics } from "./adapter/runtime.js";
7
+ // Body
8
+ export { Body, BodyType, Position, Rotation, LinearVelocity, AngularVelocity, } from "./components/body.js";
9
+ // Body tunables
10
+ export { Damping, GravityScale, MotionLocks, Sleep } from "./components/body_tuning.js";
11
+ // Shape pose
12
+ export { ShapePosition, ShapeRotation } from "./components/shape_pose.js";
13
+ // Geometry
14
+ export { Box, Circle, Capsule, Segment, Polygon, Chain } from "./components/geometry.js";
15
+ // Material / filter / sensor
16
+ export { Material, CollisionFilter, Sensor } from "./components/material.js";
17
+ // Forces & impulses
18
+ export { Force, Torque, Impulse, AngularImpulse } from "./components/force.js";
19
+ // Kinematic targets
20
+ export { KinematicTargetPosition, KinematicTargetRotation } from "./components/target.js";
21
+ // Events
22
+ export { ContactEvents, SensorEvents, HitEvents, } from "./components/events.js";
23
+ //# sourceMappingURL=index.js.map
package/index.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,wEAAwE;AACxE,uBAAuB;AAEvB,cAAc;AACd,OAAO,EAAE,cAAc,EAAuB,MAAM,cAAc,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO;AACP,OAAO,EACL,IAAI,EACJ,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,cAAc,EACd,eAAe,GAChB,MAAM,sBAAsB,CAAC;AAE9B,gBAAgB;AAChB,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,6BAA6B,CAAC;AAExF,aAAa;AACb,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAE1E,WAAW;AACX,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAa,MAAM,0BAA0B,CAAC;AAEpG,6BAA6B;AAC7B,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAE7E,oBAAoB;AACpB,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE/E,oBAAoB;AACpB,OAAO,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAE1F,SAAS;AACT,OAAO,EACL,aAAa,EAEb,YAAY,EAEZ,SAAS,GAEV,MAAM,wBAAwB,CAAC"}
package/install.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { type IPhase, type World } from "@vworlds/vecs";
2
+ /** Options for {@link installPhysics}. Every field is optional; omitted fields
3
+ * fall back to Box2D-aligned defaults. */
4
+ export type PhysicsOptions = {
5
+ gravity?: {
6
+ x: number;
7
+ y: number;
8
+ };
9
+ fixedTimeStep?: number;
10
+ subSteps?: number;
11
+ debug?: boolean;
12
+ /** Override the warning sink used by `PhysicsDebugValidation` when
13
+ * `debug: true`. Accepts any `(message: string) => void` callback.
14
+ * Primarily for tests; defaults to `console.warn`. */
15
+ debugSink?: (message: string) => void;
16
+ preStepPhase?: IPhase | string;
17
+ stepPhase?: IPhase | string;
18
+ postStepPhase?: IPhase | string;
19
+ restitutionThreshold?: number;
20
+ hitEventThreshold?: number;
21
+ contactHertz?: number;
22
+ contactDampingRatio?: number;
23
+ contactSpeed?: number;
24
+ maximumLinearSpeed?: number;
25
+ enableSleep?: boolean;
26
+ enableContinuous?: boolean;
27
+ };
28
+ /** Wire the physics engine into `world`. Must be called before `world.start()`
29
+ * and at most once per world. */
30
+ export declare function installPhysics(world: World, opts?: PhysicsOptions): void;
package/install.js ADDED
@@ -0,0 +1,139 @@
1
+ // `installPhysics` — the single entry point that wires the physics engine into a
2
+ // Vecs world. It registers every public component, declares geometry
3
+ // exclusivity, ensures the three physics phases exist, and attaches the private
4
+ // `PhysicsState`. See PLAN.md → _installPhysics Signature_ and
5
+ // _Component registration order_.
6
+ //
7
+ // VP-4 is wiring only: no systems are installed and the engine is a no-op. The
8
+ // physics systems (and the `PhysicsSystemTag` marker they carry) land in VP-6.
9
+ // Registration order still matters — Vecs assigns component type ids in
10
+ // registration order, so keeping the order stable keeps ids deterministic.
11
+ import { AngularVelocity, Body, LinearVelocity, Position, Rotation } from "./components/body.js";
12
+ import { Damping, GravityScale, MotionLocks, Sleep } from "./components/body_tuning.js";
13
+ import { Box, Capsule, Chain, Circle, Polygon, Segment } from "./components/geometry.js";
14
+ import { AngularImpulse, Force, Impulse, Torque } from "./components/force.js";
15
+ import { CollisionFilter, Material, Sensor } from "./components/material.js";
16
+ import { ContactEvents, HitEvents, SensorEvents } from "./components/events.js";
17
+ import { KinematicTargetPosition, KinematicTargetRotation } from "./components/target.js";
18
+ import { ShapePosition, ShapeRotation } from "./components/shape_pose.js";
19
+ import { getBox2D } from "./adapter/runtime.js";
20
+ import { withAllocated } from "./adapter/disposal.js";
21
+ import { attachPhysics, hasPhysics, PhysicsState, registerInternalComponents, } from "./adapter/state.js";
22
+ import { registerImpulseHooks } from "./systems/impulse_accumulate.js";
23
+ import { installPhysicsSystems } from "./systems/index.js";
24
+ const DEFAULT_PRE_STEP_PHASE = "physics-pre";
25
+ const DEFAULT_STEP_PHASE = "physics-step";
26
+ const DEFAULT_POST_STEP_PHASE = "physics-post";
27
+ /** Wire the physics engine into `world`. Must be called before `world.start()`
28
+ * and at most once per world. */
29
+ export function installPhysics(world, opts = {}) {
30
+ if (isWorldStarted(world)) {
31
+ throw new Error("installPhysics must be called before world.start()");
32
+ }
33
+ if (hasPhysics(world)) {
34
+ throw new Error("installPhysics has already been called on this world");
35
+ }
36
+ // 1. Register every public component, in a stable order (see file header).
37
+ // `ChildOf` is built into Vecs — referenced, never registered.
38
+ world.component(Position);
39
+ world.component(Rotation);
40
+ world.component(LinearVelocity);
41
+ world.component(AngularVelocity);
42
+ world.component(Body);
43
+ world.component(Damping);
44
+ world.component(GravityScale);
45
+ world.component(MotionLocks);
46
+ world.component(Sleep);
47
+ world.component(ShapePosition);
48
+ world.component(ShapeRotation);
49
+ world.component(Box);
50
+ world.component(Circle);
51
+ world.component(Capsule);
52
+ world.component(Segment);
53
+ world.component(Polygon);
54
+ world.component(Chain);
55
+ world.component(Material);
56
+ world.component(CollisionFilter);
57
+ world.component(Sensor);
58
+ world.component(Force);
59
+ world.component(Torque);
60
+ world.component(Impulse);
61
+ world.component(AngularImpulse);
62
+ world.component(KinematicTargetPosition);
63
+ world.component(KinematicTargetRotation);
64
+ world.component(ContactEvents);
65
+ world.component(SensorEvents);
66
+ world.component(HitEvents);
67
+ // 2. Internal, non-exported components (`PhysicsRoot`, `PhysicsSystemTag`),
68
+ // registered in their documented order for deterministic type ids.
69
+ registerInternalComponents(world);
70
+ // 3. Geometry components are mutually exclusive: an entity holds at most one.
71
+ world.setExclusiveComponents(Box, Circle, Capsule, Segment, Polygon, Chain);
72
+ // 4. Ensure the three physics phases exist (created if missing).
73
+ const preStep = resolvePhase(world, opts.preStepPhase, DEFAULT_PRE_STEP_PHASE);
74
+ const step = resolvePhase(world, opts.stepPhase, DEFAULT_STEP_PHASE);
75
+ const postStep = resolvePhase(world, opts.postStepPhase, DEFAULT_POST_STEP_PHASE);
76
+ // 5. Allocate WASM state, create the Box2D world, install the systems (bound
77
+ // to it), then attach it.
78
+ const box2d = getBox2D();
79
+ const options = resolveOptions(opts);
80
+ const state = new PhysicsState(box2d, options, opts.debugSink);
81
+ state.worldId = createWorldId(state, options);
82
+ registerImpulseHooks(world, state);
83
+ installPhysicsSystems(world, state, { preStep, step, postStep }, {
84
+ debug: opts.debug ?? false,
85
+ });
86
+ attachPhysics(world, state);
87
+ }
88
+ function createWorldId(state, options) {
89
+ const box2d = state.box2d;
90
+ return withAllocated(box2d.b2DefaultWorldDef, (def) => {
91
+ def.gravity.x = options.gravityX;
92
+ def.gravity.y = options.gravityY;
93
+ def.restitutionThreshold = options.restitutionThreshold;
94
+ def.hitEventThreshold = options.hitEventThreshold;
95
+ def.contactHertz = options.contactHertz;
96
+ def.contactDampingRatio = options.contactDampingRatio;
97
+ def.maximumLinearSpeed = options.maximumLinearSpeed;
98
+ def.enableSleep = options.enableSleep;
99
+ def.enableContinuous = options.enableContinuous;
100
+ const worldId = box2d.b2CreateWorld(def);
101
+ box2d.b2World_SetContactTuning(worldId, options.contactHertz, options.contactDampingRatio, options.contactSpeed);
102
+ return worldId;
103
+ });
104
+ }
105
+ /** Resolve a phase placement option to a concrete phase. An `IPhase` is used
106
+ * as-is; a name (or the default name) is looked up and created if missing. */
107
+ function resolvePhase(world, given, defaultName) {
108
+ if (given !== undefined && typeof given !== "string") {
109
+ return given;
110
+ }
111
+ const name = given ?? defaultName;
112
+ return world._pipeline.get(name) ?? world.addPhase(name);
113
+ }
114
+ /** Apply Box2D-aligned defaults to `opts`. */
115
+ function resolveOptions(opts) {
116
+ return {
117
+ gravityX: opts.gravity?.x ?? 0,
118
+ gravityY: opts.gravity?.y ?? -10,
119
+ fixedTimeStep: opts.fixedTimeStep ?? 1 / 60,
120
+ subSteps: opts.subSteps ?? 4,
121
+ debug: opts.debug ?? false,
122
+ restitutionThreshold: opts.restitutionThreshold ?? 1.0,
123
+ // box2d b2DefaultWorldDef: def.hitEventThreshold = 1.0f * lengthUnits (types.c:16).
124
+ hitEventThreshold: opts.hitEventThreshold ?? 1.0,
125
+ contactHertz: opts.contactHertz ?? 30,
126
+ contactDampingRatio: opts.contactDampingRatio ?? 10,
127
+ // box2d b2DefaultWorldDef: def.contactSpeed = 3.0f * lengthUnits (types.c:18).
128
+ contactSpeed: opts.contactSpeed ?? 3.0,
129
+ maximumLinearSpeed: opts.maximumLinearSpeed ?? 400,
130
+ enableSleep: opts.enableSleep ?? true,
131
+ enableContinuous: opts.enableContinuous ?? true,
132
+ };
133
+ }
134
+ /** Whether `world.start()` has already run. Reads the world's internal started
135
+ * flag; there is no public accessor. */
136
+ function isWorldStarted(world) {
137
+ return world._started === true;
138
+ }
139
+ //# sourceMappingURL=install.js.map
package/install.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install.js","sourceRoot":"","sources":["../../../src/install.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@vworlds/vecs-physics",
3
+ "version": "1.0.16",
4
+ "type": "module",
5
+ "main": "./index.js",
6
+ "types": "./index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./index.d.ts",
10
+ "import": "./index.js"
11
+ }
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/vworlds/vecs.git"
16
+ },
17
+ "dependencies": {
18
+ "@vworlds/vecs": "1.0.16",
19
+ "box2d3-wasm": "^5.2.0"
20
+ },
21
+ "license": "MIT"
22
+ }
@@ -0,0 +1,5 @@
1
+ import { type IPhase, type System, type World } from "@vworlds/vecs";
2
+ import { type PhysicsState } from "../adapter/state.js";
3
+ /** Create `PhysicsDebugValidation` on `world` in `phase`.
4
+ * Returns the system so the caller can tag its entity. */
5
+ export declare function installDebugValidation(world: World, state: PhysicsState, phase: IPhase): System;
@@ -0,0 +1,266 @@
1
+ // `PhysicsDebugValidation` — per-step structural validation with warn-once
2
+ // semantics. Registered ONLY when `installPhysics` is called with
3
+ // `{ debug: true }`. In normal mode this system is never installed, and no
4
+ // validation overhead exists.
5
+ //
6
+ // Phase: `physics-pre`, AFTER all other physics-pre systems (last position).
7
+ // This ensures the validator sees the world after lifecycle/sync have run, so
8
+ // ephemeral intermediate states during entity construction are not flagged.
9
+ //
10
+ // Seven issue codes (docs/debug-validation.md):
11
+ // 1. shape-without-parent-body — geometry entity whose immediate parent
12
+ // lacks Body (or has no parent at all).
13
+ // 2. dynamic-body-without-density — dynamic Body with no valid shape child:
14
+ // either no children, or every child shape
15
+ // has Material.density === 0. Sensor shapes
16
+ // with positive density DO contribute mass
17
+ // (Box2D parity: skip only density === 0).
18
+ // 3. chain-too-few-points — Chain.points.length < 4.
19
+ // 4. polygon-invalid — Polygon.vertices fail validateHull.
20
+ // 5. shape-position-on-body — entity has Body AND ShapePosition or
21
+ // ShapeRotation.
22
+ // 6. position-on-shape-entity — entity has a geometry component AND
23
+ // Position.
24
+ // 7. force-without-target — force/impulse entity has no Body and no
25
+ // immediate Body parent.
26
+ //
27
+ // Re-arm semantics: when a (eid, code) pair that was previously warned becomes
28
+ // valid for that code (issue cleared), we call rearm() so a future relapse can
29
+ // warn again. The WarnDedupe key is `(eid, code)`, so each code is independent.
30
+ //
31
+ // Design points:
32
+ // - Uses world.filter() scans (non-reactive) — same pattern as ForceGather.
33
+ // An entity enters the relevant filter when it carries the matching
34
+ // component(s), and the validator re-checks it every step.
35
+ // - WarnDedupe instance is owned by PhysicsState (survives across systems).
36
+ // - The injectable WarnSink in WarnDedupe is the test hook; pass it via
37
+ // PhysicsOptions.debugSink.
38
+ // - Does NOT modify any engine state — read-only pass.
39
+ //
40
+ // Upstream docs: docs/debug-validation.md.
41
+ // Re-uses: validateHull / computeHull from engine/geometry.ts.
42
+ import { ChildOf, } from "@vworlds/vecs";
43
+ import { Body, BodyType, Position } from "../components/body.js";
44
+ import { AngularImpulse, Force, Impulse, Torque } from "../components/force.js";
45
+ import { Box, Capsule, Chain, Circle, Polygon, Segment } from "../components/geometry.js";
46
+ import { Material } from "../components/material.js";
47
+ import { ShapePosition, ShapeRotation } from "../components/shape_pose.js";
48
+ // ─── Issue codes ──────────────────────────────────────────────────────────────
49
+ const CODE_SHAPE_WITHOUT_PARENT_BODY = "shape-without-parent-body";
50
+ const CODE_DYNAMIC_BODY_WITHOUT_DENSITY = "dynamic-body-without-density";
51
+ const CODE_CHAIN_TOO_FEW_POINTS = "chain-too-few-points";
52
+ const CODE_POLYGON_INVALID = "polygon-invalid";
53
+ const CODE_SHAPE_POSITION_ON_BODY = "shape-position-on-body";
54
+ const CODE_POSITION_ON_SHAPE_ENTITY = "position-on-shape-entity";
55
+ const CODE_FORCE_WITHOUT_TARGET = "force-without-target";
56
+ // ─── System factory ───────────────────────────────────────────────────────────
57
+ /** Create `PhysicsDebugValidation` on `world` in `phase`.
58
+ * Returns the system so the caller can tag its entity. */
59
+ export function installDebugValidation(world, state, phase) {
60
+ // ── Filters (non-reactive; walked every step) ─────────────────────────────
61
+ // Geometry entities: any entity with at least one geometry component.
62
+ const geomFilter = world.filter({ any: [Box, Circle, Capsule, Segment, Polygon, Chain] });
63
+ // Body entities: any entity with Body.
64
+ const bodyFilter = world.filter(Body);
65
+ // Force-class entities: any entity with a force, torque, impulse, or angular impulse.
66
+ const forceFilter = world.filter({ any: [Force, Torque, Impulse, AngularImpulse] });
67
+ // Body entities that also carry ShapePosition or ShapeRotation.
68
+ const bodyPoseFilter = world.filter({
69
+ all: [Body, { any: [ShapePosition, ShapeRotation] }],
70
+ });
71
+ // Track body-pose entities seen in the previous step so we can rearm those
72
+ // that leave the filter (ShapePosition/ShapeRotation removed). Re-entry into
73
+ // the filter will then produce a fresh warning, completing the re-arm cycle.
74
+ const prevBodyPoseEids = new Set();
75
+ return world
76
+ .system("PhysicsDebugValidation")
77
+ .phase(phase)
78
+ .run(() => {
79
+ _validateGeomEntities(state, geomFilter, state.warn);
80
+ _validateBodyEntities(geomFilter, bodyFilter, state.warn);
81
+ _validateBodyShapePositionEntities(bodyPoseFilter, prevBodyPoseEids, state.warn);
82
+ _validateForceEntities(forceFilter, state.warn);
83
+ });
84
+ }
85
+ // ─── Geometry-entity validation ───────────────────────────────────────────────
86
+ /** Walk every entity with a geometry component and check:
87
+ * - shape-without-parent-body (code 1)
88
+ * - chain-too-few-points (code 3)
89
+ * - polygon-invalid (code 4)
90
+ * - position-on-shape-entity (code 6) */
91
+ function _validateGeomEntities(state, geomFilter, dedupe) {
92
+ geomFilter.forEach((e) => {
93
+ const eid = e.eid;
94
+ // ── shape-without-parent-body (code 1) ───────────────────────────────────
95
+ const parent = e.parent(ChildOf);
96
+ const parentHasBody = parent !== undefined && parent.get(Body) !== undefined;
97
+ if (!parentHasBody) {
98
+ dedupe.warnOnce(eid, CODE_SHAPE_WITHOUT_PARENT_BODY, `Entity${eid} has geometry but its immediate parent does not have Body.`);
99
+ }
100
+ else {
101
+ dedupe.rearm(eid, CODE_SHAPE_WITHOUT_PARENT_BODY);
102
+ }
103
+ // ── chain-too-few-points (code 3) ────────────────────────────────────────
104
+ const chain = e.get(Chain);
105
+ if (chain !== undefined) {
106
+ if (chain.points.length < 4) {
107
+ dedupe.warnOnce(eid, CODE_CHAIN_TOO_FEW_POINTS, `Entity${eid} has Chain with ${chain.points.length} point(s); at least 4 are required.`);
108
+ }
109
+ else {
110
+ dedupe.rearm(eid, CODE_CHAIN_TOO_FEW_POINTS);
111
+ }
112
+ }
113
+ // ── polygon-invalid (code 4) ─────────────────────────────────────────────
114
+ const polygon = e.get(Polygon);
115
+ if (polygon !== undefined) {
116
+ const valid = isValidPolygon(state, polygon.vertices);
117
+ if (!valid) {
118
+ dedupe.warnOnce(eid, CODE_POLYGON_INVALID, `Entity${eid} has Polygon with invalid or non-convex vertices.`);
119
+ }
120
+ else {
121
+ dedupe.rearm(eid, CODE_POLYGON_INVALID);
122
+ }
123
+ }
124
+ // ── position-on-shape-entity (code 6) ────────────────────────────────────
125
+ const hasPosition = e.get(Position) !== undefined;
126
+ if (hasPosition) {
127
+ dedupe.warnOnce(eid, CODE_POSITION_ON_SHAPE_ENTITY, `Entity${eid} has a geometry component and Position; use ShapePosition for shape-local offsets.`);
128
+ }
129
+ else {
130
+ dedupe.rearm(eid, CODE_POSITION_ON_SHAPE_ENTITY);
131
+ }
132
+ });
133
+ }
134
+ // ─── Body-entity validation ───────────────────────────────────────────────────
135
+ /** Walk every entity with a Body component and check:
136
+ * - dynamic-body-without-density (code 2)
137
+ *
138
+ * A dynamic body must have at least one child shape with density > 0. Sensor
139
+ * shapes count — Box2D parity skips only density === 0, not isSensor shapes.
140
+ *
141
+ * To find child shapes without engine-state access, we build a map of
142
+ * body-eid → geometry child entities by walking the geometry filter and checking
143
+ * which have a direct Body parent. This is a read-only ECS walk with no
144
+ * dependency on PhysicsState internals. */
145
+ function _validateBodyEntities(geomFilter, bodyFilter, dedupe) {
146
+ // Build a map: body-eid → list of geometry child entities (per this step).
147
+ const geomChildrenByBody = new Map();
148
+ geomFilter.forEach((e) => {
149
+ const parent = e.parent(ChildOf);
150
+ if (parent === undefined || parent.get(Body) === undefined) {
151
+ return;
152
+ }
153
+ const bodyEid = parent.eid;
154
+ let list = geomChildrenByBody.get(bodyEid);
155
+ if (list === undefined) {
156
+ list = [];
157
+ geomChildrenByBody.set(bodyEid, list);
158
+ }
159
+ list.push(e);
160
+ });
161
+ // Walk every body entity and check dynamic-body-without-density.
162
+ bodyFilter.forEach((e) => {
163
+ const eid = e.eid;
164
+ const body = e.get(Body);
165
+ if (body.type !== BodyType.Dynamic) {
166
+ return;
167
+ }
168
+ const children = geomChildrenByBody.get(eid);
169
+ // A dynamic body with no geometry children always warns.
170
+ if (children === undefined || children.length === 0) {
171
+ dedupe.warnOnce(eid, CODE_DYNAMIC_BODY_WITHOUT_DENSITY, `Entity${eid} is a dynamic body with no shape children; it will have zero mass.`);
172
+ return;
173
+ }
174
+ // Check if ANY child shape has density > 0.
175
+ // Sensor shapes with positive density DO contribute mass (Box2D parity:
176
+ // b2UpdateBodyMassData skips only density === 0, not isSensor shapes).
177
+ // Missing Material means default density = 1 (fine).
178
+ let hasValidDensity = false;
179
+ for (const child of children) {
180
+ const mat = child.get(Material);
181
+ const density = mat !== undefined ? mat.density : 1; // default density is 1
182
+ if (density > 0) {
183
+ hasValidDensity = true;
184
+ break;
185
+ }
186
+ }
187
+ if (!hasValidDensity) {
188
+ dedupe.warnOnce(eid, CODE_DYNAMIC_BODY_WITHOUT_DENSITY, `Entity${eid} is a dynamic body whose shapes all have density 0; it will have zero mass.`);
189
+ }
190
+ });
191
+ }
192
+ // ─── Body-with-shape-pose validation ─────────────────────────────────────────
193
+ /** Walk entities that have Body AND ShapePosition or ShapeRotation (code 5).
194
+ *
195
+ * Re-arm mechanism: entities that leave this filter (ShapePosition/ShapeRotation
196
+ * removed) are detected via `prevBodyPoseEids`. They are rearmed so re-entry
197
+ * into the filter produces a fresh warning if the issue recurs. */
198
+ function _validateBodyShapePositionEntities(bodyPoseFilter, prevBodyPoseEids, dedupe) {
199
+ const currentEids = new Set();
200
+ bodyPoseFilter.forEach((e) => {
201
+ const eid = e.eid;
202
+ currentEids.add(eid);
203
+ const hasShapePos = e.get(ShapePosition) !== undefined;
204
+ const hasShapeRot = e.get(ShapeRotation) !== undefined;
205
+ const component = hasShapePos && hasShapeRot
206
+ ? "ShapePosition and ShapeRotation"
207
+ : hasShapePos
208
+ ? "ShapePosition"
209
+ : "ShapeRotation";
210
+ dedupe.warnOnce(eid, CODE_SHAPE_POSITION_ON_BODY, `Entity${eid} has Body and ${component}; move ${component} to a child shape entity.`);
211
+ });
212
+ // Rearm any entities that left the filter this step (ShapePosition/ShapeRotation
213
+ // was removed). If they re-enter in a future step, a fresh warning can fire.
214
+ for (const eid of prevBodyPoseEids) {
215
+ if (!currentEids.has(eid)) {
216
+ dedupe.rearm(eid, CODE_SHAPE_POSITION_ON_BODY);
217
+ }
218
+ }
219
+ // Update the previous-seen set for next step.
220
+ prevBodyPoseEids.clear();
221
+ for (const eid of currentEids) {
222
+ prevBodyPoseEids.add(eid);
223
+ }
224
+ }
225
+ // ─── Polygon hull validation ──────────────────────────────────────────────────
226
+ /**
227
+ * Returns true if `vertices` forms a valid convex polygon according to Box2D.
228
+ * Delegates to `b2ComputeHull` (WASM); a hull with count >= 3 is valid. Allocates
229
+ * and frees temporary b2Vec2 objects via the WASM heap.
230
+ */
231
+ function isValidPolygon(state, vertices) {
232
+ if (vertices.length < 3) {
233
+ return false;
234
+ }
235
+ const box2d = state.box2d;
236
+ const points = vertices.map((v) => new box2d.b2Vec2(v.x, v.y));
237
+ try {
238
+ const hull = box2d.b2ComputeHull(points);
239
+ try {
240
+ return hull.count >= 3;
241
+ }
242
+ finally {
243
+ hull.delete();
244
+ }
245
+ }
246
+ finally {
247
+ for (const p of points) {
248
+ p.delete();
249
+ }
250
+ }
251
+ }
252
+ // ─── Force-target validation ─────────────────────────────────────────────────
253
+ function _validateForceEntities(forceFilter, dedupe) {
254
+ forceFilter.forEach((e) => {
255
+ const eid = e.eid;
256
+ const parent = e.parent(ChildOf);
257
+ const hasTarget = e.get(Body) !== undefined || (parent !== undefined && parent.get(Body) !== undefined);
258
+ if (!hasTarget) {
259
+ dedupe.warnOnce(eid, CODE_FORCE_WITHOUT_TARGET, `Entity${eid} has Force/Torque/Impulse/AngularImpulse but no Body or immediate Body parent.`);
260
+ }
261
+ else {
262
+ dedupe.rearm(eid, CODE_FORCE_WITHOUT_TARGET);
263
+ }
264
+ });
265
+ }
266
+ //# sourceMappingURL=debug.js.map