@codexo/exojs-physics 0.13.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/LICENSE +21 -0
- package/README.md +82 -0
- package/dist/esm/Aabb.d.ts +14 -0
- package/dist/esm/Aabb.js +12 -0
- package/dist/esm/Aabb.js.map +1 -0
- package/dist/esm/Collider.d.ts +77 -0
- package/dist/esm/Collider.js +138 -0
- package/dist/esm/Collider.js.map +1 -0
- package/dist/esm/ContactGraph.d.ts +34 -0
- package/dist/esm/ContactGraph.js +157 -0
- package/dist/esm/ContactGraph.js.map +1 -0
- package/dist/esm/PhysicsBody.d.ts +103 -0
- package/dist/esm/PhysicsBody.js +186 -0
- package/dist/esm/PhysicsBody.js.map +1 -0
- package/dist/esm/PhysicsWorld.d.ts +126 -0
- package/dist/esm/PhysicsWorld.js +253 -0
- package/dist/esm/PhysicsWorld.js.map +1 -0
- package/dist/esm/TimeStepper.d.ts +39 -0
- package/dist/esm/TimeStepper.js +70 -0
- package/dist/esm/TimeStepper.js.map +1 -0
- package/dist/esm/backend/NativePhysicsBackend.d.ts +19 -0
- package/dist/esm/backend/NativePhysicsBackend.js +31 -0
- package/dist/esm/backend/NativePhysicsBackend.js.map +1 -0
- package/dist/esm/backend/PhysicsBackend.d.ts +23 -0
- package/dist/esm/binding/BindingRegistry.d.ts +21 -0
- package/dist/esm/binding/BindingRegistry.js +40 -0
- package/dist/esm/binding/BindingRegistry.js.map +1 -0
- package/dist/esm/binding/PhysicsBinding.d.ts +27 -0
- package/dist/esm/binding/PhysicsBinding.js +24 -0
- package/dist/esm/binding/PhysicsBinding.js.map +1 -0
- package/dist/esm/broadphase/BroadPhase.d.ts +22 -0
- package/dist/esm/broadphase/SweepAndPrune.d.ts +14 -0
- package/dist/esm/broadphase/SweepAndPrune.js +48 -0
- package/dist/esm/broadphase/SweepAndPrune.js.map +1 -0
- package/dist/esm/broadphase/index.d.ts +2 -0
- package/dist/esm/collision/CollisionProxy.d.ts +14 -0
- package/dist/esm/collision/Manifold.d.ts +33 -0
- package/dist/esm/collision/Manifold.js +28 -0
- package/dist/esm/collision/Manifold.js.map +1 -0
- package/dist/esm/collision/index.d.ts +3 -0
- package/dist/esm/collision/narrowphase.d.ts +14 -0
- package/dist/esm/collision/narrowphase.js +377 -0
- package/dist/esm/collision/narrowphase.js.map +1 -0
- package/dist/esm/debug/PhysicsDebugDraw.d.ts +46 -0
- package/dist/esm/debug/PhysicsDebugDraw.js +171 -0
- package/dist/esm/debug/PhysicsDebugDraw.js.map +1 -0
- package/dist/esm/debug/index.d.ts +1 -0
- package/dist/esm/debug/index.js +2 -0
- package/dist/esm/debug/index.js.map +1 -0
- package/dist/esm/events.d.ts +35 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +12 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/math.d.ts +32 -0
- package/dist/esm/math.js +48 -0
- package/dist/esm/math.js.map +1 -0
- package/dist/esm/physicsBuildInfo.d.ts +15 -0
- package/dist/esm/physicsBuildInfo.js +8 -0
- package/dist/esm/physicsBuildInfo.js.map +1 -0
- package/dist/esm/public.d.ts +21 -0
- package/dist/esm/query/QueryEngine.d.ts +51 -0
- package/dist/esm/query/QueryEngine.js +246 -0
- package/dist/esm/query/QueryEngine.js.map +1 -0
- package/dist/esm/shapes/BoxShape.d.ts +11 -0
- package/dist/esm/shapes/BoxShape.js +29 -0
- package/dist/esm/shapes/BoxShape.js.map +1 -0
- package/dist/esm/shapes/CircleShape.d.ts +16 -0
- package/dist/esm/shapes/CircleShape.js +30 -0
- package/dist/esm/shapes/CircleShape.js.map +1 -0
- package/dist/esm/shapes/PolygonShape.d.ts +26 -0
- package/dist/esm/shapes/PolygonShape.js +156 -0
- package/dist/esm/shapes/PolygonShape.js.map +1 -0
- package/dist/esm/shapes/Shape.d.ts +26 -0
- package/dist/esm/shapes/Shape.js +16 -0
- package/dist/esm/shapes/Shape.js.map +1 -0
- package/dist/esm/shapes/index.d.ts +4 -0
- package/dist/esm/types.d.ts +39 -0
- package/dist/esm/types.js +28 -0
- package/dist/esm/types.js.map +1 -0
- package/package.json +51 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixed-timestep accumulator (package-owned; no engine clock dependency). Each
|
|
3
|
+
* frame, {@link advance} adds the variable frame delta to an internal
|
|
4
|
+
* accumulator and reports how many whole fixed sub-steps to run
|
|
5
|
+
* (`floor(accumulator / fixedDelta)`), clamped to {@link maxSubSteps}. When the
|
|
6
|
+
* clamp trips, the backlog beyond the clamp is discarded so a slow frame cannot
|
|
7
|
+
* cascade into an unbounded catch-up (the "spiral of death").
|
|
8
|
+
*
|
|
9
|
+
* {@link alpha} is the leftover fraction of a step `[0, 1)`, for interpolating
|
|
10
|
+
* bound transforms between sub-steps.
|
|
11
|
+
*
|
|
12
|
+
* The stepper holds no mutable module-level state — two stepper instances are
|
|
13
|
+
* fully independent (world isolation, gate I-1).
|
|
14
|
+
*/
|
|
15
|
+
class TimeStepper {
|
|
16
|
+
fixedDelta;
|
|
17
|
+
maxSubSteps;
|
|
18
|
+
_accumulator = 0;
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
const fixedDelta = options.fixedDelta ?? 1 / 60;
|
|
21
|
+
const maxSubSteps = options.maxSubSteps ?? 8;
|
|
22
|
+
if (!Number.isFinite(fixedDelta) || fixedDelta <= 0) {
|
|
23
|
+
throw new RangeError(`TimeStepper: fixedDelta must be a positive finite number, received ${fixedDelta}.`);
|
|
24
|
+
}
|
|
25
|
+
if (!Number.isInteger(maxSubSteps) || maxSubSteps < 1) {
|
|
26
|
+
throw new RangeError(`TimeStepper: maxSubSteps must be an integer ≥ 1, received ${maxSubSteps}.`);
|
|
27
|
+
}
|
|
28
|
+
this.fixedDelta = fixedDelta;
|
|
29
|
+
this.maxSubSteps = maxSubSteps;
|
|
30
|
+
}
|
|
31
|
+
/** Unconsumed accumulated time (seconds) currently held, always `< fixedDelta` after an `advance`. */
|
|
32
|
+
get accumulator() {
|
|
33
|
+
return this._accumulator;
|
|
34
|
+
}
|
|
35
|
+
/** Interpolation fraction in `[0, 1)` = `accumulator / fixedDelta`. */
|
|
36
|
+
get alpha() {
|
|
37
|
+
return Math.min(1, Math.max(0, this._accumulator / this.fixedDelta));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Accumulate `frameDeltaSeconds` and return the number of fixed sub-steps to
|
|
41
|
+
* run this frame. Non-finite or non-positive deltas contribute nothing and
|
|
42
|
+
* return `0`.
|
|
43
|
+
*/
|
|
44
|
+
advance(frameDeltaSeconds) {
|
|
45
|
+
if (!Number.isFinite(frameDeltaSeconds) || frameDeltaSeconds <= 0) {
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
this._accumulator += frameDeltaSeconds;
|
|
49
|
+
let steps = Math.floor(this._accumulator / this.fixedDelta);
|
|
50
|
+
if (steps <= 0) {
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
if (steps > this.maxSubSteps) {
|
|
54
|
+
steps = this.maxSubSteps;
|
|
55
|
+
}
|
|
56
|
+
this._accumulator -= steps * this.fixedDelta;
|
|
57
|
+
// Discard any backlog beyond the clamp, keeping only the sub-frame remainder.
|
|
58
|
+
if (this._accumulator >= this.fixedDelta) {
|
|
59
|
+
this._accumulator %= this.fixedDelta;
|
|
60
|
+
}
|
|
61
|
+
return steps;
|
|
62
|
+
}
|
|
63
|
+
/** Clear the accumulator (e.g. after a teleport or a pause). */
|
|
64
|
+
reset() {
|
|
65
|
+
this._accumulator = 0;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { TimeStepper };
|
|
70
|
+
//# sourceMappingURL=TimeStepper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TimeStepper.js","sources":["../../../src/TimeStepper.ts"],"sourcesContent":[null],"names":[],"mappings":"AAQA;;;;;;;;;;;;;AAaG;MACU,WAAW,CAAA;AACN,IAAA,UAAU;AACV,IAAA,WAAW;IAEnB,YAAY,GAAG,CAAC;AAExB,IAAA,WAAA,CAAmB,UAA8B,EAAE,EAAA;QACjD,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,CAAC,GAAG,EAAE;AAC/C,QAAA,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,CAAC;AAE5C,QAAA,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,UAAU,IAAI,CAAC,EAAE;AACnD,YAAA,MAAM,IAAI,UAAU,CAAC,sEAAsE,UAAU,CAAA,CAAA,CAAG,CAAC;QAC3G;AAEA,QAAA,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,IAAI,WAAW,GAAG,CAAC,EAAE;AACrD,YAAA,MAAM,IAAI,UAAU,CAAC,6DAA6D,WAAW,CAAA,CAAA,CAAG,CAAC;QACnG;AAEA,QAAA,IAAI,CAAC,UAAU,GAAG,UAAU;AAC5B,QAAA,IAAI,CAAC,WAAW,GAAG,WAAW;IAChC;;AAGA,IAAA,IAAW,WAAW,GAAA;QACpB,OAAO,IAAI,CAAC,YAAY;IAC1B;;AAGA,IAAA,IAAW,KAAK,GAAA;QACd,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC;IACtE;AAEA;;;;AAIG;AACI,IAAA,OAAO,CAAC,iBAAyB,EAAA;AACtC,QAAA,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAC,IAAI,iBAAiB,IAAI,CAAC,EAAE;AACjE,YAAA,OAAO,CAAC;QACV;AAEA,QAAA,IAAI,CAAC,YAAY,IAAI,iBAAiB;AAEtC,QAAA,IAAI,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,UAAU,CAAC;AAE3D,QAAA,IAAI,KAAK,IAAI,CAAC,EAAE;AACd,YAAA,OAAO,CAAC;QACV;AAEA,QAAA,IAAI,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE;AAC5B,YAAA,KAAK,GAAG,IAAI,CAAC,WAAW;QAC1B;QAEA,IAAI,CAAC,YAAY,IAAI,KAAK,GAAG,IAAI,CAAC,UAAU;;QAG5C,IAAI,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,UAAU,EAAE;AACxC,YAAA,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,UAAU;QACtC;AAEA,QAAA,OAAO,KAAK;IACd;;IAGO,KAAK,GAAA;AACV,QAAA,IAAI,CAAC,YAAY,GAAG,CAAC;IACvB;AACD;;;;"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CandidatePair } from '../broadphase/BroadPhase';
|
|
2
|
+
import type { Collider } from '../Collider';
|
|
3
|
+
import { ContactGraph } from '../ContactGraph';
|
|
4
|
+
import type { PhysicsBackend } from './PhysicsBackend';
|
|
5
|
+
/**
|
|
6
|
+
* The native, dependency-free backend: a {@link BroadPhase} (sweep-and-prune in
|
|
7
|
+
* the MVP, swappable for a dynamic AABB tree later) feeding the
|
|
8
|
+
* {@link ContactGraph} that diffs touching pairs into events. The dynamics
|
|
9
|
+
* solver plugs in here in a later phase without changing the frontend.
|
|
10
|
+
*/
|
|
11
|
+
export declare class NativePhysicsBackend implements PhysicsBackend {
|
|
12
|
+
readonly contactGraph: ContactGraph;
|
|
13
|
+
private readonly _broadPhase;
|
|
14
|
+
private readonly _pairs;
|
|
15
|
+
get candidatePairs(): readonly CandidatePair[];
|
|
16
|
+
detect(colliders: readonly Collider[]): void;
|
|
17
|
+
removeCollider(collider: Collider): void;
|
|
18
|
+
destroy(): void;
|
|
19
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { SweepAndPrune } from '../broadphase/SweepAndPrune.js';
|
|
2
|
+
import { ContactGraph } from '../ContactGraph.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The native, dependency-free backend: a {@link BroadPhase} (sweep-and-prune in
|
|
6
|
+
* the MVP, swappable for a dynamic AABB tree later) feeding the
|
|
7
|
+
* {@link ContactGraph} that diffs touching pairs into events. The dynamics
|
|
8
|
+
* solver plugs in here in a later phase without changing the frontend.
|
|
9
|
+
*/
|
|
10
|
+
class NativePhysicsBackend {
|
|
11
|
+
contactGraph = new ContactGraph();
|
|
12
|
+
_broadPhase = new SweepAndPrune();
|
|
13
|
+
_pairs = [];
|
|
14
|
+
get candidatePairs() {
|
|
15
|
+
return this._pairs;
|
|
16
|
+
}
|
|
17
|
+
detect(colliders) {
|
|
18
|
+
this._broadPhase.computePairs(colliders, this._pairs);
|
|
19
|
+
this.contactGraph.update(this._pairs);
|
|
20
|
+
}
|
|
21
|
+
removeCollider(collider) {
|
|
22
|
+
this.contactGraph.removeCollider(collider);
|
|
23
|
+
}
|
|
24
|
+
destroy() {
|
|
25
|
+
this.contactGraph.clear();
|
|
26
|
+
this._pairs.length = 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { NativePhysicsBackend };
|
|
31
|
+
//# sourceMappingURL=NativePhysicsBackend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NativePhysicsBackend.js","sources":["../../../../src/backend/NativePhysicsBackend.ts"],"sourcesContent":[null],"names":[],"mappings":";;;AAMA;;;;;AAKG;MACU,oBAAoB,CAAA;AACf,IAAA,YAAY,GAAG,IAAI,YAAY,EAAE;AAEhC,IAAA,WAAW,GAAe,IAAI,aAAa,EAAE;IAC7C,MAAM,GAAoB,EAAE;AAE7C,IAAA,IAAW,cAAc,GAAA;QACvB,OAAO,IAAI,CAAC,MAAM;IACpB;AAEO,IAAA,MAAM,CAAC,SAA8B,EAAA;QAC1C,IAAI,CAAC,WAAW,CAAC,YAAY,CAAC,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC;QACrD,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;IACvC;AAEO,IAAA,cAAc,CAAC,QAAkB,EAAA;AACtC,QAAA,IAAI,CAAC,YAAY,CAAC,cAAc,CAAC,QAAQ,CAAC;IAC5C;IAEO,OAAO,GAAA;AACZ,QAAA,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE;AACzB,QAAA,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC;IACxB;AACD;;;;"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { CandidatePair } from '../broadphase/BroadPhase';
|
|
2
|
+
import type { Collider } from '../Collider';
|
|
3
|
+
import type { ContactGraph } from '../ContactGraph';
|
|
4
|
+
/**
|
|
5
|
+
* Internal world-level backend seam. The frontend {@link PhysicsWorld} delegates
|
|
6
|
+
* collision detection to a single backend; binding, debug draw and queries stay
|
|
7
|
+
* in the frontend so they work over any backend. In the MVP the only backend is
|
|
8
|
+
* {@link NativePhysicsBackend}. This boundary is intentionally **not** a public,
|
|
9
|
+
* stable contract — promoting it (or adding a Rapier backend) is a deliberate
|
|
10
|
+
* later decision (spec 00 §11), so it lives outside the published surface.
|
|
11
|
+
*/
|
|
12
|
+
export interface PhysicsBackend {
|
|
13
|
+
/** The persistent contact set, source of the begin/end/sensor events. */
|
|
14
|
+
readonly contactGraph: ContactGraph;
|
|
15
|
+
/** The latest broad-phase candidate pairs (read-only; for debug draw). */
|
|
16
|
+
readonly candidatePairs: readonly CandidatePair[];
|
|
17
|
+
/** Run one detection pass over `colliders`, refreshing the contact graph. */
|
|
18
|
+
detect(colliders: readonly Collider[]): void;
|
|
19
|
+
/** Forget any state referencing `collider` (called on destruction). */
|
|
20
|
+
removeCollider(collider: Collider): void;
|
|
21
|
+
/** Release all backend state. */
|
|
22
|
+
destroy(): void;
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SceneNode } from '@codexo/exojs';
|
|
2
|
+
import type { PhysicsBody } from '../PhysicsBody';
|
|
3
|
+
import type { BindingOptions } from './PhysicsBinding';
|
|
4
|
+
import { PhysicsBinding } from './PhysicsBinding';
|
|
5
|
+
/**
|
|
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
|
+
export declare class BindingRegistry {
|
|
10
|
+
private readonly _bindings;
|
|
11
|
+
/** Link `body` to `node`. Rejects nodes with non-zero skew; runtime scale is ignored. */
|
|
12
|
+
bind(body: PhysicsBody, node: SceneNode, options?: BindingOptions): PhysicsBinding;
|
|
13
|
+
/** Remove the link for `body`, if any. */
|
|
14
|
+
unbind(body: PhysicsBody): void;
|
|
15
|
+
/** Write every bound node's transform from its body. */
|
|
16
|
+
sync(): void;
|
|
17
|
+
/** `true` when at least one binding exists. */
|
|
18
|
+
get size(): number;
|
|
19
|
+
/** Drop all links. */
|
|
20
|
+
clear(): void;
|
|
21
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { PhysicsBinding } from './PhysicsBinding.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Owns the body ↔ node links and writes bound node transforms after each step.
|
|
5
|
+
* Backend-agnostic (frontend-level) — it reads only the public body transform.
|
|
6
|
+
*/
|
|
7
|
+
class BindingRegistry {
|
|
8
|
+
_bindings = new Map();
|
|
9
|
+
/** Link `body` to `node`. Rejects nodes with non-zero skew; runtime scale is ignored. */
|
|
10
|
+
bind(body, node, options) {
|
|
11
|
+
if (node.skewX !== 0 || node.skewY !== 0) {
|
|
12
|
+
throw new Error(`PhysicsBinding: the bound node has non-zero skew (${node.skewX}°, ${node.skewY}°); skewed nodes are not supported.`);
|
|
13
|
+
}
|
|
14
|
+
const binding = new PhysicsBinding(body, node, options?.drive ?? 'body-to-node');
|
|
15
|
+
this._bindings.set(body, binding);
|
|
16
|
+
binding.sync();
|
|
17
|
+
return binding;
|
|
18
|
+
}
|
|
19
|
+
/** Remove the link for `body`, if any. */
|
|
20
|
+
unbind(body) {
|
|
21
|
+
this._bindings.delete(body);
|
|
22
|
+
}
|
|
23
|
+
/** Write every bound node's transform from its body. */
|
|
24
|
+
sync() {
|
|
25
|
+
for (const binding of this._bindings.values()) {
|
|
26
|
+
binding.sync();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** `true` when at least one binding exists. */
|
|
30
|
+
get size() {
|
|
31
|
+
return this._bindings.size;
|
|
32
|
+
}
|
|
33
|
+
/** Drop all links. */
|
|
34
|
+
clear() {
|
|
35
|
+
this._bindings.clear();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { BindingRegistry };
|
|
40
|
+
//# sourceMappingURL=BindingRegistry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BindingRegistry.js","sources":["../../../../src/binding/BindingRegistry.ts"],"sourcesContent":[null],"names":[],"mappings":";;AAMA;;;AAGG;MACU,eAAe,CAAA;AACT,IAAA,SAAS,GAAG,IAAI,GAAG,EAA+B;;AAG5D,IAAA,IAAI,CAAC,IAAiB,EAAE,IAAe,EAAE,OAAwB,EAAA;AACtE,QAAA,IAAI,IAAI,CAAC,KAAK,KAAK,CAAC,IAAI,IAAI,CAAC,KAAK,KAAK,CAAC,EAAE;AACxC,YAAA,MAAM,IAAI,KAAK,CAAC,CAAA,kDAAA,EAAqD,IAAI,CAAC,KAAK,CAAA,GAAA,EAAM,IAAI,CAAC,KAAK,CAAA,mCAAA,CAAqC,CAAC;QACvI;AAEA,QAAA,MAAM,OAAO,GAAG,IAAI,cAAc,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,cAAc,CAAC;QAEhF,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC;QACjC,OAAO,CAAC,IAAI,EAAE;AAEd,QAAA,OAAO,OAAO;IAChB;;AAGO,IAAA,MAAM,CAAC,IAAiB,EAAA;AAC7B,QAAA,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC;IAC7B;;IAGO,IAAI,GAAA;QACT,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE;YAC7C,OAAO,CAAC,IAAI,EAAE;QAChB;IACF;;AAGA,IAAA,IAAW,IAAI,GAAA;AACb,QAAA,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI;IAC5B;;IAGO,KAAK,GAAA;AACV,QAAA,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE;IACxB;AACD;;;;"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { SceneNode } from '@codexo/exojs';
|
|
2
|
+
import type { PhysicsBody } from '../PhysicsBody';
|
|
3
|
+
/** Options for {@link PhysicsWorld.bind}. */
|
|
4
|
+
export interface BindingOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Which way the transform flows. `'body-to-node'` (default) writes the body's
|
|
7
|
+
* transform onto the node each step — for dynamic and kinematic-position
|
|
8
|
+
* bodies. `'node-to-body'` (read the node, drive the body) is reserved for the
|
|
9
|
+
* kinematic-velocity follow-up and currently behaves like `'body-to-node'`.
|
|
10
|
+
*/
|
|
11
|
+
drive?: 'body-to-node' | 'node-to-body';
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A link between a {@link PhysicsBody} and a {@link SceneNode}. After each
|
|
15
|
+
* {@link PhysicsWorld.step}, the body's world position is written onto the node.
|
|
16
|
+
* Rotation is written once dynamics ship (a body cannot rotate under contacts in
|
|
17
|
+
* this collision/query release). The node must be world-space-rooted; runtime
|
|
18
|
+
* scale is ignored and non-zero skew is rejected at bind time.
|
|
19
|
+
*/
|
|
20
|
+
export declare class PhysicsBinding {
|
|
21
|
+
readonly body: PhysicsBody;
|
|
22
|
+
readonly node: SceneNode;
|
|
23
|
+
readonly drive: 'body-to-node' | 'node-to-body';
|
|
24
|
+
constructor(body: PhysicsBody, node: SceneNode, drive?: 'body-to-node' | 'node-to-body');
|
|
25
|
+
/** Write the body's current transform onto the bound node. */
|
|
26
|
+
sync(): void;
|
|
27
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A link between a {@link PhysicsBody} and a {@link SceneNode}. After each
|
|
3
|
+
* {@link PhysicsWorld.step}, the body's world position is written onto the node.
|
|
4
|
+
* Rotation is written once dynamics ship (a body cannot rotate under contacts in
|
|
5
|
+
* this collision/query release). The node must be world-space-rooted; runtime
|
|
6
|
+
* scale is ignored and non-zero skew is rejected at bind time.
|
|
7
|
+
*/
|
|
8
|
+
class PhysicsBinding {
|
|
9
|
+
body;
|
|
10
|
+
node;
|
|
11
|
+
drive;
|
|
12
|
+
constructor(body, node, drive = 'body-to-node') {
|
|
13
|
+
this.body = body;
|
|
14
|
+
this.node = node;
|
|
15
|
+
this.drive = drive;
|
|
16
|
+
}
|
|
17
|
+
/** Write the body's current transform onto the bound node. */
|
|
18
|
+
sync() {
|
|
19
|
+
this.node.setPosition(this.body.x, this.body.y);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { PhysicsBinding };
|
|
24
|
+
//# sourceMappingURL=PhysicsBinding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PhysicsBinding.js","sources":["../../../../src/binding/PhysicsBinding.ts"],"sourcesContent":[null],"names":[],"mappings":"AAeA;;;;;;AAMG;MACU,cAAc,CAAA;AAEP,IAAA,IAAA;AACA,IAAA,IAAA;AACA,IAAA,KAAA;AAHlB,IAAA,WAAA,CACkB,IAAiB,EACjB,IAAe,EACf,QAAyC,cAAc,EAAA;QAFvD,IAAA,CAAA,IAAI,GAAJ,IAAI;QACJ,IAAA,CAAA,IAAI,GAAJ,IAAI;QACJ,IAAA,CAAA,KAAK,GAAL,KAAK;IACpB;;IAGI,IAAI,GAAA;AACT,QAAA,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IACjD;AACD;;;;"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Collider } from '../Collider';
|
|
2
|
+
/** An unordered pair of colliders whose AABBs the broad phase reports as overlapping. */
|
|
3
|
+
export interface CandidatePair {
|
|
4
|
+
a: Collider;
|
|
5
|
+
b: Collider;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Broad-phase contract: reduce the O(n²) all-pairs test to a candidate set of
|
|
9
|
+
* AABB-overlapping pairs. The candidate set must contain **every** truly
|
|
10
|
+
* overlapping pair (zero false negatives, gate B-3); false positives are
|
|
11
|
+
* resolved by the narrow phase. This interface is the seam behind which the MVP
|
|
12
|
+
* ships {@link SweepAndPrune} and a future dynamic-AABB-tree can be swapped in
|
|
13
|
+
* without touching callers.
|
|
14
|
+
*/
|
|
15
|
+
export interface BroadPhase {
|
|
16
|
+
/**
|
|
17
|
+
* Fill `out` with the candidate pairs for the current collider set and return
|
|
18
|
+
* it. Each pair is ordered `a.id < b.id`, and the array is sorted by
|
|
19
|
+
* `(a.id, b.id)` for deterministic downstream processing.
|
|
20
|
+
*/
|
|
21
|
+
computePairs(colliders: readonly Collider[], out: CandidatePair[]): CandidatePair[];
|
|
22
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Collider } from '../Collider';
|
|
2
|
+
import type { BroadPhase, CandidatePair } from './BroadPhase';
|
|
3
|
+
/**
|
|
4
|
+
* Sort-and-sweep broad phase on the X axis. Each query sorts the live colliders
|
|
5
|
+
* by their AABB `minX`, then sweeps once, comparing each collider only against
|
|
6
|
+
* those whose X intervals still overlap and confirming the Y interval. This is
|
|
7
|
+
* an exact AABB-overlap broad phase (zero false negatives) with a deterministic,
|
|
8
|
+
* id-sorted output. The recompute-each-step design keeps it stateless between
|
|
9
|
+
* frames — two worlds never share sweep state (gate I-1).
|
|
10
|
+
*/
|
|
11
|
+
export declare class SweepAndPrune implements BroadPhase {
|
|
12
|
+
private readonly _sorted;
|
|
13
|
+
computePairs(colliders: readonly Collider[], out: CandidatePair[]): CandidatePair[];
|
|
14
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sort-and-sweep broad phase on the X axis. Each query sorts the live colliders
|
|
3
|
+
* by their AABB `minX`, then sweeps once, comparing each collider only against
|
|
4
|
+
* those whose X intervals still overlap and confirming the Y interval. This is
|
|
5
|
+
* an exact AABB-overlap broad phase (zero false negatives) with a deterministic,
|
|
6
|
+
* id-sorted output. The recompute-each-step design keeps it stateless between
|
|
7
|
+
* frames — two worlds never share sweep state (gate I-1).
|
|
8
|
+
*/
|
|
9
|
+
class SweepAndPrune {
|
|
10
|
+
_sorted = [];
|
|
11
|
+
computePairs(colliders, out) {
|
|
12
|
+
out.length = 0;
|
|
13
|
+
const sorted = this._sorted;
|
|
14
|
+
sorted.length = 0;
|
|
15
|
+
for (const collider of colliders) {
|
|
16
|
+
sorted.push(collider);
|
|
17
|
+
}
|
|
18
|
+
sorted.sort(byMinX);
|
|
19
|
+
const count = sorted.length;
|
|
20
|
+
for (let i = 0; i < count; i++) {
|
|
21
|
+
const a = sorted[i];
|
|
22
|
+
const aBox = a.aabb;
|
|
23
|
+
const aMaxX = aBox.maxX;
|
|
24
|
+
const aMinY = aBox.minY;
|
|
25
|
+
const aMaxY = aBox.maxY;
|
|
26
|
+
for (let j = i + 1; j < count; j++) {
|
|
27
|
+
const b = sorted[j];
|
|
28
|
+
const bBox = b.aabb;
|
|
29
|
+
// X intervals are sorted: once b starts past a's right edge, so do all
|
|
30
|
+
// later colliders — stop scanning this i.
|
|
31
|
+
if (bBox.minX > aMaxX) {
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
if (bBox.minY > aMaxY || bBox.maxY < aMinY) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
out.push(a.id < b.id ? { a, b } : { a: b, b: a });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
out.sort(byPairId);
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const byMinX = (a, b) => a.aabb.minX - b.aabb.minX || a.id - b.id;
|
|
45
|
+
const byPairId = (p, q) => p.a.id - q.a.id || p.b.id - q.b.id;
|
|
46
|
+
|
|
47
|
+
export { SweepAndPrune };
|
|
48
|
+
//# sourceMappingURL=SweepAndPrune.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SweepAndPrune.js","sources":["../../../../src/broadphase/SweepAndPrune.ts"],"sourcesContent":[null],"names":[],"mappings":"AAGA;;;;;;;AAOG;MACU,aAAa,CAAA;IACP,OAAO,GAAe,EAAE;IAElC,YAAY,CAAC,SAA8B,EAAE,GAAoB,EAAA;AACtE,QAAA,GAAG,CAAC,MAAM,GAAG,CAAC;AAEd,QAAA,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO;AAC3B,QAAA,MAAM,CAAC,MAAM,GAAG,CAAC;AAEjB,QAAA,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE;AAChC,YAAA,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC;QACvB;AAEA,QAAA,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;AAEnB,QAAA,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM;AAE3B,QAAA,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE;AAC9B,YAAA,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC;AACnB,YAAA,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI;AACnB,YAAA,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI;AACvB,YAAA,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI;AACvB,YAAA,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI;AAEvB,YAAA,KAAK,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE;AAClC,gBAAA,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC;AACnB,gBAAA,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI;;;AAInB,gBAAA,IAAI,IAAI,CAAC,IAAI,GAAG,KAAK,EAAE;oBACrB;gBACF;AAEA,gBAAA,IAAI,IAAI,CAAC,IAAI,GAAG,KAAK,IAAI,IAAI,CAAC,IAAI,GAAG,KAAK,EAAE;oBAC1C;gBACF;AAEA,gBAAA,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;YACnD;QACF;AAEA,QAAA,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC;AAElB,QAAA,OAAO,GAAG;IACZ;AACD;AAED,MAAM,MAAM,GAAG,CAAC,CAAW,EAAE,CAAW,KAAa,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE;AAE7F,MAAM,QAAQ,GAAG,CAAC,CAAgB,EAAE,CAAgB,KAAa,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE;;;;"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Mutable2D } from '../math';
|
|
2
|
+
import type { Shape } from '../shapes/Shape';
|
|
3
|
+
/**
|
|
4
|
+
* The geometric surface the narrow phase needs: a shape plus its cached world
|
|
5
|
+
* data. {@link Collider} satisfies this structurally, and shape-overlap queries
|
|
6
|
+
* build a throwaway proxy to test an arbitrary shape against the world without
|
|
7
|
+
* allocating a body/collider.
|
|
8
|
+
*/
|
|
9
|
+
export interface CollisionProxy {
|
|
10
|
+
readonly shape: Shape;
|
|
11
|
+
readonly worldCenter: Readonly<Mutable2D>;
|
|
12
|
+
readonly worldVertices: readonly number[];
|
|
13
|
+
readonly worldNormals: readonly number[];
|
|
14
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** A single point of a {@link Manifold}. */
|
|
2
|
+
export interface ManifoldPoint {
|
|
3
|
+
/** World-space contact position X. */
|
|
4
|
+
x: number;
|
|
5
|
+
/** World-space contact position Y. */
|
|
6
|
+
y: number;
|
|
7
|
+
/** Penetration depth in px (≥ 0). */
|
|
8
|
+
penetration: number;
|
|
9
|
+
/**
|
|
10
|
+
* Stable feature id identifying which geometric features produced this point.
|
|
11
|
+
* Constant across frames while the contact features are unchanged — the basis
|
|
12
|
+
* for warm-starting the solver later (gate B-2 / SG-M*).
|
|
13
|
+
*/
|
|
14
|
+
id: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A contact manifold: the collision normal (oriented from collider A toward
|
|
18
|
+
* collider B) plus 1–2 contact points. A `Manifold` is reused across narrow-phase
|
|
19
|
+
* calls — it preallocates its two points and `reset()`s `pointCount` to 0. The
|
|
20
|
+
* generation here is forward-looking work for the dynamics solver; in this
|
|
21
|
+
* collision/query release it drives debug draw and validates the narrow phase.
|
|
22
|
+
*/
|
|
23
|
+
export declare class Manifold {
|
|
24
|
+
/** Collision normal X (unit, A → B). */
|
|
25
|
+
normalX: number;
|
|
26
|
+
/** Collision normal Y (unit, A → B). */
|
|
27
|
+
normalY: number;
|
|
28
|
+
/** Number of valid entries in {@link points} (0, 1 or 2). */
|
|
29
|
+
pointCount: number;
|
|
30
|
+
readonly points: readonly [ManifoldPoint, ManifoldPoint];
|
|
31
|
+
/** Clear the manifold for reuse. */
|
|
32
|
+
reset(): void;
|
|
33
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A contact manifold: the collision normal (oriented from collider A toward
|
|
3
|
+
* collider B) plus 1–2 contact points. A `Manifold` is reused across narrow-phase
|
|
4
|
+
* calls — it preallocates its two points and `reset()`s `pointCount` to 0. The
|
|
5
|
+
* generation here is forward-looking work for the dynamics solver; in this
|
|
6
|
+
* collision/query release it drives debug draw and validates the narrow phase.
|
|
7
|
+
*/
|
|
8
|
+
class Manifold {
|
|
9
|
+
/** Collision normal X (unit, A → B). */
|
|
10
|
+
normalX = 0;
|
|
11
|
+
/** Collision normal Y (unit, A → B). */
|
|
12
|
+
normalY = 0;
|
|
13
|
+
/** Number of valid entries in {@link points} (0, 1 or 2). */
|
|
14
|
+
pointCount = 0;
|
|
15
|
+
points = [
|
|
16
|
+
{ x: 0, y: 0, penetration: 0, id: 0 },
|
|
17
|
+
{ x: 0, y: 0, penetration: 0, id: 0 },
|
|
18
|
+
];
|
|
19
|
+
/** Clear the manifold for reuse. */
|
|
20
|
+
reset() {
|
|
21
|
+
this.pointCount = 0;
|
|
22
|
+
this.normalX = 0;
|
|
23
|
+
this.normalY = 0;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { Manifold };
|
|
28
|
+
//# sourceMappingURL=Manifold.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Manifold.js","sources":["../../../../src/collision/Manifold.ts"],"sourcesContent":[null],"names":[],"mappings":"AAgBA;;;;;;AAMG;MACU,QAAQ,CAAA;;IAEZ,OAAO,GAAG,CAAC;;IAEX,OAAO,GAAG,CAAC;;IAEX,UAAU,GAAG,CAAC;AAEL,IAAA,MAAM,GAA4C;AAChE,QAAA,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE;AACrC,QAAA,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE;KACtC;;IAGM,KAAK,GAAA;AACV,QAAA,IAAI,CAAC,UAAU,GAAG,CAAC;AACnB,QAAA,IAAI,CAAC,OAAO,GAAG,CAAC;AAChB,QAAA,IAAI,CAAC,OAAO,GAAG,CAAC;IAClB;AACD;;;;"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CollisionProxy } from './CollisionProxy';
|
|
2
|
+
import type { Manifold } from './Manifold';
|
|
3
|
+
/**
|
|
4
|
+
* Generate the contact manifold for `a` vs `b`, writing into `manifold` and
|
|
5
|
+
* returning `true` when the colliders touch. The manifold normal is oriented
|
|
6
|
+
* from `a` toward `b`. Dispatches on shape pair; polygon/circle is handled by
|
|
7
|
+
* the circle/polygon routine with a `flip` so the normal stays `a → b`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const collide: (a: CollisionProxy, b: CollisionProxy, manifold: Manifold) => boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Boolean overlap test (no manifold) used by sensors and shape-overlap queries.
|
|
12
|
+
* Exact for the circle/circle, circle/polygon and polygon/polygon pairs.
|
|
13
|
+
*/
|
|
14
|
+
export declare const testOverlap: (a: CollisionProxy, b: CollisionProxy) => boolean;
|