@codexo/exojs-physics 0.15.3 → 0.16.0
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 +51 -23
- package/dist/esm/Aabb.d.ts +6 -10
- package/dist/esm/Aabb.d.ts.map +1 -0
- package/dist/esm/Aabb.js +9 -5
- package/dist/esm/Aabb.js.map +1 -1
- package/dist/esm/Collider.d.ts +62 -10
- package/dist/esm/Collider.d.ts.map +1 -0
- package/dist/esm/Collider.js +263 -157
- package/dist/esm/Collider.js.map +1 -1
- package/dist/esm/ContactGraph.d.ts +51 -5
- package/dist/esm/ContactGraph.d.ts.map +1 -0
- package/dist/esm/ContactGraph.js +282 -223
- package/dist/esm/ContactGraph.js.map +1 -1
- package/dist/esm/ContactModifier.d.ts +85 -0
- package/dist/esm/ContactModifier.d.ts.map +1 -0
- package/dist/esm/ContactModifier.js +49 -0
- package/dist/esm/ContactModifier.js.map +1 -0
- package/dist/esm/PhysicsBody.d.ts +56 -22
- package/dist/esm/PhysicsBody.d.ts.map +1 -0
- package/dist/esm/PhysicsBody.js +461 -443
- package/dist/esm/PhysicsBody.js.map +1 -1
- package/dist/esm/PhysicsWorld.d.ts +276 -43
- package/dist/esm/PhysicsWorld.d.ts.map +1 -0
- package/dist/esm/PhysicsWorld.js +890 -626
- package/dist/esm/PhysicsWorld.js.map +1 -1
- package/dist/esm/TimeStepper.d.ts +3 -2
- package/dist/esm/TimeStepper.d.ts.map +1 -0
- package/dist/esm/TimeStepper.js +56 -67
- package/dist/esm/TimeStepper.js.map +1 -1
- package/dist/esm/backend/NativePhysicsBackend.d.ts +8 -4
- package/dist/esm/backend/NativePhysicsBackend.d.ts.map +1 -0
- package/dist/esm/backend/NativePhysicsBackend.js +48 -41
- package/dist/esm/backend/NativePhysicsBackend.js.map +1 -1
- package/dist/esm/backend/PhysicsBackend.d.ts +9 -2
- package/dist/esm/backend/PhysicsBackend.d.ts.map +1 -0
- package/dist/esm/binding/BindingRegistry.d.ts +25 -4
- package/dist/esm/binding/BindingRegistry.d.ts.map +1 -0
- package/dist/esm/binding/BindingRegistry.js +70 -36
- package/dist/esm/binding/BindingRegistry.js.map +1 -1
- package/dist/esm/binding/PhysicsBinding.d.ts +22 -5
- package/dist/esm/binding/PhysicsBinding.d.ts.map +1 -0
- package/dist/esm/binding/PhysicsBinding.js +46 -21
- package/dist/esm/binding/PhysicsBinding.js.map +1 -1
- package/dist/esm/broadphase/AabbTreeBroadPhase.d.ts +68 -0
- package/dist/esm/broadphase/AabbTreeBroadPhase.d.ts.map +1 -0
- package/dist/esm/broadphase/AabbTreeBroadPhase.js +149 -0
- package/dist/esm/broadphase/AabbTreeBroadPhase.js.map +1 -0
- package/dist/esm/broadphase/BroadPhase.d.ts +7 -4
- package/dist/esm/broadphase/BroadPhase.d.ts.map +1 -0
- package/dist/esm/broadphase/index.d.ts +2 -1
- package/dist/esm/broadphase/index.d.ts.map +1 -0
- package/dist/esm/collision/CollisionProxy.d.ts +3 -2
- package/dist/esm/collision/CollisionProxy.d.ts.map +1 -0
- package/dist/esm/collision/Manifold.d.ts +5 -4
- package/dist/esm/collision/Manifold.d.ts.map +1 -0
- package/dist/esm/collision/Manifold.js +34 -25
- package/dist/esm/collision/Manifold.js.map +1 -1
- package/dist/esm/collision/chainAdjacency.d.ts +29 -0
- package/dist/esm/collision/chainAdjacency.d.ts.map +1 -0
- package/dist/esm/collision/chainAdjacency.js +44 -0
- package/dist/esm/collision/chainAdjacency.js.map +1 -0
- package/dist/esm/collision/dispatch.d.ts +35 -0
- package/dist/esm/collision/dispatch.d.ts.map +1 -0
- package/dist/esm/collision/dispatch.js +58 -0
- package/dist/esm/collision/dispatch.js.map +1 -0
- package/dist/esm/collision/index.d.ts +2 -0
- package/dist/esm/collision/index.d.ts.map +1 -0
- package/dist/esm/collision/narrowphase.d.ts +5 -2
- package/dist/esm/collision/narrowphase.d.ts.map +1 -0
- package/dist/esm/collision/narrowphase.js +524 -399
- package/dist/esm/collision/narrowphase.js.map +1 -1
- package/dist/esm/collision/segments.d.ts +19 -0
- package/dist/esm/collision/segments.d.ts.map +1 -0
- package/dist/esm/collision/segments.js +36 -0
- package/dist/esm/collision/segments.js.map +1 -0
- package/dist/esm/collision/sweep.d.ts +35 -0
- package/dist/esm/collision/sweep.d.ts.map +1 -0
- package/dist/esm/collision/sweep.js +437 -0
- package/dist/esm/collision/sweep.js.map +1 -0
- package/dist/esm/debug/PhysicsDebugDraw.d.ts +23 -4
- package/dist/esm/debug/PhysicsDebugDraw.d.ts.map +1 -0
- package/dist/esm/debug/PhysicsDebugDraw.js +230 -188
- package/dist/esm/debug/PhysicsDebugDraw.js.map +1 -1
- package/dist/esm/debug/index.d.ts +1 -0
- package/dist/esm/debug/index.d.ts.map +1 -0
- package/dist/esm/debug/index.js +3 -2
- package/dist/esm/events.d.ts +1 -0
- package/dist/esm/events.d.ts.map +1 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +24 -19
- package/dist/esm/joints/DistanceJoint.d.ts +4 -3
- package/dist/esm/joints/DistanceJoint.d.ts.map +1 -0
- package/dist/esm/joints/DistanceJoint.js +160 -172
- package/dist/esm/joints/DistanceJoint.js.map +1 -1
- package/dist/esm/joints/Joint.d.ts +5 -4
- package/dist/esm/joints/Joint.d.ts.map +1 -0
- package/dist/esm/joints/Joint.js +23 -21
- package/dist/esm/joints/Joint.js.map +1 -1
- package/dist/esm/joints/MouseJoint.d.ts +8 -7
- package/dist/esm/joints/MouseJoint.d.ts.map +1 -0
- package/dist/esm/joints/MouseJoint.js +132 -133
- package/dist/esm/joints/MouseJoint.js.map +1 -1
- package/dist/esm/joints/PrismaticJoint.d.ts +5 -4
- package/dist/esm/joints/PrismaticJoint.d.ts.map +1 -0
- package/dist/esm/joints/PrismaticJoint.js +211 -240
- package/dist/esm/joints/PrismaticJoint.js.map +1 -1
- package/dist/esm/joints/RevoluteJoint.d.ts +4 -3
- package/dist/esm/joints/RevoluteJoint.d.ts.map +1 -0
- package/dist/esm/joints/RevoluteJoint.js +191 -213
- package/dist/esm/joints/RevoluteJoint.js.map +1 -1
- package/dist/esm/joints/WeldJoint.d.ts +3 -2
- package/dist/esm/joints/WeldJoint.d.ts.map +1 -0
- package/dist/esm/joints/WeldJoint.js +156 -152
- package/dist/esm/joints/WeldJoint.js.map +1 -1
- package/dist/esm/joints/WheelJoint.d.ts +4 -3
- package/dist/esm/joints/WheelJoint.d.ts.map +1 -0
- package/dist/esm/joints/WheelJoint.js +228 -255
- package/dist/esm/joints/WheelJoint.js.map +1 -1
- package/dist/esm/math.d.ts +6 -9
- package/dist/esm/math.d.ts.map +1 -0
- package/dist/esm/math.js +37 -39
- package/dist/esm/math.js.map +1 -1
- package/dist/esm/physicsBuildInfo.d.ts +1 -0
- package/dist/esm/physicsBuildInfo.d.ts.map +1 -0
- package/dist/esm/physicsBuildInfo.js +6 -4
- package/dist/esm/physicsBuildInfo.js.map +1 -1
- package/dist/esm/public.d.ts +8 -3
- package/dist/esm/public.d.ts.map +1 -0
- package/dist/esm/query/QueryEngine.d.ts +56 -12
- package/dist/esm/query/QueryEngine.d.ts.map +1 -0
- package/dist/esm/query/QueryEngine.js +494 -237
- package/dist/esm/query/QueryEngine.js.map +1 -1
- package/dist/esm/query/SpatialIndex.d.ts +34 -0
- package/dist/esm/query/SpatialIndex.d.ts.map +1 -0
- package/dist/esm/shapes/AnyShape.d.ts +9 -4
- package/dist/esm/shapes/AnyShape.d.ts.map +1 -0
- package/dist/esm/shapes/BoxShape.d.ts +2 -1
- package/dist/esm/shapes/BoxShape.d.ts.map +1 -0
- package/dist/esm/shapes/BoxShape.js +37 -25
- package/dist/esm/shapes/BoxShape.js.map +1 -1
- package/dist/esm/shapes/CapsuleShape.d.ts +30 -0
- package/dist/esm/shapes/CapsuleShape.d.ts.map +1 -0
- package/dist/esm/shapes/CapsuleShape.js +88 -0
- package/dist/esm/shapes/CapsuleShape.js.map +1 -0
- package/dist/esm/shapes/ChainEdgeShape.d.ts +30 -0
- package/dist/esm/shapes/ChainEdgeShape.d.ts.map +1 -0
- package/dist/esm/shapes/ChainEdgeShape.js +66 -0
- package/dist/esm/shapes/ChainEdgeShape.js.map +1 -0
- package/dist/esm/shapes/ChainShape.d.ts +52 -0
- package/dist/esm/shapes/ChainShape.d.ts.map +1 -0
- package/dist/esm/shapes/ChainShape.js +85 -0
- package/dist/esm/shapes/ChainShape.js.map +1 -0
- package/dist/esm/shapes/CircleShape.d.ts +3 -4
- package/dist/esm/shapes/CircleShape.d.ts.map +1 -0
- package/dist/esm/shapes/CircleShape.js +27 -26
- package/dist/esm/shapes/CircleShape.js.map +1 -1
- package/dist/esm/shapes/PolygonShape.d.ts +7 -8
- package/dist/esm/shapes/PolygonShape.d.ts.map +1 -0
- package/dist/esm/shapes/PolygonShape.js +131 -162
- package/dist/esm/shapes/PolygonShape.js.map +1 -1
- package/dist/esm/shapes/SegmentShape.d.ts +31 -0
- package/dist/esm/shapes/SegmentShape.d.ts.map +1 -0
- package/dist/esm/shapes/SegmentShape.js +62 -0
- package/dist/esm/shapes/SegmentShape.js.map +1 -0
- package/dist/esm/shapes/Shape.d.ts +28 -17
- package/dist/esm/shapes/Shape.d.ts.map +1 -0
- package/dist/esm/shapes/Shape.js +12 -13
- package/dist/esm/shapes/Shape.js.map +1 -1
- package/dist/esm/shapes/convexParts.d.ts +21 -0
- package/dist/esm/shapes/convexParts.d.ts.map +1 -0
- package/dist/esm/shapes/convexParts.js +26 -0
- package/dist/esm/shapes/convexParts.js.map +1 -0
- package/dist/esm/shapes/decompose.d.ts +24 -0
- package/dist/esm/shapes/decompose.d.ts.map +1 -0
- package/dist/esm/shapes/decompose.js +272 -0
- package/dist/esm/shapes/decompose.js.map +1 -0
- package/dist/esm/shapes/index.d.ts +4 -0
- package/dist/esm/shapes/index.d.ts.map +1 -0
- package/dist/esm/solver/ContactSolver.d.ts +12 -5
- package/dist/esm/solver/ContactSolver.d.ts.map +1 -0
- package/dist/esm/solver/ContactSolver.js +425 -471
- package/dist/esm/solver/ContactSolver.js.map +1 -1
- package/dist/esm/solver/tolerances.d.ts +34 -0
- package/dist/esm/solver/tolerances.d.ts.map +1 -0
- package/dist/esm/solver/tolerances.js +38 -0
- package/dist/esm/solver/tolerances.js.map +1 -0
- package/dist/esm/sort.d.ts +5 -4
- package/dist/esm/sort.d.ts.map +1 -0
- package/dist/esm/sort.js +41 -51
- package/dist/esm/sort.js.map +1 -1
- package/dist/esm/types.d.ts +4 -8
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +15 -17
- package/dist/esm/types.js.map +1 -1
- package/package.json +6 -6
- package/dist/esm/broadphase/SweepAndPrune.d.ts +0 -15
- package/dist/esm/broadphase/SweepAndPrune.js +0 -77
- package/dist/esm/broadphase/SweepAndPrune.js.map +0 -1
- package/dist/esm/debug/index.js.map +0 -1
- package/dist/esm/index.js.map +0 -1
|
@@ -1,20 +1,41 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type SceneNode } from '@codexo/exojs';
|
|
2
2
|
import type { PhysicsBody } from '../PhysicsBody';
|
|
3
3
|
import { PhysicsBinding } from './PhysicsBinding';
|
|
4
4
|
/**
|
|
5
5
|
* Owns the body ↔ node links and writes bound node transforms after each step.
|
|
6
|
-
* Backend-agnostic (frontend-level)
|
|
6
|
+
* Backend-agnostic (frontend-level) - it reads only the public body transform.
|
|
7
7
|
*/
|
|
8
8
|
export declare class BindingRegistry {
|
|
9
9
|
private readonly _bindings;
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Link `body` to `node`. Rejects destroyed nodes and nodes with non-zero
|
|
12
|
+
* skew; runtime scale is ignored. In dev builds, warns when the node is not
|
|
13
|
+
* world-space-rooted (moved ancestor or transform-group boundary above it) -
|
|
14
|
+
* see {@link PhysicsBinding} for the local==world contract.
|
|
15
|
+
*/
|
|
11
16
|
bind(body: PhysicsBody, node: SceneNode): PhysicsBinding;
|
|
12
17
|
/** Remove the link for `body`, if any. */
|
|
13
18
|
unbind(body: PhysicsBody): void;
|
|
14
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Write every bound node's transform from its body, dropping links whose node
|
|
21
|
+
* has been destroyed.
|
|
22
|
+
*
|
|
23
|
+
* `SceneNode.destroy()` returns the node's position/rotation state to a shared
|
|
24
|
+
* pool, so a `sync()` that kept writing would not merely update a dead node -
|
|
25
|
+
* it would write through recycled state into whichever live node next takes
|
|
26
|
+
* that slot. Pruning here (rather than only on an explicit `unbind`) is what
|
|
27
|
+
* makes destroying a bound node safe without a physics-side teardown call.
|
|
28
|
+
*/
|
|
15
29
|
sync(): void;
|
|
30
|
+
/**
|
|
31
|
+
* Like {@link sync}, but places each node `alpha` of the way from its body's
|
|
32
|
+
* previous fixed state to its current one - see
|
|
33
|
+
* {@link PhysicsBinding.syncInterpolated}. Same destroyed-node pruning.
|
|
34
|
+
*/
|
|
35
|
+
syncInterpolated(alpha: number): void;
|
|
16
36
|
/** `true` when at least one binding exists. */
|
|
17
37
|
get size(): number;
|
|
18
38
|
/** Drop all links. */
|
|
19
39
|
clear(): void;
|
|
20
40
|
}
|
|
41
|
+
//# sourceMappingURL=BindingRegistry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BindingRegistry.d.ts","sourceRoot":"","sources":["../../../src/binding/BindingRegistry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAEvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAgDlD;;;GAGG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0C;IAEpE;;;;;OAKG;IACI,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,SAAS,GAAG,cAAc;IAqB/D,0CAA0C;IACnC,MAAM,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAItC;;;;;;;;;OASG;IACI,IAAI,IAAI,IAAI;IAWnB;;;;OAIG;IACI,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAW5C,+CAA+C;IAC/C,IAAW,IAAI,IAAI,MAAM,CAExB;IAED,sBAAsB;IACf,KAAK,IAAI,IAAI;CAGrB"}
|
|
@@ -1,40 +1,74 @@
|
|
|
1
|
-
import { PhysicsBinding } from
|
|
1
|
+
import { PhysicsBinding } from "./PhysicsBinding.js";
|
|
2
|
+
import "@codexo/exojs";
|
|
2
3
|
|
|
4
|
+
//#region src/binding/BindingRegistry.ts
|
|
3
5
|
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
6
|
+
* Owns the body ↔ node links and writes bound node transforms after each step.
|
|
7
|
+
* Backend-agnostic (frontend-level) - it reads only the public body transform.
|
|
8
|
+
*/
|
|
9
|
+
var BindingRegistry = class {
|
|
10
|
+
_bindings = /* @__PURE__ */ new Map();
|
|
11
|
+
/**
|
|
12
|
+
* Link `body` to `node`. Rejects destroyed nodes and nodes with non-zero
|
|
13
|
+
* skew; runtime scale is ignored. In dev builds, warns when the node is not
|
|
14
|
+
* world-space-rooted (moved ancestor or transform-group boundary above it) -
|
|
15
|
+
* see {@link PhysicsBinding} for the local==world contract.
|
|
16
|
+
*/
|
|
17
|
+
bind(body, node) {
|
|
18
|
+
if (node.destroyed) throw new Error("PhysicsBinding: cannot bind a destroyed node — its pooled transform has already been released.");
|
|
19
|
+
if (node.skewX !== 0 || node.skewY !== 0) throw new Error(`PhysicsBinding: the bound node has non-zero skew (${node.skewX}°, ${node.skewY}°); skewed nodes are not supported.`);
|
|
20
|
+
const binding = new PhysicsBinding(body, node);
|
|
21
|
+
this._bindings.set(body, binding);
|
|
22
|
+
binding.sync();
|
|
23
|
+
return binding;
|
|
24
|
+
}
|
|
25
|
+
/** Remove the link for `body`, if any. */
|
|
26
|
+
unbind(body) {
|
|
27
|
+
this._bindings.delete(body);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Write every bound node's transform from its body, dropping links whose node
|
|
31
|
+
* has been destroyed.
|
|
32
|
+
*
|
|
33
|
+
* `SceneNode.destroy()` returns the node's position/rotation state to a shared
|
|
34
|
+
* pool, so a `sync()` that kept writing would not merely update a dead node -
|
|
35
|
+
* it would write through recycled state into whichever live node next takes
|
|
36
|
+
* that slot. Pruning here (rather than only on an explicit `unbind`) is what
|
|
37
|
+
* makes destroying a bound node safe without a physics-side teardown call.
|
|
38
|
+
*/
|
|
39
|
+
sync() {
|
|
40
|
+
for (const [body, binding] of this._bindings) {
|
|
41
|
+
if (binding.node.destroyed) {
|
|
42
|
+
this._bindings.delete(body);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
binding.sync();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Like {@link sync}, but places each node `alpha` of the way from its body's
|
|
50
|
+
* previous fixed state to its current one - see
|
|
51
|
+
* {@link PhysicsBinding.syncInterpolated}. Same destroyed-node pruning.
|
|
52
|
+
*/
|
|
53
|
+
syncInterpolated(alpha) {
|
|
54
|
+
for (const [body, binding] of this._bindings) {
|
|
55
|
+
if (binding.node.destroyed) {
|
|
56
|
+
this._bindings.delete(body);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
binding.syncInterpolated(alpha);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** `true` when at least one binding exists. */
|
|
63
|
+
get size() {
|
|
64
|
+
return this._bindings.size;
|
|
65
|
+
}
|
|
66
|
+
/** Drop all links. */
|
|
67
|
+
clear() {
|
|
68
|
+
this._bindings.clear();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
38
71
|
|
|
72
|
+
//#endregion
|
|
39
73
|
export { BindingRegistry };
|
|
40
|
-
//# sourceMappingURL=BindingRegistry.js.map
|
|
74
|
+
//# sourceMappingURL=BindingRegistry.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BindingRegistry.js","sources":["
|
|
1
|
+
{"version":3,"file":"BindingRegistry.js","names":[],"sources":["../../../src/binding/BindingRegistry.ts"],"sourcesContent":["import { logger, type SceneNode } from '@codexo/exojs';\n\nimport type { PhysicsBody } from '../PhysicsBody';\nimport { PhysicsBinding } from './PhysicsBinding';\n\n/** `true` when `ancestor` contributes anything beyond identity to a descendant's world transform. */\nconst hasNonIdentityTransform = (ancestor: SceneNode): boolean =>\n ancestor.x !== 0 ||\n ancestor.y !== 0 ||\n ancestor.rotation !== 0 ||\n ancestor.skewX !== 0 ||\n ancestor.skewY !== 0 ||\n ancestor.scale.x !== 1 ||\n ancestor.scale.y !== 1 ||\n ancestor.origin.x !== 0 ||\n ancestor.origin.y !== 0;\n\n/**\n * Dev-only bind-time guard (F16): {@link PhysicsBinding.sync} writes the\n * body's WORLD transform into the node's LOCAL position/rotation, which is\n * only correct when local space == world space. Walk the parent chain and\n * warn when an ancestor either carries a non-identity transform or is a\n * transform-group boundary (RetainedContainer) - in both cases the ancestor\n * contribution is applied ON TOP of the world-space values sync() writes,\n * double-transforming the node on screen.\n */\nconst warnUnlessWorldSpaceRooted = (node: SceneNode): void => {\n // Duck-typed test doubles may omit `parent`; treat that as world-space-rooted.\n let ancestor: SceneNode | null = node.parent ?? null;\n\n while (ancestor !== null) {\n if (ancestor._isTransformGroupBoundary || hasNonIdentityTransform(ancestor)) {\n const nodeName = node.name !== null && node.name !== '' ? ` '${node.name}'` : '';\n const ancestorName = ancestor.name !== null && ancestor.name !== '' ? ` '${ancestor.name}'` : '';\n\n logger.warn(\n `PhysicsBinding: the bound node${nodeName} is not world-space-rooted — ancestor${ancestorName} ${\n ancestor._isTransformGroupBoundary ? 'is a transform-group boundary (RetainedContainer)' : 'has a non-identity transform'\n }. sync() writes the body's WORLD transform into the node's LOCAL position/rotation, so nesting it under a ` +\n 'moved/rotated/scaled container (or a transform group) will double-transform it on screen. Re-parent the node ' +\n 'to a world-space-rooted container.',\n { source: 'physics' },\n );\n\n return;\n }\n\n ancestor = ancestor.parent ?? null;\n }\n};\n\n/**\n * Owns the body ↔ node links and writes bound node transforms after each step.\n * Backend-agnostic (frontend-level) - it reads only the public body transform.\n */\nexport class BindingRegistry {\n private readonly _bindings = new Map<PhysicsBody, PhysicsBinding>();\n\n /**\n * Link `body` to `node`. Rejects destroyed nodes and nodes with non-zero\n * skew; runtime scale is ignored. In dev builds, warns when the node is not\n * world-space-rooted (moved ancestor or transform-group boundary above it) -\n * see {@link PhysicsBinding} for the local==world contract.\n */\n public bind(body: PhysicsBody, node: SceneNode): PhysicsBinding {\n if (node.destroyed) {\n throw new Error('PhysicsBinding: cannot bind a destroyed node — its pooled transform has already been released.');\n }\n\n if (node.skewX !== 0 || node.skewY !== 0) {\n throw new Error(`PhysicsBinding: the bound node has non-zero skew (${node.skewX}°, ${node.skewY}°); skewed nodes are not supported.`);\n }\n\n if (__DEV__) {\n warnUnlessWorldSpaceRooted(node);\n }\n\n const binding = new PhysicsBinding(body, node);\n\n this._bindings.set(body, binding);\n binding.sync();\n\n return binding;\n }\n\n /** Remove the link for `body`, if any. */\n public unbind(body: PhysicsBody): void {\n this._bindings.delete(body);\n }\n\n /**\n * Write every bound node's transform from its body, dropping links whose node\n * has been destroyed.\n *\n * `SceneNode.destroy()` returns the node's position/rotation state to a shared\n * pool, so a `sync()` that kept writing would not merely update a dead node -\n * it would write through recycled state into whichever live node next takes\n * that slot. Pruning here (rather than only on an explicit `unbind`) is what\n * makes destroying a bound node safe without a physics-side teardown call.\n */\n public sync(): void {\n for (const [body, binding] of this._bindings) {\n if (binding.node.destroyed) {\n this._bindings.delete(body);\n continue;\n }\n\n binding.sync();\n }\n }\n\n /**\n * Like {@link sync}, but places each node `alpha` of the way from its body's\n * previous fixed state to its current one - see\n * {@link PhysicsBinding.syncInterpolated}. Same destroyed-node pruning.\n */\n public syncInterpolated(alpha: number): void {\n for (const [body, binding] of this._bindings) {\n if (binding.node.destroyed) {\n this._bindings.delete(body);\n continue;\n }\n\n binding.syncInterpolated(alpha);\n }\n }\n\n /** `true` when at least one binding exists. */\n public get size(): number {\n return this._bindings.size;\n }\n\n /** Drop all links. */\n public clear(): void {\n this._bindings.clear();\n }\n}\n"],"mappings":";;;;;;;;AAuDA,IAAa,kBAAb,MAA6B;CAC3B,AAAiB,4BAAY,IAAI,IAAiC;;;;;;;CAQlE,AAAO,KAAK,MAAmB,MAAiC;EAC9D,IAAI,KAAK,WACP,MAAM,IAAI,MAAM,gGAAgG;EAGlH,IAAI,KAAK,UAAU,KAAK,KAAK,UAAU,GACrC,MAAM,IAAI,MAAM,qDAAqD,KAAK,MAAM,KAAK,KAAK,MAAM,oCAAoC;EAOtI,MAAM,UAAU,IAAI,eAAe,MAAM,IAAI;EAE7C,KAAK,UAAU,IAAI,MAAM,OAAO;EAChC,QAAQ,KAAK;EAEb,OAAO;CACT;;CAGA,AAAO,OAAO,MAAyB;EACrC,KAAK,UAAU,OAAO,IAAI;CAC5B;;;;;;;;;;;CAYA,AAAO,OAAa;EAClB,KAAK,MAAM,CAAC,MAAM,YAAY,KAAK,WAAW;GAC5C,IAAI,QAAQ,KAAK,WAAW;IAC1B,KAAK,UAAU,OAAO,IAAI;IAC1B;GACF;GAEA,QAAQ,KAAK;EACf;CACF;;;;;;CAOA,AAAO,iBAAiB,OAAqB;EAC3C,KAAK,MAAM,CAAC,MAAM,YAAY,KAAK,WAAW;GAC5C,IAAI,QAAQ,KAAK,WAAW;IAC1B,KAAK,UAAU,OAAO,IAAI;IAC1B;GACF;GAEA,QAAQ,iBAAiB,KAAK;EAChC;CACF;;CAGA,IAAW,OAAe;EACxB,OAAO,KAAK,UAAU;CACxB;;CAGA,AAAO,QAAc;EACnB,KAAK,UAAU,MAAM;CACvB;AACF"}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { type SceneNode } from '@codexo/exojs';
|
|
2
2
|
import type { PhysicsBody } from '../PhysicsBody';
|
|
3
3
|
/**
|
|
4
|
-
* A link between a {@link PhysicsBody} and a {@link SceneNode}.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* A link between a {@link PhysicsBody} and a {@link SceneNode}. The body's world
|
|
5
|
+
* position **and rotation** are written onto the node (the body's angle is
|
|
6
|
+
* radians; the node's rotation is degrees). The node must be world-space-rooted;
|
|
7
|
+
* runtime scale is ignored and non-zero skew is rejected at bind time.
|
|
8
|
+
*
|
|
9
|
+
* By default the node snaps to the latest fixed-step state after each step. With
|
|
10
|
+
* `PhysicsWorld`'s interpolation enabled the node is instead placed between the
|
|
11
|
+
* body's previous and current fixed states, which smooths motion when the fixed
|
|
12
|
+
* rate is below the frame rate. Either way this is presentation: nothing written
|
|
13
|
+
* onto the node is read back into the simulation.
|
|
9
14
|
*/
|
|
10
15
|
export declare class PhysicsBinding {
|
|
11
16
|
readonly body: PhysicsBody;
|
|
@@ -13,4 +18,16 @@ export declare class PhysicsBinding {
|
|
|
13
18
|
constructor(body: PhysicsBody, node: SceneNode);
|
|
14
19
|
/** Write the body's current transform (position + rotation) onto the bound node. */
|
|
15
20
|
sync(): void;
|
|
21
|
+
/**
|
|
22
|
+
* Place the node `alpha` of the way from the body's previous fixed state to
|
|
23
|
+
* its current one. `alpha` is the host's leftover sub-step fraction, so `0`
|
|
24
|
+
* renders the state the last fixed step started from and values approaching
|
|
25
|
+
* `1` render the state it produced.
|
|
26
|
+
*
|
|
27
|
+
* A plain lerp is correct for the angle too: body angles are continuous and
|
|
28
|
+
* unbounded, never wrapped into a range, so there is no shortest-arc case to
|
|
29
|
+
* resolve.
|
|
30
|
+
*/
|
|
31
|
+
syncInterpolated(alpha: number): void;
|
|
16
32
|
}
|
|
33
|
+
//# sourceMappingURL=PhysicsBinding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PhysicsBinding.d.ts","sourceRoot":"","sources":["../../../src/binding/PhysicsBinding.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAEjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD;;;;;;;;;;;GAWG;AACH,qBAAa,cAAc;aAEP,IAAI,EAAE,WAAW;aACjB,IAAI,EAAE,SAAS;gBADf,IAAI,EAAE,WAAW,EACjB,IAAI,EAAE,SAAS;IAGjC,oFAAoF;IAC7E,IAAI,IAAI,IAAI;IAKnB;;;;;;;;;OASG;IACI,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;CAS7C"}
|
|
@@ -1,25 +1,50 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { radiansToDegrees } from "@codexo/exojs";
|
|
2
2
|
|
|
3
|
+
//#region src/binding/PhysicsBinding.ts
|
|
3
4
|
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
5
|
+
* A link between a {@link PhysicsBody} and a {@link SceneNode}. The body's world
|
|
6
|
+
* position **and rotation** are written onto the node (the body's angle is
|
|
7
|
+
* radians; the node's rotation is degrees). The node must be world-space-rooted;
|
|
8
|
+
* runtime scale is ignored and non-zero skew is rejected at bind time.
|
|
9
|
+
*
|
|
10
|
+
* By default the node snaps to the latest fixed-step state after each step. With
|
|
11
|
+
* `PhysicsWorld`'s interpolation enabled the node is instead placed between the
|
|
12
|
+
* body's previous and current fixed states, which smooths motion when the fixed
|
|
13
|
+
* rate is below the frame rate. Either way this is presentation: nothing written
|
|
14
|
+
* onto the node is read back into the simulation.
|
|
15
|
+
*/
|
|
16
|
+
var PhysicsBinding = class {
|
|
17
|
+
body;
|
|
18
|
+
node;
|
|
19
|
+
constructor(body, node) {
|
|
20
|
+
this.body = body;
|
|
21
|
+
this.node = node;
|
|
22
|
+
}
|
|
23
|
+
/** Write the body's current transform (position + rotation) onto the bound node. */
|
|
24
|
+
sync() {
|
|
25
|
+
this.node.setPosition(this.body.x, this.body.y);
|
|
26
|
+
this.node.setRotation(radiansToDegrees(this.body.angle));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Place the node `alpha` of the way from the body's previous fixed state to
|
|
30
|
+
* its current one. `alpha` is the host's leftover sub-step fraction, so `0`
|
|
31
|
+
* renders the state the last fixed step started from and values approaching
|
|
32
|
+
* `1` render the state it produced.
|
|
33
|
+
*
|
|
34
|
+
* A plain lerp is correct for the angle too: body angles are continuous and
|
|
35
|
+
* unbounded, never wrapped into a range, so there is no shortest-arc case to
|
|
36
|
+
* resolve.
|
|
37
|
+
*/
|
|
38
|
+
syncInterpolated(alpha) {
|
|
39
|
+
const body = this.body;
|
|
40
|
+
const previousX = body.previousX;
|
|
41
|
+
const previousY = body.previousY;
|
|
42
|
+
const previousAngle = body.previousAngle;
|
|
43
|
+
this.node.setPosition(previousX + (body.x - previousX) * alpha, previousY + (body.y - previousY) * alpha);
|
|
44
|
+
this.node.setRotation(radiansToDegrees(previousAngle + (body.angle - previousAngle) * alpha));
|
|
45
|
+
}
|
|
46
|
+
};
|
|
23
47
|
|
|
48
|
+
//#endregion
|
|
24
49
|
export { PhysicsBinding };
|
|
25
|
-
//# sourceMappingURL=PhysicsBinding.js.map
|
|
50
|
+
//# sourceMappingURL=PhysicsBinding.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PhysicsBinding.js","sources":["
|
|
1
|
+
{"version":3,"file":"PhysicsBinding.js","names":[],"sources":["../../../src/binding/PhysicsBinding.ts"],"sourcesContent":["import { radiansToDegrees, type SceneNode } from '@codexo/exojs';\n\nimport type { PhysicsBody } from '../PhysicsBody';\n\n/**\n * A link between a {@link PhysicsBody} and a {@link SceneNode}. The body's world\n * position **and rotation** are written onto the node (the body's angle is\n * radians; the node's rotation is degrees). The node must be world-space-rooted;\n * runtime scale is ignored and non-zero skew is rejected at bind time.\n *\n * By default the node snaps to the latest fixed-step state after each step. With\n * `PhysicsWorld`'s interpolation enabled the node is instead placed between the\n * body's previous and current fixed states, which smooths motion when the fixed\n * rate is below the frame rate. Either way this is presentation: nothing written\n * onto the node is read back into the simulation.\n */\nexport class PhysicsBinding {\n public constructor(\n public readonly body: PhysicsBody,\n public readonly node: SceneNode,\n ) {}\n\n /** Write the body's current transform (position + rotation) onto the bound node. */\n public sync(): void {\n this.node.setPosition(this.body.x, this.body.y);\n this.node.setRotation(radiansToDegrees(this.body.angle));\n }\n\n /**\n * Place the node `alpha` of the way from the body's previous fixed state to\n * its current one. `alpha` is the host's leftover sub-step fraction, so `0`\n * renders the state the last fixed step started from and values approaching\n * `1` render the state it produced.\n *\n * A plain lerp is correct for the angle too: body angles are continuous and\n * unbounded, never wrapped into a range, so there is no shortest-arc case to\n * resolve.\n */\n public syncInterpolated(alpha: number): void {\n const body = this.body;\n const previousX = body.previousX;\n const previousY = body.previousY;\n const previousAngle = body.previousAngle;\n\n this.node.setPosition(previousX + (body.x - previousX) * alpha, previousY + (body.y - previousY) * alpha);\n this.node.setRotation(radiansToDegrees(previousAngle + (body.angle - previousAngle) * alpha));\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AAgBA,IAAa,iBAAb,MAA4B;CAER;CACA;CAFlB,AAAO,YACL,AAAgB,MAChB,AAAgB,MAChB;EAFgB;EACA;CACf;;CAGH,AAAO,OAAa;EAClB,KAAK,KAAK,YAAY,KAAK,KAAK,GAAG,KAAK,KAAK,CAAC;EAC9C,KAAK,KAAK,YAAY,iBAAiB,KAAK,KAAK,KAAK,CAAC;CACzD;;;;;;;;;;;CAYA,AAAO,iBAAiB,OAAqB;EAC3C,MAAM,OAAO,KAAK;EAClB,MAAM,YAAY,KAAK;EACvB,MAAM,YAAY,KAAK;EACvB,MAAM,gBAAgB,KAAK;EAE3B,KAAK,KAAK,YAAY,aAAa,KAAK,IAAI,aAAa,OAAO,aAAa,KAAK,IAAI,aAAa,KAAK;EACxG,KAAK,KAAK,YAAY,iBAAiB,iBAAiB,KAAK,QAAQ,iBAAiB,KAAK,CAAC;CAC9F;AACF"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { AabbLike } from '@codexo/exojs';
|
|
2
|
+
import type { Collider } from '../Collider';
|
|
3
|
+
import type { SpatialIndex } from '../query/SpatialIndex';
|
|
4
|
+
import type { BroadPhase, CandidatePair } from './BroadPhase';
|
|
5
|
+
/**
|
|
6
|
+
* Dynamic-AABB-tree broad phase (Box2D-style), wrapping a generic
|
|
7
|
+
* `DynamicAabbTree<Collider>`. Stateful across steps: a collider whose tight
|
|
8
|
+
* AABB stays inside its stored fat AABB costs nothing to re-sync.
|
|
9
|
+
*
|
|
10
|
+
* Three phases per `computePairs` call: (1) sync every live collider (insert
|
|
11
|
+
* new ones, reinsert moved ones, querying only around reinsertions to discover
|
|
12
|
+
* NEW fat-overlap candidates); (2) one full pass over the persistent pair set
|
|
13
|
+
* dropping any pair whose fat AABBs no longer overlap - cheap (O(1) per pair)
|
|
14
|
+
* and run every step regardless of movement, exactly mirroring Box2D's own
|
|
15
|
+
* `b2ContactManager::Collide` (see the design's correctness argument for why a
|
|
16
|
+
* single global pass, not a per-moved-leaf rescan, is both correct and the
|
|
17
|
+
* right complexity); (3) emit, keeping only pairs whose *tight* AABBs overlap.
|
|
18
|
+
*
|
|
19
|
+
* The persistent set is intentionally keyed on *fat* overlap so temporal
|
|
20
|
+
* coherence carries across steps (a leaf that stays inside its fat AABB is
|
|
21
|
+
* never re-discovered). Emitting is filtered to tight-AABB overlap so the
|
|
22
|
+
* candidate set the caller sees is exact - the fat margin never leaks a false
|
|
23
|
+
* positive downstream, and a fat-but-not-tight pair simply stays parked in the
|
|
24
|
+
* set (costing one O(1) re-check per step) until the leaves separate.
|
|
25
|
+
*
|
|
26
|
+
* **Single-owner contract.** A collider's tree membership is tracked through its
|
|
27
|
+
* `Collider._treeProxy` field, which - like `PhysicsBody._islandIndex` /
|
|
28
|
+
* `_sleepTime` - is a single slot on the shared object. Exactly ONE
|
|
29
|
+
* `AabbTreeBroadPhase` instance may track a given collider at a time (in
|
|
30
|
+
* production that is the world's own `_broadPhase`, for the collider's whole
|
|
31
|
+
* lifetime until `removeCollider`). Do NOT stand up a second, throwaway
|
|
32
|
+
* `AabbTreeBroadPhase` over colliders another instance already owns (e.g. for
|
|
33
|
+
* debug visualization): the second instance would clobber the first's
|
|
34
|
+
* `_treeProxy` values and corrupt its tree. Use a plain brute-force scan for
|
|
35
|
+
* such one-off, read-only needs instead.
|
|
36
|
+
*/
|
|
37
|
+
export declare class AabbTreeBroadPhase implements BroadPhase, SpatialIndex {
|
|
38
|
+
private readonly _tree;
|
|
39
|
+
private readonly _pairs;
|
|
40
|
+
private readonly _scratchFatAabb;
|
|
41
|
+
private _syncingCollider;
|
|
42
|
+
private _syncingProxy;
|
|
43
|
+
private readonly _onNeighborFound;
|
|
44
|
+
private readonly _dropIfStale;
|
|
45
|
+
private _collectTarget;
|
|
46
|
+
private readonly _collectPair;
|
|
47
|
+
private _queryTarget;
|
|
48
|
+
private readonly _onQueryHit;
|
|
49
|
+
private readonly _resetTreeProxy;
|
|
50
|
+
computePairs(colliders: readonly Collider[], out: CandidatePair[]): CandidatePair[];
|
|
51
|
+
/**
|
|
52
|
+
* Insert every not-yet-tracked collider and reinsert every moved one (the
|
|
53
|
+
* `computePairs` phase-1 loop, extracted so {@link queryAabb} callers can
|
|
54
|
+
* force it independently of a full detection pass - see {@link SpatialIndex.sync}).
|
|
55
|
+
*/
|
|
56
|
+
sync(colliders: readonly Collider[]): void;
|
|
57
|
+
/** Colliders whose AABB overlaps `aabb`. Writes into `out` (cleared first) and returns it. */
|
|
58
|
+
queryAabb(aabb: Readonly<AabbLike>, out: Collider[]): Collider[];
|
|
59
|
+
/** Colliders whose AABB the ray could cross within `maxDistance`, via the tree's own ray-cast prune. */
|
|
60
|
+
rayCast(originX: number, originY: number, dirX: number, dirY: number, maxDistance: number, callback: (collider: Collider) => void): void;
|
|
61
|
+
/** Drop `collider`'s leaf and every pair referencing it. Called when a collider is destroyed. */
|
|
62
|
+
removeCollider(collider: Collider): void;
|
|
63
|
+
destroy(): void;
|
|
64
|
+
private _sync;
|
|
65
|
+
private _discoverNeighbors;
|
|
66
|
+
private _addPair;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=AabbTreeBroadPhase.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AabbTreeBroadPhase.d.ts","sourceRoot":"","sources":["../../../src/broadphase/AabbTreeBroadPhase.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAI9C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE1D,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAe9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,qBAAa,kBAAmB,YAAW,UAAU,EAAE,YAAY;IACjE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA8C;IACpE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoC;IAC3D,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAoD;IAEpF,OAAO,CAAC,gBAAgB,CAAyB;IACjD,OAAO,CAAC,aAAa,CAAM;IAC3B,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAM/B;IAEF,OAAO,CAAC,QAAQ,CAAC,YAAY,CAI3B;IAEF,OAAO,CAAC,cAAc,CAAuB;IAC7C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAM3B;IAEF,OAAO,CAAC,YAAY,CAAkB;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAE1B;IAEF,OAAO,CAAC,QAAQ,CAAC,eAAe,CAE9B;IAEK,YAAY,CAAC,SAAS,EAAE,SAAS,QAAQ,EAAE,EAAE,GAAG,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE;IAa1F;;;;OAIG;IACI,IAAI,CAAC,SAAS,EAAE,SAAS,QAAQ,EAAE,GAAG,IAAI;IAMjD,8FAA8F;IACvF,SAAS,CAAC,IAAI,EAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,QAAQ,EAAE;IAQvE,wGAAwG;IACjG,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,GAAG,IAAI;IAI/I,iGAAiG;IAC1F,cAAc,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI;IAexC,OAAO,IAAI,IAAI;IAYtB,OAAO,CAAC,KAAK;IAiBb,OAAO,CAAC,kBAAkB;IAS1B,OAAO,CAAC,QAAQ;CAiBjB"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { aabbOverlap } from "../Aabb.js";
|
|
2
|
+
import { sortInPlace } from "../sort.js";
|
|
3
|
+
import { pairKey } from "../ContactGraph.js";
|
|
4
|
+
import { DynamicAabbTree } from "@codexo/exojs";
|
|
5
|
+
|
|
6
|
+
//#region src/broadphase/AabbTreeBroadPhase.ts
|
|
7
|
+
/**
|
|
8
|
+
* Fixed fat-AABB margin (world px) applied to every collider's tree leaf.
|
|
9
|
+
* Chosen relative to the engine's typical small-collider scale (test/example
|
|
10
|
+
* shapes commonly span 10-30px): large enough that a few steps of ordinary
|
|
11
|
+
* motion stay inside the margin (skipping the sync phase's remove+reinsert),
|
|
12
|
+
* small enough it doesn't inflate fat AABBs into spurious candidates for
|
|
13
|
+
* densely packed scenes. Fixed per the design's v1 scope (no velocity-based
|
|
14
|
+
* predictive expansion) - revisit if benchmarks want a different value.
|
|
15
|
+
*/
|
|
16
|
+
const AABB_MARGIN = 4;
|
|
17
|
+
const byPairId = (p, q) => p.a.id - q.a.id || p.b.id - q.b.id;
|
|
18
|
+
/**
|
|
19
|
+
* Dynamic-AABB-tree broad phase (Box2D-style), wrapping a generic
|
|
20
|
+
* `DynamicAabbTree<Collider>`. Stateful across steps: a collider whose tight
|
|
21
|
+
* AABB stays inside its stored fat AABB costs nothing to re-sync.
|
|
22
|
+
*
|
|
23
|
+
* Three phases per `computePairs` call: (1) sync every live collider (insert
|
|
24
|
+
* new ones, reinsert moved ones, querying only around reinsertions to discover
|
|
25
|
+
* NEW fat-overlap candidates); (2) one full pass over the persistent pair set
|
|
26
|
+
* dropping any pair whose fat AABBs no longer overlap - cheap (O(1) per pair)
|
|
27
|
+
* and run every step regardless of movement, exactly mirroring Box2D's own
|
|
28
|
+
* `b2ContactManager::Collide` (see the design's correctness argument for why a
|
|
29
|
+
* single global pass, not a per-moved-leaf rescan, is both correct and the
|
|
30
|
+
* right complexity); (3) emit, keeping only pairs whose *tight* AABBs overlap.
|
|
31
|
+
*
|
|
32
|
+
* The persistent set is intentionally keyed on *fat* overlap so temporal
|
|
33
|
+
* coherence carries across steps (a leaf that stays inside its fat AABB is
|
|
34
|
+
* never re-discovered). Emitting is filtered to tight-AABB overlap so the
|
|
35
|
+
* candidate set the caller sees is exact - the fat margin never leaks a false
|
|
36
|
+
* positive downstream, and a fat-but-not-tight pair simply stays parked in the
|
|
37
|
+
* set (costing one O(1) re-check per step) until the leaves separate.
|
|
38
|
+
*
|
|
39
|
+
* **Single-owner contract.** A collider's tree membership is tracked through its
|
|
40
|
+
* `Collider._treeProxy` field, which - like `PhysicsBody._islandIndex` /
|
|
41
|
+
* `_sleepTime` - is a single slot on the shared object. Exactly ONE
|
|
42
|
+
* `AabbTreeBroadPhase` instance may track a given collider at a time (in
|
|
43
|
+
* production that is the world's own `_broadPhase`, for the collider's whole
|
|
44
|
+
* lifetime until `removeCollider`). Do NOT stand up a second, throwaway
|
|
45
|
+
* `AabbTreeBroadPhase` over colliders another instance already owns (e.g. for
|
|
46
|
+
* debug visualization): the second instance would clobber the first's
|
|
47
|
+
* `_treeProxy` values and corrupt its tree. Use a plain brute-force scan for
|
|
48
|
+
* such one-off, read-only needs instead.
|
|
49
|
+
*/
|
|
50
|
+
var AabbTreeBroadPhase = class {
|
|
51
|
+
_tree = new DynamicAabbTree(AABB_MARGIN);
|
|
52
|
+
_pairs = /* @__PURE__ */ new Map();
|
|
53
|
+
_scratchFatAabb = {
|
|
54
|
+
minX: 0,
|
|
55
|
+
minY: 0,
|
|
56
|
+
maxX: 0,
|
|
57
|
+
maxY: 0
|
|
58
|
+
};
|
|
59
|
+
_syncingCollider = null;
|
|
60
|
+
_syncingProxy = -1;
|
|
61
|
+
_onNeighborFound = (payload, proxy) => {
|
|
62
|
+
if (proxy === this._syncingProxy) return;
|
|
63
|
+
this._addPair(this._syncingCollider, payload);
|
|
64
|
+
};
|
|
65
|
+
_dropIfStale = (pair, key) => {
|
|
66
|
+
if (!this._tree.fatOverlaps(pair.a._treeProxy, pair.b._treeProxy)) this._pairs.delete(key);
|
|
67
|
+
};
|
|
68
|
+
_collectTarget = [];
|
|
69
|
+
_collectPair = (pair) => {
|
|
70
|
+
if (aabbOverlap(pair.a.aabb, pair.b.aabb)) this._collectTarget.push(pair);
|
|
71
|
+
};
|
|
72
|
+
_queryTarget = [];
|
|
73
|
+
_onQueryHit = (payload) => {
|
|
74
|
+
this._queryTarget.push(payload);
|
|
75
|
+
};
|
|
76
|
+
_resetTreeProxy = (payload) => {
|
|
77
|
+
payload._treeProxy = -1;
|
|
78
|
+
};
|
|
79
|
+
computePairs(colliders, out) {
|
|
80
|
+
this.sync(colliders);
|
|
81
|
+
this._pairs.forEach(this._dropIfStale);
|
|
82
|
+
out.length = 0;
|
|
83
|
+
this._collectTarget = out;
|
|
84
|
+
this._pairs.forEach(this._collectPair);
|
|
85
|
+
sortInPlace(out, byPairId);
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Insert every not-yet-tracked collider and reinsert every moved one (the
|
|
90
|
+
* `computePairs` phase-1 loop, extracted so {@link queryAabb} callers can
|
|
91
|
+
* force it independently of a full detection pass - see {@link SpatialIndex.sync}).
|
|
92
|
+
*/
|
|
93
|
+
sync(colliders) {
|
|
94
|
+
for (const collider of colliders) this._sync(collider);
|
|
95
|
+
}
|
|
96
|
+
/** Colliders whose AABB overlaps `aabb`. Writes into `out` (cleared first) and returns it. */
|
|
97
|
+
queryAabb(aabb, out) {
|
|
98
|
+
out.length = 0;
|
|
99
|
+
this._queryTarget = out;
|
|
100
|
+
this._tree.query(aabb.minX, aabb.minY, aabb.maxX, aabb.maxY, this._onQueryHit);
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
/** Colliders whose AABB the ray could cross within `maxDistance`, via the tree's own ray-cast prune. */
|
|
104
|
+
rayCast(originX, originY, dirX, dirY, maxDistance, callback) {
|
|
105
|
+
this._tree.rayCast(originX, originY, dirX, dirY, maxDistance, callback);
|
|
106
|
+
}
|
|
107
|
+
/** Drop `collider`'s leaf and every pair referencing it. Called when a collider is destroyed. */
|
|
108
|
+
removeCollider(collider) {
|
|
109
|
+
if (collider._treeProxy === -1) return;
|
|
110
|
+
this._tree.remove(collider._treeProxy);
|
|
111
|
+
collider._treeProxy = -1;
|
|
112
|
+
for (const [key, pair] of this._pairs) if (pair.a === collider || pair.b === collider) this._pairs.delete(key);
|
|
113
|
+
}
|
|
114
|
+
destroy() {
|
|
115
|
+
this._tree.query(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, this._resetTreeProxy);
|
|
116
|
+
this._tree.destroy();
|
|
117
|
+
this._pairs.clear();
|
|
118
|
+
}
|
|
119
|
+
_sync(collider) {
|
|
120
|
+
const box = collider.aabb;
|
|
121
|
+
if (collider._treeProxy === -1) {
|
|
122
|
+
collider._treeProxy = this._tree.insert(box.minX, box.minY, box.maxX, box.maxY, collider);
|
|
123
|
+
this._discoverNeighbors(collider);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (this._tree.update(collider._treeProxy, box.minX, box.minY, box.maxX, box.maxY)) this._discoverNeighbors(collider);
|
|
127
|
+
}
|
|
128
|
+
_discoverNeighbors(collider) {
|
|
129
|
+
const proxy = collider._treeProxy;
|
|
130
|
+
const fat = this._tree.fatAabbOf(proxy, this._scratchFatAabb);
|
|
131
|
+
this._syncingCollider = collider;
|
|
132
|
+
this._syncingProxy = proxy;
|
|
133
|
+
this._tree.query(fat.minX, fat.minY, fat.maxX, fat.maxY, this._onNeighborFound);
|
|
134
|
+
}
|
|
135
|
+
_addPair(a, b) {
|
|
136
|
+
if (a.body === b.body) return;
|
|
137
|
+
const lo = a.id < b.id ? a : b;
|
|
138
|
+
const hi = a.id < b.id ? b : a;
|
|
139
|
+
const key = pairKey(lo.id, hi.id);
|
|
140
|
+
if (!this._pairs.has(key)) this._pairs.set(key, {
|
|
141
|
+
a: lo,
|
|
142
|
+
b: hi
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
//#endregion
|
|
148
|
+
export { AabbTreeBroadPhase };
|
|
149
|
+
//# sourceMappingURL=AabbTreeBroadPhase.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AabbTreeBroadPhase.js","names":[],"sources":["../../../src/broadphase/AabbTreeBroadPhase.ts"],"sourcesContent":["import type { AabbLike } from '@codexo/exojs';\nimport { DynamicAabbTree } from '@codexo/exojs';\n\nimport { aabbOverlap } from '../Aabb';\nimport type { Collider } from '../Collider';\nimport { pairKey } from '../ContactGraph';\nimport type { SpatialIndex } from '../query/SpatialIndex';\nimport { sortInPlace } from '../sort';\nimport type { BroadPhase, CandidatePair } from './BroadPhase';\n\n/**\n * Fixed fat-AABB margin (world px) applied to every collider's tree leaf.\n * Chosen relative to the engine's typical small-collider scale (test/example\n * shapes commonly span 10-30px): large enough that a few steps of ordinary\n * motion stay inside the margin (skipping the sync phase's remove+reinsert),\n * small enough it doesn't inflate fat AABBs into spurious candidates for\n * densely packed scenes. Fixed per the design's v1 scope (no velocity-based\n * predictive expansion) - revisit if benchmarks want a different value.\n */\nconst AABB_MARGIN = 4;\n\nconst byPairId = (p: CandidatePair, q: CandidatePair): number => p.a.id - q.a.id || p.b.id - q.b.id;\n\n/**\n * Dynamic-AABB-tree broad phase (Box2D-style), wrapping a generic\n * `DynamicAabbTree<Collider>`. Stateful across steps: a collider whose tight\n * AABB stays inside its stored fat AABB costs nothing to re-sync.\n *\n * Three phases per `computePairs` call: (1) sync every live collider (insert\n * new ones, reinsert moved ones, querying only around reinsertions to discover\n * NEW fat-overlap candidates); (2) one full pass over the persistent pair set\n * dropping any pair whose fat AABBs no longer overlap - cheap (O(1) per pair)\n * and run every step regardless of movement, exactly mirroring Box2D's own\n * `b2ContactManager::Collide` (see the design's correctness argument for why a\n * single global pass, not a per-moved-leaf rescan, is both correct and the\n * right complexity); (3) emit, keeping only pairs whose *tight* AABBs overlap.\n *\n * The persistent set is intentionally keyed on *fat* overlap so temporal\n * coherence carries across steps (a leaf that stays inside its fat AABB is\n * never re-discovered). Emitting is filtered to tight-AABB overlap so the\n * candidate set the caller sees is exact - the fat margin never leaks a false\n * positive downstream, and a fat-but-not-tight pair simply stays parked in the\n * set (costing one O(1) re-check per step) until the leaves separate.\n *\n * **Single-owner contract.** A collider's tree membership is tracked through its\n * `Collider._treeProxy` field, which - like `PhysicsBody._islandIndex` /\n * `_sleepTime` - is a single slot on the shared object. Exactly ONE\n * `AabbTreeBroadPhase` instance may track a given collider at a time (in\n * production that is the world's own `_broadPhase`, for the collider's whole\n * lifetime until `removeCollider`). Do NOT stand up a second, throwaway\n * `AabbTreeBroadPhase` over colliders another instance already owns (e.g. for\n * debug visualization): the second instance would clobber the first's\n * `_treeProxy` values and corrupt its tree. Use a plain brute-force scan for\n * such one-off, read-only needs instead.\n */\nexport class AabbTreeBroadPhase implements BroadPhase, SpatialIndex {\n private readonly _tree = new DynamicAabbTree<Collider>(AABB_MARGIN);\n private readonly _pairs = new Map<number, CandidatePair>();\n private readonly _scratchFatAabb: AabbLike = { minX: 0, minY: 0, maxX: 0, maxY: 0 };\n\n private _syncingCollider: Collider | null = null;\n private _syncingProxy = -1;\n private readonly _onNeighborFound = (payload: Collider, proxy: number): void => {\n if (proxy === this._syncingProxy) {\n return;\n }\n\n this._addPair(this._syncingCollider!, payload);\n };\n\n private readonly _dropIfStale = (pair: CandidatePair, key: number): void => {\n if (!this._tree.fatOverlaps(pair.a._treeProxy, pair.b._treeProxy)) {\n this._pairs.delete(key);\n }\n };\n\n private _collectTarget: CandidatePair[] = [];\n private readonly _collectPair = (pair: CandidatePair): void => {\n // Emit only true (tight-AABB) overlaps; the persistent set is fat-keyed for\n // coherence, so a fat-but-not-tight pair is parked here, not a candidate.\n if (aabbOverlap(pair.a.aabb, pair.b.aabb)) {\n this._collectTarget.push(pair);\n }\n };\n\n private _queryTarget: Collider[] = [];\n private readonly _onQueryHit = (payload: Collider): void => {\n this._queryTarget.push(payload);\n };\n\n private readonly _resetTreeProxy = (payload: Collider): void => {\n payload._treeProxy = -1;\n };\n\n public computePairs(colliders: readonly Collider[], out: CandidatePair[]): CandidatePair[] {\n this.sync(colliders);\n\n this._pairs.forEach(this._dropIfStale);\n\n out.length = 0;\n this._collectTarget = out;\n this._pairs.forEach(this._collectPair);\n sortInPlace(out, byPairId);\n\n return out;\n }\n\n /**\n * Insert every not-yet-tracked collider and reinsert every moved one (the\n * `computePairs` phase-1 loop, extracted so {@link queryAabb} callers can\n * force it independently of a full detection pass - see {@link SpatialIndex.sync}).\n */\n public sync(colliders: readonly Collider[]): void {\n for (const collider of colliders) {\n this._sync(collider);\n }\n }\n\n /** Colliders whose AABB overlaps `aabb`. Writes into `out` (cleared first) and returns it. */\n public queryAabb(aabb: Readonly<AabbLike>, out: Collider[]): Collider[] {\n out.length = 0;\n this._queryTarget = out;\n this._tree.query(aabb.minX, aabb.minY, aabb.maxX, aabb.maxY, this._onQueryHit);\n\n return out;\n }\n\n /** Colliders whose AABB the ray could cross within `maxDistance`, via the tree's own ray-cast prune. */\n public rayCast(originX: number, originY: number, dirX: number, dirY: number, maxDistance: number, callback: (collider: Collider) => void): void {\n this._tree.rayCast(originX, originY, dirX, dirY, maxDistance, callback);\n }\n\n /** Drop `collider`'s leaf and every pair referencing it. Called when a collider is destroyed. */\n public removeCollider(collider: Collider): void {\n if (collider._treeProxy === -1) {\n return;\n }\n\n this._tree.remove(collider._treeProxy);\n collider._treeProxy = -1;\n\n for (const [key, pair] of this._pairs) {\n if (pair.a === collider || pair.b === collider) {\n this._pairs.delete(key);\n }\n }\n }\n\n public destroy(): void {\n // Reset `_treeProxy` on every still-tracked collider before tearing down the\n // tree: the single-owner contract only guarantees at most one owner AT A\n // TIME, so a collider handed to a later `AabbTreeBroadPhase` (sequential\n // reuse, not concurrent dual-ownership) must not carry a stale non-`-1`\n // proxy from this instance forward. `query` with infinite bounds visits\n // every leaf currently in the tree, allocation-free.\n this._tree.query(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, this._resetTreeProxy);\n this._tree.destroy();\n this._pairs.clear();\n }\n\n private _sync(collider: Collider): void {\n const box = collider.aabb;\n\n if (collider._treeProxy === -1) {\n collider._treeProxy = this._tree.insert(box.minX, box.minY, box.maxX, box.maxY, collider);\n this._discoverNeighbors(collider);\n\n return;\n }\n\n const moved = this._tree.update(collider._treeProxy, box.minX, box.minY, box.maxX, box.maxY);\n\n if (moved) {\n this._discoverNeighbors(collider);\n }\n }\n\n private _discoverNeighbors(collider: Collider): void {\n const proxy = collider._treeProxy;\n const fat = this._tree.fatAabbOf(proxy, this._scratchFatAabb);\n\n this._syncingCollider = collider;\n this._syncingProxy = proxy;\n this._tree.query(fat.minX, fat.minY, fat.maxX, fat.maxY, this._onNeighborFound);\n }\n\n private _addPair(a: Collider, b: Collider): void {\n // Two colliders of the same body are a compound shape, not a collision.\n // Rejecting the pair here rather than downstream also keeps it out of the\n // persistent set, so a compound body costs nothing per step - which matters\n // once convex decomposition attaches many edge-sharing colliders to one body.\n if (a.body === b.body) {\n return;\n }\n\n const lo = a.id < b.id ? a : b;\n const hi = a.id < b.id ? b : a;\n const key = pairKey(lo.id, hi.id);\n\n if (!this._pairs.has(key)) {\n this._pairs.set(key, { a: lo, b: hi });\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -7,10 +7,12 @@ export interface CandidatePair {
|
|
|
7
7
|
/**
|
|
8
8
|
* Broad-phase contract: reduce the O(n²) all-pairs test to a candidate set of
|
|
9
9
|
* AABB-overlapping pairs. The candidate set must contain **every** truly
|
|
10
|
-
* overlapping pair (zero false negatives
|
|
11
|
-
* resolved by the narrow phase.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* overlapping pair of colliders on *different* bodies (zero false negatives);
|
|
11
|
+
* false positives are resolved by the narrow phase. Two colliders of the same
|
|
12
|
+
* body form a compound shape and must never be paired. This interface is the
|
|
13
|
+
* seam behind which broad-phase implementations (currently the
|
|
14
|
+
* dynamic-AABB-tree-backed `AabbTreeBroadPhase`) can be swapped without
|
|
15
|
+
* touching callers.
|
|
14
16
|
*/
|
|
15
17
|
export interface BroadPhase {
|
|
16
18
|
/**
|
|
@@ -20,3 +22,4 @@ export interface BroadPhase {
|
|
|
20
22
|
*/
|
|
21
23
|
computePairs(colliders: readonly Collider[], out: CandidatePair[]): CandidatePair[];
|
|
22
24
|
}
|
|
25
|
+
//# sourceMappingURL=BroadPhase.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BroadPhase.d.ts","sourceRoot":"","sources":["../../../src/broadphase/BroadPhase.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,yFAAyF;AACzF,MAAM,WAAW,aAAa;IAC5B,CAAC,EAAE,QAAQ,CAAC;IACZ,CAAC,EAAE,QAAQ,CAAC;CACb;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,UAAU;IACzB;;;;OAIG;IACH,YAAY,CAAC,SAAS,EAAE,SAAS,QAAQ,EAAE,EAAE,GAAG,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE,CAAC;CACrF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/broadphase/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PointLike } from '@codexo/exojs';
|
|
2
2
|
import type { AnyShape } from '../shapes/AnyShape';
|
|
3
3
|
/**
|
|
4
4
|
* The geometric surface the narrow phase needs: a shape plus its cached world
|
|
@@ -8,7 +8,8 @@ import type { AnyShape } from '../shapes/AnyShape';
|
|
|
8
8
|
*/
|
|
9
9
|
export interface CollisionProxy {
|
|
10
10
|
readonly shape: AnyShape;
|
|
11
|
-
readonly worldCenter: Readonly<
|
|
11
|
+
readonly worldCenter: Readonly<PointLike>;
|
|
12
12
|
readonly worldVertices: readonly number[];
|
|
13
13
|
readonly worldNormals: readonly number[];
|
|
14
14
|
}
|
|
15
|
+
//# sourceMappingURL=CollisionProxy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CollisionProxy.d.ts","sourceRoot":"","sources":["../../../src/collision/CollisionProxy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE/C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAEnD;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAC;IAC1C,QAAQ,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C"}
|