ecspresso
Version:
A minimal Entity-Component-System library for typescript and javascript.
130 lines (129 loc) • 5.89 kB
TypeScript
/**
* Shared Narrowphase Module
*
* Provides contact-computing narrowphase tests and a generic collision
* iteration pipeline used by both the collision plugin (event-only) and
* the physics2D plugin (impulse response).
*/
import type { SpatialIndex } from './spatial-hash';
/**
* Contact result from a 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 `detectCollisions` receives a shared module-
* level instance — **subscribers must consume it synchronously and must not
* retain the reference across frames**.
*/
export interface Contact {
normalX: number;
normalY: number;
/** Penetration depth (positive = overlapping) */
depth: number;
}
/** Collider shape discriminator for the flattened BaseColliderInfo layout. */
export declare const AABB_SHAPE = 0;
export declare const CIRCLE_SHAPE = 1;
export type ColliderShape = typeof AABB_SHAPE | typeof CIRCLE_SHAPE;
/**
* Minimum collider data shared by collision and physics bundles.
*
* Flat layout (no nested `aabb` / `circle` sub-objects): the `shape`
* discriminator tells you whether to read `halfWidth`/`halfHeight`
* (AABB) or `radius` (Circle). Unused fields are set to 0.
*
* This shape is pool-friendly — all fields are assigned in place each
* frame without allocating nested objects.
*/
export interface BaseColliderInfo<L extends string = string> {
entityId: number;
x: number;
y: number;
layer: L;
collidesWith: readonly L[];
/**
* Bit assigned to `layer` from the lazy layer registry. Populated by
* `fillBaseColliderInfo`. Used together with `collidesWithMask` to
* replace per-pair `Array.includes` layer checks with a single AND.
*/
layerBit: number;
/** OR of `getLayerBit` for every entry in `collidesWith`. */
collidesWithMask: number;
shape: ColliderShape;
halfWidth: number;
halfHeight: number;
radius: number;
}
export declare const getLayerBit: (layer: string) => number;
export declare const getCollidesWithMask: (collidesWith: readonly string[]) => number;
/**
* Populate a `BaseColliderInfo` 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 AABB and circle colliders, AABB wins and only
* the AABB offset is applied. This matches the dispatch precedence in
* `computeContact`; the previous implementation stacked both offsets,
* which was a bug.
*/
export declare function fillBaseColliderInfo<L extends string>(info: BaseColliderInfo<L>, entityId: number, x: number, y: number, layer: L, collidesWith: readonly L[], aabb: {
width: number;
height: number;
offsetX?: number;
offsetY?: number;
} | undefined, circle: {
radius: number;
offsetX?: number;
offsetY?: number;
} | undefined): boolean;
/**
* Retrieve the optional spatialIndex resource, returning undefined when absent.
* Centralizes the cross-plugin typed lookup so individual plugins don't each
* need to import SpatialIndex or repeat the tryGetResource pattern.
*/
export declare function tryGetSpatialIndex(tryGetResource: <T>(key: string) => T | undefined): SpatialIndex | undefined;
/**
* Write an AABB-AABB contact into `out`. Returns `true` if the shapes
* overlap (out was filled), `false` otherwise.
*/
export declare function computeAABBvsAABB(ax: number, ay: number, ahw: number, ahh: number, bx: number, by: number, bhw: number, bhh: number, out: Contact): boolean;
export declare function computeCircleVsCircle(ax: number, ay: number, ar: number, bx: number, by: number, br: number, out: Contact): boolean;
export declare function computeAABBvsCircle(aabbX: number, aabbY: number, ahw: number, ahh: number, circleX: number, circleY: number, radius: number, out: Contact): 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 computeContact(a: BaseColliderInfo, b: BaseColliderInfo, out: Contact): boolean;
/**
* Per-caller scratch for the broadphase entityId → collider lookup.
*
* Dense `arr` indexed by entityId, paired with a `gen` stamp array that marks
* which slots are live this call. Bumping `current` invalidates all prior
* entries without clearing — replaces the per-frame `Map.clear()` + N
* `Map.set()` allocation churn that a `Map<number, I>` would incur.
*
* Owned per plugin instance (alongside its `colliderPool`), so concurrent
* worlds don't share state and `I` stays fully typed without erasure.
*/
export interface BroadphaseScratch<I extends BaseColliderInfo> {
arr: (I | undefined)[];
gen: number[];
current: number;
}
export declare function createBroadphaseScratch<I extends BaseColliderInfo>(): BroadphaseScratch<I>;
/**
* Generic 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.
*
* `scratch` is a caller-owned `BroadphaseScratch<I>` used by the broadphase
* path as an entityId → collider lookup. Allocate it once per plugin instance
* and pass the same reference every call.
*
* Uses a context parameter forwarded to the callback to avoid
* per-frame closure allocation.
*/
export declare function detectCollisions<I extends BaseColliderInfo, C>(colliders: I[], count: number, scratch: BroadphaseScratch<I>, spatialIndex: SpatialIndex | undefined, onContact: (a: I, b: I, contact: Contact, context: C) => void, context: C): void;