ecspresso
Version:
A minimal Entity-Component-System library for typescript and javascript.
131 lines (130 loc) • 5.76 kB
TypeScript
/**
* Shared Narrowphase Module — 3D
*
* Provides contact-computing narrowphase tests and a generic collision
* iteration pipeline for 3D collider pairs (AABB3D + Sphere).
*
* Mirrors the 2D narrowphase (`narrowphase.ts`) with an added Z axis.
*/
import type { SpatialIndex3D } from './spatial-hash3D';
/**
* Contact result from a 3D narrowphase test. Normal points from A toward B.
*
* Narrowphase functions use this as an out-parameter: the caller owns the
* struct, the function writes fields in place and returns `true` on hit.
* The `onContact` callback in `detectCollisions3D` receives a shared module-
* level instance — **subscribers must consume it synchronously and must not
* retain the reference across frames**.
*/
export interface Contact3D {
normalX: number;
normalY: number;
normalZ: number;
/** Penetration depth (positive = overlapping) */
depth: number;
}
/** Collider shape discriminator for the flattened BaseColliderInfo3D layout. */
export declare const AABB3D_SHAPE = 0;
export declare const SPHERE_SHAPE = 1;
export type ColliderShape3D = typeof AABB3D_SHAPE | typeof SPHERE_SHAPE;
/**
* Minimum collider data shared by 3D collision and physics bundles.
*
* Flat layout (no nested sub-objects): the `shape` discriminator tells you
* whether to read `halfWidth`/`halfHeight`/`halfDepth` (AABB3D) or `radius`
* (Sphere). Unused fields are set to 0.
*
* Pool-friendly — all fields are assigned in place each frame.
*/
export interface BaseColliderInfo3D<L extends string = string> {
entityId: number;
x: number;
y: number;
z: number;
layer: L;
collidesWith: readonly L[];
/**
* Bit assigned to `layer` from the lazy layer registry. Populated by
* `fillBaseColliderInfo3D`. Used together with `collidesWithMask` to
* replace per-pair `Array.includes` layer checks with a single AND.
*/
layerBit: number;
/** OR of `getLayerBit3D` for every entry in `collidesWith`. */
collidesWithMask: number;
shape: ColliderShape3D;
halfWidth: number;
halfHeight: number;
halfDepth: number;
radius: number;
}
export declare const getLayerBit3D: (layer: string) => number;
export declare const getCollidesWithMask3D: (collidesWith: readonly string[]) => number;
/**
* Populate a `BaseColliderInfo3D` slot in place from raw component data.
* Returns `true` if the slot was filled, `false` if the entity has no
* collider (caller should skip it).
*
* If an entity has both AABB3D and sphere colliders, AABB3D wins and only
* the AABB3D offset is applied.
*/
export declare function fillBaseColliderInfo3D<L extends string>(info: BaseColliderInfo3D<L>, entityId: number, x: number, y: number, z: number, layer: L, collidesWith: readonly L[], aabb3D: {
width: number;
height: number;
depth: number;
offsetX?: number;
offsetY?: number;
offsetZ?: number;
} | undefined, sphere: {
radius: number;
offsetX?: number;
offsetY?: number;
offsetZ?: number;
} | undefined): boolean;
/**
* Retrieve the optional spatialIndex3D resource, returning undefined when absent.
* Centralizes the cross-plugin typed lookup so individual plugins don't each
* need to import SpatialIndex3D or repeat the tryGetResource pattern.
*/
export declare function tryGetSpatialIndex3D(tryGetResource: <T>(key: string) => T | undefined): SpatialIndex3D | undefined;
/**
* Write an AABB3D-vs-AABB3D contact into `out`. Returns `true` if the
* shapes overlap (out was filled), `false` otherwise.
*
* Resolves along the axis with minimum penetration depth.
*/
export declare function computeAABB3DvsAABB3D(ax: number, ay: number, az: number, ahw: number, ahh: number, ahd: number, bx: number, by: number, bz: number, bhw: number, bhh: number, bhd: number, out: Contact3D): boolean;
/**
* Write a sphere-vs-sphere contact into `out`. Returns `true` if the
* spheres overlap.
*/
export declare function computeSphereVsSphere(ax: number, ay: number, az: number, ar: number, bx: number, by: number, bz: number, br: number, out: Contact3D): boolean;
/**
* Write an AABB3D-vs-sphere contact into `out`. Returns `true` if the
* shapes overlap.
*
* Uses closest-point-on-AABB to sphere center. When the sphere center
* is inside the AABB, resolves along the axis with minimum push distance.
*/
export declare function computeAABB3DvsSphere(aabbX: number, aabbY: number, aabbZ: number, ahw: number, ahh: number, ahd: number, sphereX: number, sphereY: number, sphereZ: number, radius: number, out: Contact3D): boolean;
/**
* Dispatch to the correct narrowphase function for the given pair and
* write the contact into `out`. Returns `true` if the pair overlaps.
*/
export declare function computeContact3D(a: BaseColliderInfo3D, b: BaseColliderInfo3D, out: Contact3D): boolean;
/**
* Generic 3D collision detection pipeline: brute-force or broadphase,
* with layer filtering and contact computation.
*
* `count` is the number of live entries at the front of `colliders`.
* The array itself may be a grow-only pool — only indices `[0, count)`
* are iterated, so trailing pool slots are ignored.
*
* `workingMap` is a caller-owned `Map<number, I>` used by the broadphase
* path as an entityId → collider lookup. It is cleared and repopulated on
* each call; callers should allocate it once and pass the same instance
* every frame.
*
* Uses a context parameter forwarded to the callback to avoid
* per-frame closure allocation.
*/
export declare function detectCollisions3D<I extends BaseColliderInfo3D, C>(colliders: I[], count: number, workingMap: Map<number, I>, spatialIndex: SpatialIndex3D | undefined, onContact: (a: I, b: I, contact: Contact3D, context: C) => void, context: C): void;