UNPKG

angular-three-rapier

Version:
1,096 lines (1,083 loc) 96.7 kB
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)