angular-three-rapier
Version:
Physics Rapier for Angular Three
1,096 lines (1,083 loc) • 96.7 kB
JavaScript
import * as i0 from '@angular/core';
import { input, viewChild, Component, CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy, effect, Directive, contentChild, TemplateRef, signal, computed, untracked, inject, DestroyRef, model, output, ElementRef, viewChildren } from '@angular/core';
import { Vector3 as Vector3$1, Quaternion as Quaternion$1, EventQueue, ColliderDesc, ActiveEvents, RigidBodyDesc } from '@dimforge/rapier3d-compat';
import { extend, injectBeforeRender, injectStore, pick, vector3, applyProps, getLocalState, getEmitter, hasListener, resolveRef } from 'angular-three';
import { mergeInputs } from 'ngxtension/inject-inputs';
import { Group, LineSegments, LineBasicMaterial, BufferAttribute, Quaternion, Euler, Vector3, Object3D, Matrix4, MathUtils, DynamicDrawUsage } from 'three';
import { NgTemplateOutlet } from '@angular/common';
import { mergeVertices } from 'three-stdlib';
import { assertInjector } from 'ngxtension/assert-injector';
class NgtrDebug {
world = input.required();
lineSegmentsRef = viewChild.required('lineSegments');
constructor() {
extend({ Group, LineSegments, LineBasicMaterial, BufferAttribute });
injectBeforeRender(() => {
const [world, lineSegments] = [this.world(), this.lineSegmentsRef().nativeElement];
if (!world || !lineSegments)
return;
const buffers = world.debugRender();
lineSegments.geometry.setAttribute('position', new BufferAttribute(buffers.vertices, 3));
lineSegments.geometry.setAttribute('color', new BufferAttribute(buffers.colors, 4));
});
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.4", ngImport: i0, type: NgtrDebug, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "19.1.4", type: NgtrDebug, isStandalone: true, selector: "ngtr-debug", inputs: { world: { classPropertyName: "world", publicName: "world", isSignal: true, isRequired: true, transformFunction: null } }, viewQueries: [{ propertyName: "lineSegmentsRef", first: true, predicate: ["lineSegments"], descendants: true, isSignal: true }], ngImport: i0, template: `
<ngt-group>
<ngt-line-segments #lineSegments [frustumCulled]="false">
<ngt-line-basic-material color="white" [vertexColors]="true" />
<ngt-buffer-geometry />
</ngt-line-segments>
</ngt-group>
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.4", ngImport: i0, type: NgtrDebug, decorators: [{
type: Component,
args: [{
selector: 'ngtr-debug',
template: `
<ngt-group>
<ngt-line-segments #lineSegments [frustumCulled]="false">
<ngt-line-basic-material color="white" [vertexColors]="true" />
<ngt-buffer-geometry />
</ngt-line-segments>
</ngt-group>
`,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
changeDetection: ChangeDetectionStrategy.OnPush,
}]
}], ctorParameters: () => [] });
class NgtrFrameStepper {
ready = input(false);
updatePriority = input(0);
stepFn = input.required();
type = input.required();
constructor() {
const store = injectStore();
effect((onCleanup) => {
const ready = this.ready();
if (!ready)
return;
const [type, stepFn] = [this.type(), this.stepFn()];
if (type === 'follow') {
const updatePriority = this.updatePriority();
const cleanup = store.snapshot.internal.subscribe(({ delta }) => {
stepFn(delta);
}, updatePriority, store);
onCleanup(() => cleanup());
return;
}
let lastFrame = 0;
let raf = 0;
const loop = () => {
const now = performance.now();
const delta = now - lastFrame;
raf = requestAnimationFrame(loop);
stepFn(delta);
lastFrame = now;
};
raf = requestAnimationFrame(loop);
onCleanup(() => cancelAnimationFrame(raf));
});
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.4", ngImport: i0, type: NgtrFrameStepper, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.1.4", type: NgtrFrameStepper, isStandalone: true, selector: "ngtr-frame-stepper", inputs: { ready: { classPropertyName: "ready", publicName: "ready", isSignal: true, isRequired: false, transformFunction: null }, updatePriority: { classPropertyName: "updatePriority", publicName: "updatePriority", isSignal: true, isRequired: false, transformFunction: null }, stepFn: { classPropertyName: "stepFn", publicName: "stepFn", isSignal: true, isRequired: true, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.4", ngImport: i0, type: NgtrFrameStepper, decorators: [{
type: Directive,
args: [{ selector: 'ngtr-frame-stepper' }]
}], ctorParameters: () => [] });
const _quaternion = new Quaternion();
const _euler = new Euler();
const _vector3 = new Vector3();
const _object3d = new Object3D();
const _matrix4 = new Matrix4();
const _position = new Vector3();
const _rotation = new Quaternion();
const _scale = new Vector3();
/**
* Creates a proxy that will create a singleton instance of the given class
* when a property is accessed, and not before.
*
* @returns A proxy and a reset function, so that the instance can created again
*/
const createSingletonProxy = (
/**
* A function that returns a new instance of the class
*/
createInstance) => {
let instance;
const handler = {
get(target, prop) {
if (!instance) {
instance = createInstance();
}
return Reflect.get(instance, prop);
},
set(target, prop, value) {
if (!instance) {
instance = createInstance();
}
return Reflect.set(instance, prop, value);
},
};
const proxy = new Proxy({}, handler);
const reset = () => {
instance = undefined;
};
const set = (newInstance) => {
instance = newInstance;
};
/**
* Return the proxy and a reset function
*/
return { proxy, reset, set };
};
function rapierQuaternionToQuaternion({ x, y, z, w }) {
return _quaternion.set(x, y, z, w);
}
function vector3ToRapierVector(v) {
if (Array.isArray(v)) {
return new Vector3$1(v[0], v[1], v[2]);
}
if (typeof v === 'number') {
return new Vector3$1(v, v, v);
}
const vector = v;
return new Vector3$1(vector.x, vector.y, vector.z);
}
function quaternionToRapierQuaternion(v) {
if (Array.isArray(v)) {
return new Quaternion$1(v[0], v[1], v[2], v[3]);
}
return new Quaternion$1(v.x, v.y, v.z, v.w);
}
function isChildOfMeshCollider(child) {
let flag = false;
child.traverseAncestors((a) => {
if (a.userData['ngtRapierType'] === 'MeshCollider')
flag = true;
});
return flag;
}
const autoColliderMap = {
cuboid: 'cuboid',
ball: 'ball',
hull: 'convexHull',
trimesh: 'trimesh',
};
function getColliderArgsFromGeometry(geometry, colliders) {
switch (colliders) {
case 'cuboid': {
geometry.computeBoundingBox();
const { boundingBox } = geometry;
const size = boundingBox.getSize(new Vector3());
return {
args: [size.x / 2, size.y / 2, size.z / 2],
offset: boundingBox.getCenter(new Vector3()),
};
}
case 'ball': {
geometry.computeBoundingSphere();
const { boundingSphere } = geometry;
const radius = boundingSphere.radius;
return {
args: [radius],
offset: boundingSphere.center,
};
}
case 'trimesh': {
const clonedGeometry = geometry.index ? geometry.clone() : mergeVertices(geometry);
return {
args: [clonedGeometry.attributes['position'].array, clonedGeometry.index?.array],
offset: new Vector3(),
};
}
case 'hull': {
const clonedGeometry = geometry.clone();
return {
args: [clonedGeometry.attributes['position'].array],
offset: new Vector3(),
};
}
}
return { args: [], offset: new Vector3() };
}
function createColliderOptions(object, options, ignoreMeshColliders = true) {
const childColliderOptions = [];
object.updateWorldMatrix(true, false);
const invertedParentMatrixWorld = object.matrixWorld.clone().invert();
const colliderFromChild = (child) => {
if (child.isMesh) {
if (ignoreMeshColliders && isChildOfMeshCollider(child))
return;
const worldScale = child.getWorldScale(_scale);
const shape = autoColliderMap[options.colliders || 'cuboid'];
child.updateWorldMatrix(true, false);
_matrix4.copy(child.matrixWorld).premultiply(invertedParentMatrixWorld).decompose(_position, _rotation, _scale);
const rotationEuler = new Euler().setFromQuaternion(_rotation, 'XYZ');
const { geometry } = child;
const { args, offset } = getColliderArgsFromGeometry(geometry, options.colliders || 'cuboid');
const { mass, linearDamping, angularDamping, canSleep, ccd, gravityScale, softCcdPrediction, ...rest } = options;
childColliderOptions.push({
colliderOptions: rest,
args,
shape,
rotation: [rotationEuler.x, rotationEuler.y, rotationEuler.z],
position: [
_position.x + offset.x * worldScale.x,
_position.y + offset.y * worldScale.y,
_position.z + offset.z * worldScale.z,
],
scale: [worldScale.x, worldScale.y, worldScale.z],
});
}
};
if (options.includeInvisible) {
object.traverse(colliderFromChild);
}
else {
object.traverseVisible(colliderFromChild);
}
return childColliderOptions;
}
const defaultOptions$1 = {
gravity: [0, -9.81, 0],
allowedLinearError: 0.001,
numSolverIterations: 4,
numAdditionalFrictionIterations: 4,
numInternalPgsIterations: 1,
predictionDistance: 0.002,
minIslandSize: 128,
maxCcdSubsteps: 1,
contactNaturalFrequency: 30,
lengthUnit: 1,
colliders: 'cuboid',
updateLoop: 'follow',
interpolate: true,
paused: false,
timeStep: 1 / 60,
debug: false,
};
class NgtrPhysicsFallback {
static ngTemplateContextGuard(_, ctx) {
return true;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.4", ngImport: i0, type: NgtrPhysicsFallback, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.1.4", type: NgtrPhysicsFallback, isStandalone: true, selector: "ng-template[rapierFallback]", ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.4", ngImport: i0, type: NgtrPhysicsFallback, decorators: [{
type: Directive,
args: [{ selector: 'ng-template[rapierFallback]' }]
}] });
class NgtrPhysics {
options = input(defaultOptions$1, { transform: mergeInputs(defaultOptions$1) });
content = contentChild.required(TemplateRef);
fallbackContent = contentChild(NgtrPhysicsFallback, { read: TemplateRef });
updatePriority = pick(this.options, 'updatePriority');
updateLoop = pick(this.options, 'updateLoop');
numSolverIterations = pick(this.options, 'numSolverIterations');
numAdditionalFrictionIterations = pick(this.options, 'numAdditionalFrictionIterations');
numInternalPgsIterations = pick(this.options, 'numInternalPgsIterations');
allowedLinearError = pick(this.options, 'allowedLinearError');
minIslandSize = pick(this.options, 'minIslandSize');
maxCcdSubsteps = pick(this.options, 'maxCcdSubsteps');
predictionDistance = pick(this.options, 'predictionDistance');
contactNaturalFrequency = pick(this.options, 'contactNaturalFrequency');
lengthUnit = pick(this.options, 'lengthUnit');
timeStep = pick(this.options, 'timeStep');
interpolate = pick(this.options, 'interpolate');
paused = pick(this.options, 'paused');
debug = pick(this.options, 'debug');
colliders = pick(this.options, 'colliders');
vGravity = vector3(this.options, 'gravity');
store = injectStore();
rapierConstruct = signal(null);
rapierError = signal(null);
rapier = this.rapierConstruct.asReadonly();
ready = computed(() => !!this.rapier());
worldSingleton = computed(() => {
const rapier = this.rapier();
if (!rapier)
return null;
return createSingletonProxy(() => new rapier.World(untracked(this.vGravity)));
});
rigidBodyStates = new Map();
colliderStates = new Map();
rigidBodyEvents = new Map();
colliderEvents = new Map();
beforeStepCallbacks = new Set();
afterStepCallbacks = new Set();
eventQueue = computed(() => {
const rapier = this.rapier();
if (!rapier)
return null;
return new EventQueue(false);
});
steppingState = { accumulator: 0, previousState: {} };
constructor() {
import('@dimforge/rapier3d-compat')
.then((rapier) => rapier.init().then(() => rapier))
.then(this.rapierConstruct.set.bind(this.rapierConstruct))
.catch((err) => {
console.error(`[NGT] Failed to load rapier3d-compat`, err);
this.rapierError.set(err?.message ?? err.toString());
});
effect(() => {
this.updateWorldEffect();
});
inject(DestroyRef).onDestroy(() => {
const world = this.worldSingleton();
if (world) {
world.proxy.free();
world.reset();
}
});
}
step(delta) {
if (!this.paused()) {
this.internalStep(delta);
}
}
updateWorldEffect() {
const world = this.worldSingleton();
if (!world)
return;
world.proxy.gravity = this.vGravity();
world.proxy.integrationParameters.numSolverIterations = this.numSolverIterations();
world.proxy.integrationParameters.numAdditionalFrictionIterations = this.numAdditionalFrictionIterations();
world.proxy.integrationParameters.numInternalPgsIterations = this.numInternalPgsIterations();
world.proxy.integrationParameters.normalizedAllowedLinearError = this.allowedLinearError();
world.proxy.integrationParameters.minIslandSize = this.minIslandSize();
world.proxy.integrationParameters.maxCcdSubsteps = this.maxCcdSubsteps();
world.proxy.integrationParameters.normalizedPredictionDistance = this.predictionDistance();
world.proxy.integrationParameters.contact_natural_frequency = this.contactNaturalFrequency();
world.proxy.lengthUnit = this.lengthUnit();
}
internalStep(delta) {
const worldSingleton = this.worldSingleton();
if (!worldSingleton)
return;
const eventQueue = this.eventQueue();
if (!eventQueue)
return;
const world = worldSingleton.proxy;
const [timeStep, interpolate, paused] = [this.timeStep(), this.interpolate(), this.paused()];
/* Check if the timestep is supposed to be variable. We'll do this here
once so we don't have to string-check every frame. */
const timeStepVariable = timeStep === 'vary';
/**
* Fixed timeStep simulation progression.
* @see https://gafferongames.com/post/fix_your_timestep/
*/
const clampedDelta = MathUtils.clamp(delta, 0, 0.5);
const stepWorld = (innerDelta) => {
// Trigger beforeStep callbacks
this.beforeStepCallbacks.forEach((callback) => {
callback(world);
});
world.timestep = innerDelta;
world.step(eventQueue);
// Trigger afterStep callbacks
this.afterStepCallbacks.forEach((callback) => {
callback(world);
});
};
if (timeStepVariable) {
stepWorld(clampedDelta);
}
else {
// don't step time forwards if paused
// Increase accumulator
this.steppingState.accumulator += clampedDelta;
while (this.steppingState.accumulator >= timeStep) {
// Set up previous state
// needed for accurate interpolations if the world steps more than once
if (interpolate) {
this.steppingState.previousState = {};
world.forEachRigidBody((body) => {
this.steppingState.previousState[body.handle] = {
position: body.translation(),
rotation: body.rotation(),
};
});
}
stepWorld(timeStep);
this.steppingState.accumulator -= timeStep;
}
}
const interpolationAlpha = timeStepVariable || !interpolate || paused ? 1 : this.steppingState.accumulator / timeStep;
// Update meshes
this.rigidBodyStates.forEach((state, handle) => {
const rigidBody = world.getRigidBody(handle);
const events = this.rigidBodyEvents.get(handle);
if (events?.onSleep || events?.onWake) {
if (rigidBody.isSleeping() && !state.isSleeping)
events?.onSleep?.();
if (!rigidBody.isSleeping() && state.isSleeping)
events?.onWake?.();
state.isSleeping = rigidBody.isSleeping();
}
if (!rigidBody || (rigidBody.isSleeping() && !('isInstancedMesh' in state.object)) || !state.setMatrix) {
return;
}
// New states
let t = rigidBody.translation();
let r = rigidBody.rotation();
let previousState = this.steppingState.previousState[handle];
if (previousState) {
// Get previous simulated world position
_matrix4
.compose(previousState.position, rapierQuaternionToQuaternion(previousState.rotation), state.scale)
.premultiply(state.invertedWorldMatrix)
.decompose(_position, _rotation, _scale);
// Apply previous tick position
if (state.meshType == 'mesh') {
state.object.position.copy(_position);
state.object.quaternion.copy(_rotation);
}
}
// Get new position
_matrix4
.compose(t, rapierQuaternionToQuaternion(r), state.scale)
.premultiply(state.invertedWorldMatrix)
.decompose(_position, _rotation, _scale);
if (state.meshType == 'instancedMesh') {
state.setMatrix(_matrix4);
}
else {
// Interpolate to new position
state.object.position.lerp(_position, interpolationAlpha);
state.object.quaternion.slerp(_rotation, interpolationAlpha);
}
});
eventQueue.drainCollisionEvents((handle1, handle2, started) => {
const source1 = this.getSourceFromColliderHandle(handle1);
const source2 = this.getSourceFromColliderHandle(handle2);
// Collision Events
if (!source1?.collider.object || !source2?.collider.object) {
return;
}
const collisionPayload1 = this.getCollisionPayloadFromSource(source1, source2);
const collisionPayload2 = this.getCollisionPayloadFromSource(source2, source1);
if (started) {
world.contactPair(source1.collider.object, source2.collider.object, (manifold, flipped) => {
/* RigidBody events */
source1.rigidBody.events?.onCollisionEnter?.({ ...collisionPayload1, manifold, flipped });
source2.rigidBody.events?.onCollisionEnter?.({ ...collisionPayload2, manifold, flipped });
/* Collider events */
source1.collider.events?.onCollisionEnter?.({ ...collisionPayload1, manifold, flipped });
source2.collider.events?.onCollisionEnter?.({ ...collisionPayload2, manifold, flipped });
});
}
else {
source1.rigidBody.events?.onCollisionExit?.(collisionPayload1);
source2.rigidBody.events?.onCollisionExit?.(collisionPayload2);
source1.collider.events?.onCollisionExit?.(collisionPayload1);
source2.collider.events?.onCollisionExit?.(collisionPayload2);
}
// Sensor Intersections
if (started) {
if (world.intersectionPair(source1.collider.object, source2.collider.object)) {
source1.rigidBody.events?.onIntersectionEnter?.(collisionPayload1);
source2.rigidBody.events?.onIntersectionEnter?.(collisionPayload2);
source1.collider.events?.onIntersectionEnter?.(collisionPayload1);
source2.collider.events?.onIntersectionEnter?.(collisionPayload2);
}
}
else {
source1.rigidBody.events?.onIntersectionExit?.(collisionPayload1);
source2.rigidBody.events?.onIntersectionExit?.(collisionPayload2);
source1.collider.events?.onIntersectionExit?.(collisionPayload1);
source2.collider.events?.onIntersectionExit?.(collisionPayload2);
}
});
eventQueue.drainContactForceEvents((event) => {
const source1 = this.getSourceFromColliderHandle(event.collider1());
const source2 = this.getSourceFromColliderHandle(event.collider2());
// Collision Events
if (!source1?.collider.object || !source2?.collider.object) {
return;
}
const collisionPayload1 = this.getCollisionPayloadFromSource(source1, source2);
const collisionPayload2 = this.getCollisionPayloadFromSource(source2, source1);
source1.rigidBody.events?.onContactForce?.({
...collisionPayload1,
totalForce: event.totalForce(),
totalForceMagnitude: event.totalForceMagnitude(),
maxForceDirection: event.maxForceDirection(),
maxForceMagnitude: event.maxForceMagnitude(),
});
source2.rigidBody.events?.onContactForce?.({
...collisionPayload2,
totalForce: event.totalForce(),
totalForceMagnitude: event.totalForceMagnitude(),
maxForceDirection: event.maxForceDirection(),
maxForceMagnitude: event.maxForceMagnitude(),
});
source1.collider.events?.onContactForce?.({
...collisionPayload1,
totalForce: event.totalForce(),
totalForceMagnitude: event.totalForceMagnitude(),
maxForceDirection: event.maxForceDirection(),
maxForceMagnitude: event.maxForceMagnitude(),
});
source2.collider.events?.onContactForce?.({
...collisionPayload2,
totalForce: event.totalForce(),
totalForceMagnitude: event.totalForceMagnitude(),
maxForceDirection: event.maxForceDirection(),
maxForceMagnitude: event.maxForceMagnitude(),
});
});
world.forEachActiveRigidBody(() => {
this.store.snapshot.invalidate();
});
}
getSourceFromColliderHandle(handle) {
const world = this.worldSingleton();
if (!world)
return;
const collider = world.proxy.getCollider(handle);
const colEvents = this.colliderEvents.get(handle);
const colliderState = this.colliderStates.get(handle);
const rigidBodyHandle = collider.parent()?.handle;
const rigidBody = rigidBodyHandle !== undefined ? world.proxy.getRigidBody(rigidBodyHandle) : undefined;
const rigidBodyEvents = rigidBody && rigidBodyHandle !== undefined ? this.rigidBodyEvents.get(rigidBodyHandle) : undefined;
const rigidBodyState = rigidBodyHandle !== undefined ? this.rigidBodyStates.get(rigidBodyHandle) : undefined;
return {
collider: { object: collider, events: colEvents, state: colliderState },
rigidBody: { object: rigidBody, events: rigidBodyEvents, state: rigidBodyState },
};
}
getCollisionPayloadFromSource(target, other) {
return {
target: {
rigidBody: target.rigidBody.object,
collider: target.collider.object,
colliderObject: target.collider.state?.object,
rigidBodyObject: target.rigidBody.state?.object,
},
other: {
rigidBody: other.rigidBody.object,
collider: other.collider.object,
colliderObject: other.collider.state?.object,
rigidBodyObject: other.rigidBody.state?.object,
},
};
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.4", ngImport: i0, type: NgtrPhysics, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.1.4", type: NgtrPhysics, isStandalone: true, selector: "ngtr-physics", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "content", first: true, predicate: TemplateRef, descendants: true, isSignal: true }, { propertyName: "fallbackContent", first: true, predicate: NgtrPhysicsFallback, descendants: true, read: TemplateRef, isSignal: true }], ngImport: i0, template: `
@let _rapierError = rapierError();
@let _fallbackContent = fallbackContent();
@if (rapierConstruct()) {
@if (debug()) {
<ngtr-debug [world]="worldSingleton()?.proxy" />
}
<ngtr-frame-stepper
[ready]="ready()"
[stepFn]="step.bind(this)"
[type]="updateLoop()"
[updatePriority]="updatePriority()"
/>
<ng-container [ngTemplateOutlet]="content()" />
} @else if (_rapierError && _fallbackContent) {
<ng-container [ngTemplateOutlet]="_fallbackContent" [ngTemplateOutletContext]="{ error: _rapierError }" />
}
`, isInline: true, dependencies: [{ kind: "component", type: NgtrDebug, selector: "ngtr-debug", inputs: ["world"] }, { kind: "directive", type: NgtrFrameStepper, selector: "ngtr-frame-stepper", inputs: ["ready", "updatePriority", "stepFn", "type"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.4", ngImport: i0, type: NgtrPhysics, decorators: [{
type: Component,
args: [{
selector: 'ngtr-physics',
template: `
@let _rapierError = rapierError();
@let _fallbackContent = fallbackContent();
@if (rapierConstruct()) {
@if (debug()) {
<ngtr-debug [world]="worldSingleton()?.proxy" />
}
<ngtr-frame-stepper
[ready]="ready()"
[stepFn]="step.bind(this)"
[type]="updateLoop()"
[updatePriority]="updatePriority()"
/>
<ng-container [ngTemplateOutlet]="content()" />
} @else if (_rapierError && _fallbackContent) {
<ng-container [ngTemplateOutlet]="_fallbackContent" [ngTemplateOutletContext]="{ error: _rapierError }" />
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NgtrDebug, NgtrFrameStepper, NgTemplateOutlet],
}]
}], ctorParameters: () => [] });
const colliderDefaultOptions = {
contactSkin: 0,
};
class NgtrAnyCollider {
position = input([0, 0, 0]);
rotation = input([0, 0, 0]);
scale = input([1, 1, 1]);
quaternion = input([0, 0, 0, 1]);
userData = input({});
name = input();
options = input(colliderDefaultOptions, { transform: mergeInputs(rigidBodyDefaultOptions) });
object3DParameters = computed(() => {
return {
position: this.position(),
rotation: this.rotation(),
scale: this.scale(),
quaternion: this.quaternion(),
userData: this.userData(),
name: this.name(),
};
});
// TODO: change this to input required when Angular allows setting hostDirective input
shape = model(undefined, { alias: 'ngtrCollider' });
args = model([]);
collisionEnter = output();
collisionExit = output();
intersectionEnter = output();
intersectionExit = output();
contactForce = output();
sensor = pick(this.options, 'sensor');
collisionGroups = pick(this.options, 'collisionGroups');
solverGroups = pick(this.options, 'solverGroups');
friction = pick(this.options, 'friction');
frictionCombineRule = pick(this.options, 'frictionCombineRule');
restitution = pick(this.options, 'restitution');
restitutionCombineRule = pick(this.options, 'restitutionCombineRule');
activeCollisionTypes = pick(this.options, 'activeCollisionTypes');
contactSkin = pick(this.options, 'contactSkin');
mass = pick(this.options, 'mass');
massProperties = pick(this.options, 'massProperties');
density = pick(this.options, 'density');
rigidBody = inject(NgtrRigidBody, { optional: true });
physics = inject(NgtrPhysics);
objectRef = inject(ElementRef);
scaledArgs = computed(() => {
const [shape, args] = [
this.shape(),
this.args(),
];
const cloned = args.slice();
// Heightfield uses a vector
if (shape === 'heightfield') {
const s = cloned[3];
s.x *= this.worldScale.x;
s.y *= this.worldScale.y;
s.z *= this.worldScale.z;
return cloned;
}
// Trimesh and convex scale the vertices
if (shape === 'trimesh' || shape === 'convexHull') {
cloned[0] = this.scaleVertices(cloned[0], this.worldScale);
return cloned;
}
// prefill with some extra
const scaleArray = [this.worldScale.x, this.worldScale.y, this.worldScale.z, this.worldScale.x, this.worldScale.x];
return cloned.map((arg, index) => scaleArray[index] * arg);
});
collider = computed(() => {
const worldSingleton = this.physics.worldSingleton();
if (!worldSingleton)
return null;
const [shape, args, rigidBody] = [this.shape(), this.scaledArgs(), this.rigidBody?.rigidBody()];
// @ts-expect-error - we know the type of the data
const desc = ColliderDesc[shape](...args);
if (!desc)
return null;
return worldSingleton.proxy.createCollider(desc, rigidBody ?? undefined);
});
constructor() {
extend({ Object3D });
effect(() => {
const object3DParameters = this.object3DParameters();
applyProps(this.objectRef.nativeElement, object3DParameters);
});
effect((onCleanup) => {
const cleanup = this.createColliderStateEffect();
onCleanup(() => cleanup?.());
});
effect((onCleanup) => {
const cleanup = this.createColliderEventsEffect();
onCleanup(() => cleanup?.());
});
effect(() => {
this.updateColliderEffect();
this.updateMassPropertiesEffect();
});
}
get worldScale() {
return this.objectRef.nativeElement.getWorldScale(new Vector3());
}
setShape(shape) {
this.shape.set(shape);
}
setArgs(args) {
this.args.set(args);
}
createColliderStateEffect() {
const collider = this.collider();
if (!collider)
return;
const worldSingleton = this.physics.worldSingleton();
if (!worldSingleton)
return;
const localState = getLocalState(this.objectRef.nativeElement);
if (!localState)
return;
const parent = localState.parent();
if (!parent || !parent.isObject3D)
return;
const state = this.createColliderState(collider, this.objectRef.nativeElement, this.rigidBody?.objectRef.nativeElement);
this.physics.colliderStates.set(collider.handle, state);
return () => {
this.physics.colliderStates.delete(collider.handle);
if (worldSingleton.proxy.getCollider(collider.handle)) {
worldSingleton.proxy.removeCollider(collider, true);
}
};
}
createColliderEventsEffect() {
const collider = this.collider();
if (!collider)
return;
const worldSingleton = this.physics.worldSingleton();
if (!worldSingleton)
return;
const collisionEnter = getEmitter(this.collisionEnter);
const collisionExit = getEmitter(this.collisionExit);
const intersectionEnter = getEmitter(this.intersectionEnter);
const intersectionExit = getEmitter(this.intersectionExit);
const contactForce = getEmitter(this.contactForce);
const hasCollisionEvent = hasListener(this.collisionEnter, this.collisionExit, this.intersectionEnter, this.intersectionExit, this.rigidBody?.collisionEnter, this.rigidBody?.collisionExit, this.rigidBody?.intersectionEnter, this.rigidBody?.intersectionExit);
const hasContactForceEvent = hasListener(this.contactForce, this.rigidBody?.contactForce);
if (hasCollisionEvent && hasContactForceEvent) {
collider.setActiveEvents(ActiveEvents.COLLISION_EVENTS | ActiveEvents.CONTACT_FORCE_EVENTS);
}
else if (hasCollisionEvent) {
collider.setActiveEvents(ActiveEvents.COLLISION_EVENTS);
}
else if (hasContactForceEvent) {
collider.setActiveEvents(ActiveEvents.CONTACT_FORCE_EVENTS);
}
this.physics.colliderEvents.set(collider.handle, {
onCollisionEnter: collisionEnter,
onCollisionExit: collisionExit,
onIntersectionEnter: intersectionEnter,
onIntersectionExit: intersectionExit,
onContactForce: contactForce,
});
return () => {
this.physics.colliderEvents.delete(collider.handle);
};
}
updateColliderEffect() {
const collider = this.collider();
if (!collider)
return;
const worldSingleton = this.physics.worldSingleton();
if (!worldSingleton)
return;
const state = this.physics.colliderStates.get(collider.handle);
if (!state)
return;
// Update collider position based on the object's position
const parentWorldScale = state.object.parent.getWorldScale(_vector3);
const parentInvertedWorldMatrix = state.worldParent?.matrixWorld.clone().invert();
state.object.updateWorldMatrix(true, false);
_matrix4.copy(state.object.matrixWorld);
if (parentInvertedWorldMatrix) {
_matrix4.premultiply(parentInvertedWorldMatrix);
}
_matrix4.decompose(_position, _rotation, _scale);
if (collider.parent()) {
collider.setTranslationWrtParent({
x: _position.x * parentWorldScale.x,
y: _position.y * parentWorldScale.y,
z: _position.z * parentWorldScale.z,
});
collider.setRotationWrtParent(_rotation);
}
else {
collider.setTranslation({
x: _position.x * parentWorldScale.x,
y: _position.y * parentWorldScale.y,
z: _position.z * parentWorldScale.z,
});
collider.setRotation(_rotation);
}
const [sensor, collisionGroups, solverGroups, friction, frictionCombineRule, restitution, restitutionCombineRule, activeCollisionTypes, contactSkin,] = [
this.sensor(),
this.collisionGroups(),
this.solverGroups(),
this.friction(),
this.frictionCombineRule(),
this.restitution(),
this.restitutionCombineRule(),
this.activeCollisionTypes(),
this.contactSkin(),
];
if (sensor !== undefined)
collider.setSensor(sensor);
if (collisionGroups !== undefined)
collider.setCollisionGroups(collisionGroups);
if (solverGroups !== undefined)
collider.setSolverGroups(solverGroups);
if (friction !== undefined)
collider.setFriction(friction);
if (frictionCombineRule !== undefined)
collider.setFrictionCombineRule(frictionCombineRule);
if (restitution !== undefined)
collider.setRestitution(restitution);
if (restitutionCombineRule !== undefined)
collider.setRestitutionCombineRule(restitutionCombineRule);
if (activeCollisionTypes !== undefined)
collider.setActiveCollisionTypes(activeCollisionTypes);
if (contactSkin !== undefined)
collider.setContactSkin(contactSkin);
}
updateMassPropertiesEffect() {
const collider = this.collider();
if (!collider)
return;
const [mass, massProperties, density] = [this.mass(), this.massProperties(), this.density()];
if (density !== undefined) {
if (mass !== undefined || massProperties !== undefined) {
throw new Error('[NGT Rapier] Cannot set mass and massProperties along with density');
}
collider.setDensity(density);
return;
}
if (mass !== undefined) {
if (massProperties !== undefined) {
throw new Error('[NGT Rapier] Cannot set massProperties along with mass');
}
collider.setMass(mass);
return;
}
if (massProperties !== undefined) {
collider.setMassProperties(massProperties.mass, massProperties.centerOfMass, massProperties.principalAngularInertia, massProperties.angularInertiaLocalFrame);
return;
}
}
createColliderState(collider, object, rigidBodyObject) {
return { collider, worldParent: rigidBodyObject || undefined, object };
}
scaleVertices(vertices, scale) {
const scaledVerts = Array.from(vertices);
for (let i = 0; i < vertices.length / 3; i++) {
scaledVerts[i * 3] *= scale.x;
scaledVerts[i * 3 + 1] *= scale.y;
scaledVerts[i * 3 + 2] *= scale.z;
}
return scaledVerts;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.4", ngImport: i0, type: NgtrAnyCollider, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.1.4", type: NgtrAnyCollider, isStandalone: true, selector: "ngt-object3D[ngtrCollider]", inputs: { position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, rotation: { classPropertyName: "rotation", publicName: "rotation", isSignal: true, isRequired: false, transformFunction: null }, scale: { classPropertyName: "scale", publicName: "scale", isSignal: true, isRequired: false, transformFunction: null }, quaternion: { classPropertyName: "quaternion", publicName: "quaternion", isSignal: true, isRequired: false, transformFunction: null }, userData: { classPropertyName: "userData", publicName: "userData", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, shape: { classPropertyName: "shape", publicName: "ngtrCollider", isSignal: true, isRequired: false, transformFunction: null }, args: { classPropertyName: "args", publicName: "args", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { shape: "ngtrColliderChange", args: "argsChange", collisionEnter: "collisionEnter", collisionExit: "collisionExit", intersectionEnter: "intersectionEnter", intersectionExit: "intersectionExit", contactForce: "contactForce" }, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.4", ngImport: i0, type: NgtrAnyCollider, decorators: [{
type: Directive,
args: [{ selector: 'ngt-object3D[ngtrCollider]' }]
}], ctorParameters: () => [] });
const RIGID_BODY_TYPE_MAP = {
fixed: 1,
dynamic: 0,
kinematicPosition: 2,
kinematicVelocity: 3,
};
const rigidBodyDefaultOptions = {
canSleep: true,
linearVelocity: [0, 0, 0],
angularVelocity: [0, 0, 0],
gravityScale: 1,
dominanceGroup: 0,
ccd: false,
softCcdPrediction: 0,
contactSkin: 0,
};
class NgtrRigidBody {
type = input('dynamic', {
alias: 'ngtrRigidBody',
transform: (value) => {
if (value === '' || value === undefined)
return 'dynamic';
return value;
},
});
position = input([0, 0, 0]);
rotation = input([0, 0, 0]);
scale = input([1, 1, 1]);
quaternion = input([0, 0, 0, 1]);
userData = input({});
options = input(rigidBodyDefaultOptions, { transform: mergeInputs(rigidBodyDefaultOptions) });
object3DParameters = computed(() => {
return {
position: this.position(),
rotation: this.rotation(),
scale: this.scale(),
quaternion: this.quaternion(),
userData: this.userData(),
};
});
wake = output();
sleep = output();
collisionEnter = output();
collisionExit = output();
intersectionEnter = output();
intersectionExit = output();
contactForce = output();
canSleep = pick(this.options, 'canSleep');
colliders = pick(this.options, 'colliders');
transformState = pick(this.options, 'transformState');
gravityScale = pick(this.options, 'gravityScale');
dominanceGroup = pick(this.options, 'dominanceGroup');
ccd = pick(this.options, 'ccd');
softCcdPrediction = pick(this.options, 'softCcdPrediction');
additionalSolverIterations = pick(this.options, 'additionalSolverIterations');
linearDamping = pick(this.options, 'linearDamping');
angularDamping = pick(this.options, 'angularDamping');
lockRotations = pick(this.options, 'lockRotations');
lockTranslations = pick(this.options, 'lockTranslations');
enabledRotations = pick(this.options, 'enabledRotations');
enabledTranslations = pick(this.options, 'enabledTranslations');
angularVelocity = pick(this.options, 'angularVelocity');
linearVelocity = pick(this.options, 'linearVelocity');
objectRef = inject(ElementRef);
physics = inject(NgtrPhysics);
bodyType = computed(() => RIGID_BODY_TYPE_MAP[this.type()]);
bodyDesc = computed(() => {
const [canSleep, bodyType] = [this.canSleep(), untracked(this.bodyType), this.colliders()];
return new RigidBodyDesc(bodyType).setCanSleep(canSleep);
});
rigidBody = computed(() => {
const worldSingleton = this.physics.worldSingleton();
if (!worldSingleton)
return null;
return worldSingleton.proxy.createRigidBody(this.bodyDesc());
});
childColliderOptions = computed(() => {
const colliders = this.colliders();
// if self colliders is false explicitly, disable auto colliders for this object entirely.
if (colliders === false)
return [];
const physicsColliders = this.physics.colliders();
// if physics colliders is false explicitly AND colliders is not set, disable auto colliders for this object entirely.
if (physicsColliders === false && colliders === undefined)
return [];
const options = untracked(this.options);
// if colliders on object is not set, use physics colliders
if (!options.colliders)
options.colliders = physicsColliders;
const objectLocalState = getLocalState(this.objectRef.nativeElement);
if (!objectLocalState)
return [];
// track object's parent and non-object children
const [parent] = [objectLocalState.parent(), objectLocalState.nonObjects()];
if (!parent || !parent.isObject3D)
return [];
return createColliderOptions(this.objectRef.nativeElement, options, true);
});
constructor() {
extend({ Object3D });
effect(() => {
const object3DParameters = this.object3DParameters();
applyProps(this.objectRef.nativeElement, object3DParameters);
});
effect((onCleanup) => {
const cleanup = this.createRigidBodyStateEffect();
onCleanup(() => cleanup?.());
});
effect((onCleanup) => {
const cleanup = this.createRigidBodyEventsEffect();
onCleanup(() => cleanup?.());
});
effect(() => {
this.updateRigidBodyEffect();
});
}
createRigidBodyStateEffect() {
const worldSingleton = this.physics.worldSingleton();
if (!worldSingleton)
return;
const body = this.rigidBody();
if (!body)
return;
const transformState = untracked(this.transformState);
const localState = getLocalState(this.objectRef.nativeElement);
if (!localState)
return;
const parent = localState.parent();
if (!parent || !parent.isObject3D)
return;
const state = this.createRigidBodyState(body, this.objectRef.nativeElement);
this.physics.rigidBodyStates.set(body.handle, transformState ? transformState(state) : state);
return () => {
this.physics.rigidBodyStates.delete(body.handle);
if (worldSingleton.proxy.getRigidBody(body.handle)) {
worldSingleton.proxy.removeRigidBody(body);
}
};
}
createRigidBodyEventsEffect() {
const worldSingleton = this.physics.worldSingleton();
if (!worldSingleton)
return;
const body = this.rigidBody();
if (!body)
return;
const wake = getEmitter(this.wake);
const sleep = getEmitter(this.sleep);
const collisionEnter = getEmitter(this.collisionEnter);
const collisionExit = getEmitter(this.collisionExit);
const intersectionEnter = getEmitter(this.intersectionEnter);
const intersectionExit = getEmitter(this.intersectionExit);
const contactForce = getEmitter(this.contactForce);
this.physics.rigidBodyEvents.set(body.handle, {
onWake: wake,
onSleep: sleep,
onCollisionEnter: collisionEnter,
onCollisionExit: collisionExit,
onIntersectionEnter: intersectionEnter,
onIntersectionExit: intersectionExit,
onContactForce: contactForce,
});
return () => {
this.physics.rigidBodyEvents.delete(body.handle);
};
}
updateRigidBodyEffect() {
const worldSingleton = this.physics.worldSingleton();
if (!worldSingleton)
return;
const body = this.rigidBody();
if (!body)
return;
const state = this.physics.rigidBodyStates.get(body.handle);
if (!state)
return;
state.object.updateWorldMatrix(true, false);
_matrix4.copy(state.object.matrixWorld).decompose(_position, _rotation, _scale);
body.setTranslation(_position, true);
body.setRotation(_rotation, true);
const [gravityScale, additionalSolverIterations, linearDamping, angularDamping, lockRotations, lockTranslations, enabledRotations, enabledTranslations, angularVelocity, linearVelocity, ccd, softCcdPrediction, dominanceGroup, userData, bodyType,] = [
this.gravityScale(),
this.additionalSolverIterations(),
this.linearDamping(),
this.angularDamping(),
this.lockRotations(),
this.lockTranslations(),
this.enabledRotations(),
this.enabledTranslations(),
this.angularVelocity(),
this.linearVelocity(),
this.ccd(),
this.softCcdPrediction(),
this.dominanceGroup(),
this.userData(),
this.bodyType(),
];
body.setGravityScale(gravityScale, true);
if (additionalSolverIterations !== undefined)