UNPKG

ecspresso

Version:

A minimal Entity-Component-System library for typescript and javascript.

130 lines (129 loc) 5.89 kB
/** * 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;