UNPKG

planck

Version:

2D JavaScript/TypeScript physics engine for cross-platform HTML5 game development

506 lines (434 loc) 14.2 kB
/* * Planck.js * * Copyright (c) Erin Catto, Ali Shakiba * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import * as matrix from "../common/Matrix"; import { options } from "../util/options"; import { Vec2Value } from "../common/Vec2"; import { AABB, RayCastInput, RayCastOutput } from "../collision/AABB"; import { Shape, ShapeType } from "../collision/Shape"; import { Body, MassData } from "./Body"; import { BroadPhase } from "../collision/BroadPhase"; import { TransformValue } from "../common/Transform"; import { Style } from "../util/Testbed"; /** @internal */ const _ASSERT = typeof ASSERT === "undefined" ? false : ASSERT; /** @internal */ const synchronize_aabb1 = new AABB(); /** @internal */ const synchronize_aabb2 = new AABB(); /** @internal */ const displacement = matrix.vec2(0, 0); /** * A fixture definition is used to create a fixture. This class defines an * abstract fixture definition. You can reuse fixture definitions safely. */ export interface FixtureOpt { userData?: unknown; /** * The friction coefficient, usually in the range [0,1] */ friction?: number; /** * The restitution (elasticity) usually in the range [0,1] */ restitution?: number; /** * The density, usually in kg/m^2 */ density?: number; /** * A sensor shape collects contact information but never generates a collision response. */ isSensor?: boolean; /** * Zero, positive or negative collision group. * Fixtures with same positive groupIndex always collide and fixtures with same negative groupIndex never collide. */ filterGroupIndex?: number; /** * Collision category bit or bits that this fixture belongs to. * If groupIndex is zero or not matching, then at least one bit in this fixture categoryBits should match other fixture maskBits and vice versa. */ filterCategoryBits?: number; /** * Collision category bit or bits that this fixture accept for collision. */ filterMaskBits?: number; /** Styling for dev-tools. */ style?: Style; } export interface FixtureDef extends FixtureOpt { shape: Shape; } /** @internal */ const FixtureDefDefault: FixtureOpt = { userData: null, friction: 0.2, restitution: 0.0, density: 0.0, isSensor: false, filterGroupIndex: 0, filterCategoryBits: 0x0001, filterMaskBits: 0xffff, }; /** * This proxy is used internally to connect shape children to the broad-phase. */ export class FixtureProxy { aabb: AABB; fixture: Fixture; childIndex: number; proxyId: number; constructor(fixture: Fixture, childIndex: number) { this.aabb = new AABB(); this.fixture = fixture; this.childIndex = childIndex; // this.proxyId; } } /** * A fixture is used to attach a shape to a body for collision detection. A * fixture inherits its transform from its parent. Fixtures hold additional * non-geometric data such as friction, collision filters, etc. * * To create a new Fixture use {@link Body.createFixture}. */ export class Fixture { /** @internal */ m_body: Body; /** @internal */ m_friction: number; /** @internal */ m_restitution: number; /** @internal */ m_density: number; /** @internal */ m_isSensor: boolean; /** @internal */ m_filterGroupIndex: number; /** @internal */ m_filterCategoryBits: number; /** @internal */ m_filterMaskBits: number; /** @internal */ m_shape: Shape; /** @internal */ m_next: Fixture | null; /** @internal */ m_proxies: FixtureProxy[]; // 0 indicates inactive state, this is not the same as m_proxies.length /** @internal */ m_proxyCount: number; /** @internal */ m_userData: unknown; /** Styling for dev-tools. */ style: Style = {}; /** @hidden @experimental Similar to userData, but used by dev-tools or runtime environment. */ appData: Record<string, any> = {}; constructor(body: Body, def: FixtureDef); constructor(body: Body, shape: Shape, def?: FixtureOpt); constructor(body: Body, shape: Shape, density?: number); /** @internal */ constructor(body: Body, shape?, def?) { if (shape.shape) { def = shape; shape = shape.shape; } else if (typeof def === "number") { def = { density: def }; } def = options(def, FixtureDefDefault); this.m_body = body; this.m_friction = def.friction; this.m_restitution = def.restitution; this.m_density = def.density; this.m_isSensor = def.isSensor; this.m_filterGroupIndex = def.filterGroupIndex; this.m_filterCategoryBits = def.filterCategoryBits; this.m_filterMaskBits = def.filterMaskBits; // TODO validate shape this.m_shape = shape; // .clone(); this.m_next = null; this.m_proxies = []; this.m_proxyCount = 0; // fixture proxies are created here, // but they are activate in when a fixture is added to body const childCount = this.m_shape.getChildCount(); for (let i = 0; i < childCount; ++i) { this.m_proxies[i] = new FixtureProxy(this, i); } this.m_userData = def.userData; if (typeof def.style === "object" && def.style !== null) { this.style = def.style; } } /** @hidden Re-setup fixture. */ _reset(): void { const body = this.getBody(); const broadPhase = body.m_world.m_broadPhase; this.destroyProxies(broadPhase); if (this.m_shape._reset) { this.m_shape._reset(); } const childCount = this.m_shape.getChildCount(); for (let i = 0; i < childCount; ++i) { this.m_proxies[i] = new FixtureProxy(this, i); } this.createProxies(broadPhase, body.m_xf); body.resetMassData(); } /** @hidden */ _serialize(): object { return { friction: this.m_friction, restitution: this.m_restitution, density: this.m_density, isSensor: this.m_isSensor, filterGroupIndex: this.m_filterGroupIndex, filterCategoryBits: this.m_filterCategoryBits, filterMaskBits: this.m_filterMaskBits, shape: this.m_shape, }; } /** @hidden */ static _deserialize(data: any, body: any, restore: any): Fixture { const shape = restore(Shape, data.shape); const fixture = shape && new Fixture(body, shape, data); return fixture; } /** * Get the type of the child shape. You can use this to down cast to the * concrete shape. */ getType(): ShapeType { return this.m_shape.m_type; } /** * Get the child shape. You can modify the child shape, however you should not * change the number of vertices because this will crash some collision caching * mechanisms. Manipulating the shape may lead to non-physical behavior. */ getShape(): Shape { return this.m_shape; } /** * A sensor shape collects contact information but never generates a collision * response. */ isSensor(): boolean { return this.m_isSensor; } /** * Set if this fixture is a sensor. */ setSensor(sensor: boolean): void { if (sensor != this.m_isSensor) { this.m_body.setAwake(true); this.m_isSensor = sensor; } } // /** // * Get the contact filtering data. // */ // getFilterData() { // return this.m_filter; // } /** * Get the user data that was assigned in the fixture definition. Use this to * store your application specific data. */ getUserData(): unknown { return this.m_userData; } /** * Set the user data. Use this to store your application specific data. */ setUserData(data: unknown): void { this.m_userData = data; } /** * Get the parent body of this fixture. This is null if the fixture is not * attached. */ getBody(): Body { return this.m_body; } /** * Get the next fixture in the parent body's fixture list. */ getNext(): Fixture | null { return this.m_next; } /** * Get the density of this fixture. */ getDensity(): number { return this.m_density; } /** * Set the density of this fixture. This will _not_ automatically adjust the * mass of the body. You must call Body.resetMassData to update the body's mass. */ setDensity(density: number): void { if (_ASSERT) console.assert(Number.isFinite(density) && density >= 0.0); this.m_density = density; } /** * Get the coefficient of friction, usually in the range [0,1]. */ getFriction(): number { return this.m_friction; } /** * Set the coefficient of friction. This will not change the friction of * existing contacts. */ setFriction(friction: number): void { this.m_friction = friction; } /** * Get the coefficient of restitution. */ getRestitution(): number { return this.m_restitution; } /** * Set the coefficient of restitution. This will not change the restitution of * existing contacts. */ setRestitution(restitution: number): void { this.m_restitution = restitution; } /** * Test a point in world coordinates for containment in this fixture. */ testPoint(p: Vec2Value): boolean { return this.m_shape.testPoint(this.m_body.getTransform(), p); } /** * Cast a ray against this shape. */ rayCast(output: RayCastOutput, input: RayCastInput, childIndex: number): boolean { return this.m_shape.rayCast(output, input, this.m_body.getTransform(), childIndex); } /** * Get the mass data for this fixture. The mass data is based on the density and * the shape. The rotational inertia is about the shape's origin. This operation * may be expensive. */ getMassData(massData: MassData): void { this.m_shape.computeMass(massData, this.m_density); } /** * Get the fixture's AABB. This AABB may be enlarge and/or stale. If you need a * more accurate AABB, compute it using the shape and the body transform. */ getAABB(childIndex: number): AABB { if (_ASSERT) console.assert(0 <= childIndex && childIndex < this.m_proxies.length); return this.m_proxies[childIndex].aabb; } /** * These support body activation/deactivation. */ createProxies(broadPhase: BroadPhase, xf: TransformValue): void { if (_ASSERT) console.assert(this.m_proxyCount == 0); // Create proxies in the broad-phase. this.m_proxyCount = this.m_shape.getChildCount(); for (let i = 0; i < this.m_proxyCount; ++i) { const proxy = this.m_proxies[i]; this.m_shape.computeAABB(proxy.aabb, xf, i); proxy.proxyId = broadPhase.createProxy(proxy.aabb, proxy); } } destroyProxies(broadPhase: BroadPhase): void { // Destroy proxies in the broad-phase. for (let i = 0; i < this.m_proxyCount; ++i) { const proxy = this.m_proxies[i]; broadPhase.destroyProxy(proxy.proxyId); proxy.proxyId = null; } this.m_proxyCount = 0; } /** * Updates this fixture proxy in broad-phase (with combined AABB of current and * next transformation). */ synchronize(broadPhase: BroadPhase, xf1: TransformValue, xf2: TransformValue): void { for (let i = 0; i < this.m_proxyCount; ++i) { const proxy = this.m_proxies[i]; // Compute an AABB that covers the swept shape (may miss some rotation // effect). this.m_shape.computeAABB(synchronize_aabb1, xf1, proxy.childIndex); this.m_shape.computeAABB(synchronize_aabb2, xf2, proxy.childIndex); proxy.aabb.combine(synchronize_aabb1, synchronize_aabb2); matrix.subVec2(displacement, xf2.p, xf1.p); broadPhase.moveProxy(proxy.proxyId, proxy.aabb, displacement); } } /** * Set the contact filtering data. This will not update contacts until the next * time step when either parent body is active and awake. This automatically * calls refilter. */ setFilterData(filter: { groupIndex: number; categoryBits: number; maskBits: number }): void { this.m_filterGroupIndex = filter.groupIndex; this.m_filterCategoryBits = filter.categoryBits; this.m_filterMaskBits = filter.maskBits; this.refilter(); } getFilterGroupIndex(): number { return this.m_filterGroupIndex; } setFilterGroupIndex(groupIndex: number): void { this.m_filterGroupIndex = groupIndex; this.refilter(); } getFilterCategoryBits(): number { return this.m_filterCategoryBits; } setFilterCategoryBits(categoryBits: number): void { this.m_filterCategoryBits = categoryBits; this.refilter(); } getFilterMaskBits(): number { return this.m_filterMaskBits; } setFilterMaskBits(maskBits: number): void { this.m_filterMaskBits = maskBits; this.refilter(); } /** * Call this if you want to establish collision that was previously disabled by * ContactFilter. */ refilter(): void { if (this.m_body == null) { return; } // Flag associated contacts for filtering. let edge = this.m_body.getContactList(); while (edge) { const contact = edge.contact; const fixtureA = contact.getFixtureA(); const fixtureB = contact.getFixtureB(); if (fixtureA == this || fixtureB == this) { contact.flagForFiltering(); } edge = edge.next; } const world = this.m_body.getWorld(); if (world == null) { return; } // Touch each proxy so that new pairs may be created const broadPhase = world.m_broadPhase; for (let i = 0; i < this.m_proxyCount; ++i) { broadPhase.touchProxy(this.m_proxies[i].proxyId); } } /** * Implement this method to provide collision filtering, if you want finer * control over contact creation. * * Return true if contact calculations should be performed between these two * fixtures. * * Warning: for performance reasons this is only called when the AABBs begin to * overlap. */ shouldCollide(that: Fixture): boolean { if (that.m_filterGroupIndex === this.m_filterGroupIndex && that.m_filterGroupIndex !== 0) { return that.m_filterGroupIndex > 0; } const collideA = (that.m_filterMaskBits & this.m_filterCategoryBits) !== 0; const collideB = (that.m_filterCategoryBits & this.m_filterMaskBits) !== 0; const collide = collideA && collideB; return collide; } }