UNPKG

angular-three-rapier

Version:
1 lines 143 kB
{"version":3,"file":"angular-three-rapier.mjs","sources":["../../../../libs/rapier/src/lib/debug.ts","../../../../libs/rapier/src/lib/frame-stepper.ts","../../../../libs/rapier/src/lib/shared.ts","../../../../libs/rapier/src/lib/utils.ts","../../../../libs/rapier/src/lib/physics.ts","../../../../libs/rapier/src/lib/rigid-body.ts","../../../../libs/rapier/src/lib/colliders.ts","../../../../libs/rapier/src/lib/instanced-rigid-bodies.ts","../../../../libs/rapier/src/lib/joints.ts","../../../../libs/rapier/src/lib/mesh-collider.ts","../../../../libs/rapier/src/angular-three-rapier.ts"],"sourcesContent":["import {\n\tChangeDetectionStrategy,\n\tComponent,\n\tCUSTOM_ELEMENTS_SCHEMA,\n\tElementRef,\n\tinput,\n\tviewChild,\n} from '@angular/core';\nimport { World } from '@dimforge/rapier3d-compat';\nimport { extend, injectBeforeRender } from 'angular-three';\nimport { BufferAttribute, Group, LineBasicMaterial, LineSegments } from 'three';\n\n@Component({\n\tselector: 'ngtr-debug',\n\ttemplate: `\n\t\t<ngt-group>\n\t\t\t<ngt-line-segments #lineSegments [frustumCulled]=\"false\">\n\t\t\t\t<ngt-line-basic-material color=\"white\" [vertexColors]=\"true\" />\n\t\t\t\t<ngt-buffer-geometry />\n\t\t\t</ngt-line-segments>\n\t\t</ngt-group>\n\t`,\n\tschemas: [CUSTOM_ELEMENTS_SCHEMA],\n\tchangeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class NgtrDebug {\n\tworld = input.required<World | undefined>();\n\n\tprivate lineSegmentsRef = viewChild.required<ElementRef<LineSegments>>('lineSegments');\n\n\tconstructor() {\n\t\textend({ Group, LineSegments, LineBasicMaterial, BufferAttribute });\n\n\t\tinjectBeforeRender(() => {\n\t\t\tconst [world, lineSegments] = [this.world(), this.lineSegmentsRef().nativeElement];\n\t\t\tif (!world || !lineSegments) return;\n\n\t\t\tconst buffers = world.debugRender();\n\n\t\t\tlineSegments.geometry.setAttribute('position', new BufferAttribute(buffers.vertices, 3));\n\t\t\tlineSegments.geometry.setAttribute('color', new BufferAttribute(buffers.colors, 4));\n\t\t});\n\t}\n}\n","import { Directive, effect, input } from '@angular/core';\nimport { injectStore } from 'angular-three';\nimport { NgtrPhysicsOptions } from './types';\n\n@Directive({ selector: 'ngtr-frame-stepper' })\nexport class NgtrFrameStepper {\n\tready = input(false);\n\tupdatePriority = input<number | undefined>(0);\n\tstepFn = input.required<(delta: number) => void>();\n\ttype = input.required<NgtrPhysicsOptions['updateLoop']>();\n\n\tconstructor() {\n\t\tconst store = injectStore();\n\n\t\teffect((onCleanup) => {\n\t\t\tconst ready = this.ready();\n\t\t\tif (!ready) return;\n\n\t\t\tconst [type, stepFn] = [this.type(), this.stepFn()];\n\n\t\t\tif (type === 'follow') {\n\t\t\t\tconst updatePriority = this.updatePriority();\n\t\t\t\tconst cleanup = store.snapshot.internal.subscribe(\n\t\t\t\t\t({ delta }) => {\n\t\t\t\t\t\tstepFn(delta);\n\t\t\t\t\t},\n\t\t\t\t\tupdatePriority,\n\t\t\t\t\tstore,\n\t\t\t\t);\n\t\t\t\tonCleanup(() => cleanup());\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet lastFrame = 0;\n\t\t\tlet raf: ReturnType<typeof requestAnimationFrame> = 0;\n\t\t\tconst loop = () => {\n\t\t\t\tconst now = performance.now();\n\t\t\t\tconst delta = now - lastFrame;\n\t\t\t\traf = requestAnimationFrame(loop);\n\t\t\t\tstepFn(delta);\n\t\t\t\tlastFrame = now;\n\t\t\t};\n\n\t\t\traf = requestAnimationFrame(loop);\n\t\t\tonCleanup(() => cancelAnimationFrame(raf));\n\t\t});\n\t}\n}\n","import { Euler, Matrix4, Object3D, Quaternion, Vector3 } from 'three';\n\nexport const _quaternion = new Quaternion();\nexport const _euler = new Euler();\nexport const _vector3 = new Vector3();\nexport const _object3d = new Object3D();\nexport const _matrix4 = new Matrix4();\nexport const _position = new Vector3();\nexport const _rotation = new Quaternion();\nexport const _scale = new Vector3();\n","import { Quaternion as RapierQuaternion, Vector3 as RapierVector3 } from '@dimforge/rapier3d-compat';\nimport { NgtEuler, NgtQuaternion, NgtVector3 } from 'angular-three';\nimport { BufferGeometry, Euler, Mesh, Object3D, Vector3 } from 'three';\nimport { mergeVertices } from 'three-stdlib';\nimport { _matrix4, _position, _quaternion, _rotation, _scale } from './shared';\nimport { NgtrColliderOptions, NgtrColliderShape, NgtrRigidBodyAutoCollider, NgtrRigidBodyOptions } from './types';\n\n/**\n * Creates a proxy that will create a singleton instance of the given class\n * when a property is accessed, and not before.\n *\n * @returns A proxy and a reset function, so that the instance can created again\n */\nexport const createSingletonProxy = <\n\tSingletonClass extends object,\n\tCreationFn extends () => SingletonClass = () => SingletonClass,\n>(\n\t/**\n\t * A function that returns a new instance of the class\n\t */\n\tcreateInstance: CreationFn,\n): {\n\tproxy: SingletonClass;\n\treset: () => void;\n\tset: (newInstance: SingletonClass) => void;\n} => {\n\tlet instance: SingletonClass | undefined;\n\n\tconst handler: ProxyHandler<SingletonClass> = {\n\t\tget(target, prop) {\n\t\t\tif (!instance) {\n\t\t\t\tinstance = createInstance();\n\t\t\t}\n\t\t\treturn Reflect.get(instance!, prop);\n\t\t},\n\t\tset(target, prop, value) {\n\t\t\tif (!instance) {\n\t\t\t\tinstance = createInstance();\n\t\t\t}\n\t\t\treturn Reflect.set(instance!, prop, value);\n\t\t},\n\t};\n\n\tconst proxy = new Proxy({} as SingletonClass, handler) as SingletonClass;\n\n\tconst reset = () => {\n\t\tinstance = undefined;\n\t};\n\n\tconst set = (newInstance: SingletonClass) => {\n\t\tinstance = newInstance;\n\t};\n\n\t/**\n\t * Return the proxy and a reset function\n\t */\n\treturn { proxy, reset, set };\n};\n\nexport function rapierQuaternionToQuaternion({ x, y, z, w }: RapierQuaternion) {\n\treturn _quaternion.set(x, y, z, w);\n}\n\nexport function vector3ToRapierVector(v: NgtVector3) {\n\tif (Array.isArray(v)) {\n\t\treturn new RapierVector3(v[0], v[1], v[2]);\n\t}\n\n\tif (typeof v === 'number') {\n\t\treturn new RapierVector3(v, v, v);\n\t}\n\tconst vector = v as Vector3;\n\treturn new RapierVector3(vector.x, vector.y, vector.z);\n}\n\nexport function quaternionToRapierQuaternion(v: NgtQuaternion) {\n\tif (Array.isArray(v)) {\n\t\treturn new RapierQuaternion(v[0], v[1], v[2], v[3]);\n\t}\n\treturn new RapierQuaternion(v.x, v.y, v.z, v.w);\n}\n\nfunction isChildOfMeshCollider(child: Mesh) {\n\tlet flag = false;\n\tchild.traverseAncestors((a) => {\n\t\tif (a.userData['ngtRapierType'] === 'MeshCollider') flag = true;\n\t});\n\treturn flag;\n}\n\nconst autoColliderMap = {\n\tcuboid: 'cuboid',\n\tball: 'ball',\n\thull: 'convexHull',\n\ttrimesh: 'trimesh',\n};\n\nfunction getColliderArgsFromGeometry(\n\tgeometry: BufferGeometry,\n\tcolliders: NgtrRigidBodyAutoCollider,\n): { args: unknown[]; offset: Vector3 } {\n\tswitch (colliders) {\n\t\tcase 'cuboid': {\n\t\t\tgeometry.computeBoundingBox();\n\t\t\tconst { boundingBox } = geometry;\n\t\t\tconst size = boundingBox!.getSize(new Vector3());\n\t\t\treturn {\n\t\t\t\targs: [size.x / 2, size.y / 2, size.z / 2],\n\t\t\t\toffset: boundingBox!.getCenter(new Vector3()),\n\t\t\t};\n\t\t}\n\n\t\tcase 'ball': {\n\t\t\tgeometry.computeBoundingSphere();\n\t\t\tconst { boundingSphere } = geometry;\n\n\t\t\tconst radius = boundingSphere!.radius;\n\n\t\t\treturn {\n\t\t\t\targs: [radius],\n\t\t\t\toffset: boundingSphere!.center,\n\t\t\t};\n\t\t}\n\n\t\tcase 'trimesh': {\n\t\t\tconst clonedGeometry = geometry.index ? geometry.clone() : mergeVertices(geometry);\n\n\t\t\treturn {\n\t\t\t\targs: [clonedGeometry.attributes['position'].array as Float32Array, clonedGeometry.index?.array as Uint32Array],\n\t\t\t\toffset: new Vector3(),\n\t\t\t};\n\t\t}\n\n\t\tcase 'hull': {\n\t\t\tconst clonedGeometry = geometry.clone();\n\t\t\treturn {\n\t\t\t\targs: [clonedGeometry.attributes['position'].array as Float32Array],\n\t\t\t\toffset: new Vector3(),\n\t\t\t};\n\t\t}\n\t}\n\n\treturn { args: [], offset: new Vector3() };\n}\n\nexport function createColliderOptions(object: Object3D, options: NgtrRigidBodyOptions, ignoreMeshColliders = true) {\n\tconst childColliderOptions: {\n\t\tcolliderOptions: NgtrColliderOptions;\n\t\targs: unknown[];\n\t\tshape: NgtrColliderShape;\n\t\trotation: NgtEuler;\n\t\tposition: NgtVector3;\n\t\tscale: NgtVector3;\n\t}[] = [];\n\tobject.updateWorldMatrix(true, false);\n\tconst invertedParentMatrixWorld = object.matrixWorld.clone().invert();\n\n\tconst colliderFromChild = (child: Object3D) => {\n\t\tif ((child as Mesh).isMesh) {\n\t\t\tif (ignoreMeshColliders && isChildOfMeshCollider(child as Mesh)) return;\n\n\t\t\tconst worldScale = child.getWorldScale(_scale);\n\t\t\tconst shape = autoColliderMap[options.colliders || 'cuboid'] as NgtrColliderShape;\n\t\t\tchild.updateWorldMatrix(true, false);\n\t\t\t_matrix4.copy(child.matrixWorld).premultiply(invertedParentMatrixWorld).decompose(_position, _rotation, _scale);\n\n\t\t\tconst rotationEuler = new Euler().setFromQuaternion(_rotation, 'XYZ');\n\n\t\t\tconst { geometry } = child as Mesh;\n\t\t\tconst { args, offset } = getColliderArgsFromGeometry(geometry, options.colliders || 'cuboid');\n\t\t\tconst { mass, linearDamping, angularDamping, canSleep, ccd, gravityScale, softCcdPrediction, ...rest } = options;\n\n\t\t\tchildColliderOptions.push({\n\t\t\t\tcolliderOptions: rest,\n\t\t\t\targs,\n\t\t\t\tshape,\n\t\t\t\trotation: [rotationEuler.x, rotationEuler.y, rotationEuler.z],\n\t\t\t\tposition: [\n\t\t\t\t\t_position.x + offset.x * worldScale.x,\n\t\t\t\t\t_position.y + offset.y * worldScale.y,\n\t\t\t\t\t_position.z + offset.z * worldScale.z,\n\t\t\t\t],\n\t\t\t\tscale: [worldScale.x, worldScale.y, worldScale.z],\n\t\t\t});\n\t\t}\n\t};\n\n\tif (options.includeInvisible) {\n\t\tobject.traverse(colliderFromChild);\n\t} else {\n\t\tobject.traverseVisible(colliderFromChild);\n\t}\n\n\treturn childColliderOptions;\n}\n","import { NgTemplateOutlet } from '@angular/common';\nimport {\n\tChangeDetectionStrategy,\n\tComponent,\n\tcomputed,\n\tcontentChild,\n\tDestroyRef,\n\tDirective,\n\teffect,\n\tinject,\n\tinput,\n\tsignal,\n\tTemplateRef,\n\tuntracked,\n} from '@angular/core';\nimport RAPIER, { ColliderHandle, EventQueue, Rotation, Vector, World } from '@dimforge/rapier3d-compat';\nimport { injectStore, pick, vector3 } from 'angular-three';\nimport { mergeInputs } from 'ngxtension/inject-inputs';\nimport { MathUtils, Quaternion, Vector3 } from 'three';\nimport { NgtrDebug } from './debug';\nimport { NgtrFrameStepper } from './frame-stepper';\nimport { _matrix4, _position, _rotation, _scale } from './shared';\nimport {\n\tNgtrColliderStateMap,\n\tNgtrCollisionPayload,\n\tNgtrCollisionSource,\n\tNgtrEventMap,\n\tNgtrPhysicsOptions,\n\tNgtrRigidBodyStateMap,\n\tNgtrWorldStepCallbackSet,\n} from './types';\nimport { createSingletonProxy, rapierQuaternionToQuaternion } from './utils';\n\nconst defaultOptions: NgtrPhysicsOptions = {\n\tgravity: [0, -9.81, 0],\n\tallowedLinearError: 0.001,\n\tnumSolverIterations: 4,\n\tnumAdditionalFrictionIterations: 4,\n\tnumInternalPgsIterations: 1,\n\tpredictionDistance: 0.002,\n\tminIslandSize: 128,\n\tmaxCcdSubsteps: 1,\n\tcontactNaturalFrequency: 30,\n\tlengthUnit: 1,\n\tcolliders: 'cuboid',\n\tupdateLoop: 'follow',\n\tinterpolate: true,\n\tpaused: false,\n\ttimeStep: 1 / 60,\n\tdebug: false,\n};\n\n@Directive({ selector: 'ng-template[rapierFallback]' })\nexport class NgtrPhysicsFallback {\n\tstatic ngTemplateContextGuard(_: NgtrPhysicsFallback, ctx: unknown): ctx is { error: string } {\n\t\treturn true;\n\t}\n}\n\n@Component({\n\tselector: 'ngtr-physics',\n\ttemplate: `\n\t\t@let _rapierError = rapierError();\n\t\t@let _fallbackContent = fallbackContent();\n\n\t\t@if (rapierConstruct()) {\n\t\t\t@if (debug()) {\n\t\t\t\t<ngtr-debug [world]=\"worldSingleton()?.proxy\" />\n\t\t\t}\n\n\t\t\t<ngtr-frame-stepper\n\t\t\t\t[ready]=\"ready()\"\n\t\t\t\t[stepFn]=\"step.bind(this)\"\n\t\t\t\t[type]=\"updateLoop()\"\n\t\t\t\t[updatePriority]=\"updatePriority()\"\n\t\t\t/>\n\n\t\t\t<ng-container [ngTemplateOutlet]=\"content()\" />\n\t\t} @else if (_rapierError && _fallbackContent) {\n\t\t\t<ng-container [ngTemplateOutlet]=\"_fallbackContent\" [ngTemplateOutletContext]=\"{ error: _rapierError }\" />\n\t\t}\n\t`,\n\tchangeDetection: ChangeDetectionStrategy.OnPush,\n\timports: [NgtrDebug, NgtrFrameStepper, NgTemplateOutlet],\n})\nexport class NgtrPhysics {\n\toptions = input(defaultOptions, { transform: mergeInputs(defaultOptions) });\n\n\tcontent = contentChild.required(TemplateRef);\n\tfallbackContent = contentChild(NgtrPhysicsFallback, { read: TemplateRef });\n\n\tprotected updatePriority = pick(this.options, 'updatePriority');\n\tprotected updateLoop = pick(this.options, 'updateLoop');\n\n\tprivate numSolverIterations = pick(this.options, 'numSolverIterations');\n\tprivate numAdditionalFrictionIterations = pick(this.options, 'numAdditionalFrictionIterations');\n\tprivate numInternalPgsIterations = pick(this.options, 'numInternalPgsIterations');\n\tprivate allowedLinearError = pick(this.options, 'allowedLinearError');\n\tprivate minIslandSize = pick(this.options, 'minIslandSize');\n\tprivate maxCcdSubsteps = pick(this.options, 'maxCcdSubsteps');\n\tprivate predictionDistance = pick(this.options, 'predictionDistance');\n\tprivate contactNaturalFrequency = pick(this.options, 'contactNaturalFrequency');\n\tprivate lengthUnit = pick(this.options, 'lengthUnit');\n\tprivate timeStep = pick(this.options, 'timeStep');\n\tprivate interpolate = pick(this.options, 'interpolate');\n\n\tpaused = pick(this.options, 'paused');\n\tdebug = pick(this.options, 'debug');\n\tcolliders = pick(this.options, 'colliders');\n\n\tprivate vGravity = vector3(this.options, 'gravity');\n\n\tprivate store = injectStore();\n\n\tprotected rapierConstruct = signal<typeof RAPIER | null>(null);\n\tprotected rapierError = signal<string | null>(null);\n\trapier = this.rapierConstruct.asReadonly();\n\n\tready = computed(() => !!this.rapier());\n\tworldSingleton = computed(() => {\n\t\tconst rapier = this.rapier();\n\t\tif (!rapier) return null;\n\t\treturn createSingletonProxy<World>(() => new rapier.World(untracked(this.vGravity)));\n\t});\n\n\trigidBodyStates: NgtrRigidBodyStateMap = new Map();\n\tcolliderStates: NgtrColliderStateMap = new Map();\n\trigidBodyEvents: NgtrEventMap = new Map();\n\tcolliderEvents: NgtrEventMap = new Map();\n\tprivate beforeStepCallbacks: NgtrWorldStepCallbackSet = new Set();\n\tprivate afterStepCallbacks: NgtrWorldStepCallbackSet = new Set();\n\n\tprivate eventQueue = computed(() => {\n\t\tconst rapier = this.rapier();\n\t\tif (!rapier) return null;\n\t\treturn new EventQueue(false);\n\t});\n\n\tprivate steppingState: {\n\t\taccumulator: number;\n\t\tpreviousState: Record<number, { position: Vector; rotation: Rotation }>;\n\t} = { accumulator: 0, previousState: {} };\n\n\tconstructor() {\n\t\timport('@dimforge/rapier3d-compat')\n\t\t\t.then((rapier) => rapier.init().then(() => rapier))\n\t\t\t.then(this.rapierConstruct.set.bind(this.rapierConstruct))\n\t\t\t.catch((err) => {\n\t\t\t\tconsole.error(`[NGT] Failed to load rapier3d-compat`, err);\n\t\t\t\tthis.rapierError.set(err?.message ?? err.toString());\n\t\t\t});\n\n\t\teffect(() => {\n\t\t\tthis.updateWorldEffect();\n\t\t});\n\n\t\tinject(DestroyRef).onDestroy(() => {\n\t\t\tconst world = this.worldSingleton();\n\t\t\tif (world) {\n\t\t\t\tworld.proxy.free();\n\t\t\t\tworld.reset();\n\t\t\t}\n\t\t});\n\t}\n\n\tstep(delta: number) {\n\t\tif (!this.paused()) {\n\t\t\tthis.internalStep(delta);\n\t\t}\n\t}\n\n\tprivate updateWorldEffect() {\n\t\tconst world = this.worldSingleton();\n\t\tif (!world) return;\n\n\t\tworld.proxy.gravity = this.vGravity();\n\t\tworld.proxy.integrationParameters.numSolverIterations = this.numSolverIterations();\n\t\tworld.proxy.integrationParameters.numAdditionalFrictionIterations = this.numAdditionalFrictionIterations();\n\t\tworld.proxy.integrationParameters.numInternalPgsIterations = this.numInternalPgsIterations();\n\t\tworld.proxy.integrationParameters.normalizedAllowedLinearError = this.allowedLinearError();\n\t\tworld.proxy.integrationParameters.minIslandSize = this.minIslandSize();\n\t\tworld.proxy.integrationParameters.maxCcdSubsteps = this.maxCcdSubsteps();\n\t\tworld.proxy.integrationParameters.normalizedPredictionDistance = this.predictionDistance();\n\t\tworld.proxy.integrationParameters.contact_natural_frequency = this.contactNaturalFrequency();\n\t\tworld.proxy.lengthUnit = this.lengthUnit();\n\t}\n\n\tprivate internalStep(delta: number) {\n\t\tconst worldSingleton = this.worldSingleton();\n\t\tif (!worldSingleton) return;\n\n\t\tconst eventQueue = this.eventQueue();\n\t\tif (!eventQueue) return;\n\n\t\tconst world = worldSingleton.proxy;\n\t\tconst [timeStep, interpolate, paused] = [this.timeStep(), this.interpolate(), this.paused()];\n\n\t\t/* Check if the timestep is supposed to be variable. We'll do this here\n once so we don't have to string-check every frame. */\n\t\tconst timeStepVariable = timeStep === 'vary';\n\n\t\t/**\n\t\t * Fixed timeStep simulation progression.\n\t\t * @see https://gafferongames.com/post/fix_your_timestep/\n\t\t */\n\t\tconst clampedDelta = MathUtils.clamp(delta, 0, 0.5);\n\n\t\tconst stepWorld = (innerDelta: number) => {\n\t\t\t// Trigger beforeStep callbacks\n\t\t\tthis.beforeStepCallbacks.forEach((callback) => {\n\t\t\t\tcallback(world);\n\t\t\t});\n\n\t\t\tworld.timestep = innerDelta;\n\t\t\tworld.step(eventQueue);\n\n\t\t\t// Trigger afterStep callbacks\n\t\t\tthis.afterStepCallbacks.forEach((callback) => {\n\t\t\t\tcallback(world);\n\t\t\t});\n\t\t};\n\n\t\tif (timeStepVariable) {\n\t\t\tstepWorld(clampedDelta);\n\t\t} else {\n\t\t\t// don't step time forwards if paused\n\t\t\t// Increase accumulator\n\t\t\tthis.steppingState.accumulator += clampedDelta;\n\n\t\t\twhile (this.steppingState.accumulator >= timeStep) {\n\t\t\t\t// Set up previous state\n\t\t\t\t// needed for accurate interpolations if the world steps more than once\n\t\t\t\tif (interpolate) {\n\t\t\t\t\tthis.steppingState.previousState = {};\n\t\t\t\t\tworld.forEachRigidBody((body) => {\n\t\t\t\t\t\tthis.steppingState.previousState[body.handle] = {\n\t\t\t\t\t\t\tposition: body.translation(),\n\t\t\t\t\t\t\trotation: body.rotation(),\n\t\t\t\t\t\t};\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tstepWorld(timeStep);\n\t\t\t\tthis.steppingState.accumulator -= timeStep;\n\t\t\t}\n\t\t}\n\n\t\tconst interpolationAlpha =\n\t\t\ttimeStepVariable || !interpolate || paused ? 1 : this.steppingState.accumulator / timeStep;\n\n\t\t// Update meshes\n\t\tthis.rigidBodyStates.forEach((state, handle) => {\n\t\t\tconst rigidBody = world.getRigidBody(handle);\n\n\t\t\tconst events = this.rigidBodyEvents.get(handle);\n\t\t\tif (events?.onSleep || events?.onWake) {\n\t\t\t\tif (rigidBody.isSleeping() && !state.isSleeping) events?.onSleep?.();\n\t\t\t\tif (!rigidBody.isSleeping() && state.isSleeping) events?.onWake?.();\n\t\t\t\tstate.isSleeping = rigidBody.isSleeping();\n\t\t\t}\n\n\t\t\tif (!rigidBody || (rigidBody.isSleeping() && !('isInstancedMesh' in state.object)) || !state.setMatrix) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// New states\n\t\t\tlet t = rigidBody.translation() as Vector3;\n\t\t\tlet r = rigidBody.rotation() as Quaternion;\n\n\t\t\tlet previousState = this.steppingState.previousState[handle];\n\n\t\t\tif (previousState) {\n\t\t\t\t// Get previous simulated world position\n\t\t\t\t_matrix4\n\t\t\t\t\t.compose(previousState.position as Vector3, rapierQuaternionToQuaternion(previousState.rotation), state.scale)\n\t\t\t\t\t.premultiply(state.invertedWorldMatrix)\n\t\t\t\t\t.decompose(_position, _rotation, _scale);\n\n\t\t\t\t// Apply previous tick position\n\t\t\t\tif (state.meshType == 'mesh') {\n\t\t\t\t\tstate.object.position.copy(_position);\n\t\t\t\t\tstate.object.quaternion.copy(_rotation);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Get new position\n\t\t\t_matrix4\n\t\t\t\t.compose(t, rapierQuaternionToQuaternion(r), state.scale)\n\t\t\t\t.premultiply(state.invertedWorldMatrix)\n\t\t\t\t.decompose(_position, _rotation, _scale);\n\n\t\t\tif (state.meshType == 'instancedMesh') {\n\t\t\t\tstate.setMatrix(_matrix4);\n\t\t\t} else {\n\t\t\t\t// Interpolate to new position\n\t\t\t\tstate.object.position.lerp(_position, interpolationAlpha);\n\t\t\t\tstate.object.quaternion.slerp(_rotation, interpolationAlpha);\n\t\t\t}\n\t\t});\n\n\t\teventQueue.drainCollisionEvents((handle1, handle2, started) => {\n\t\t\tconst source1 = this.getSourceFromColliderHandle(handle1);\n\t\t\tconst source2 = this.getSourceFromColliderHandle(handle2);\n\n\t\t\t// Collision Events\n\t\t\tif (!source1?.collider.object || !source2?.collider.object) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst collisionPayload1 = this.getCollisionPayloadFromSource(source1, source2);\n\t\t\tconst collisionPayload2 = this.getCollisionPayloadFromSource(source2, source1);\n\n\t\t\tif (started) {\n\t\t\t\tworld.contactPair(source1.collider.object, source2.collider.object, (manifold, flipped) => {\n\t\t\t\t\t/* RigidBody events */\n\t\t\t\t\tsource1.rigidBody.events?.onCollisionEnter?.({ ...collisionPayload1, manifold, flipped });\n\t\t\t\t\tsource2.rigidBody.events?.onCollisionEnter?.({ ...collisionPayload2, manifold, flipped });\n\n\t\t\t\t\t/* Collider events */\n\t\t\t\t\tsource1.collider.events?.onCollisionEnter?.({ ...collisionPayload1, manifold, flipped });\n\t\t\t\t\tsource2.collider.events?.onCollisionEnter?.({ ...collisionPayload2, manifold, flipped });\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tsource1.rigidBody.events?.onCollisionExit?.(collisionPayload1);\n\t\t\t\tsource2.rigidBody.events?.onCollisionExit?.(collisionPayload2);\n\t\t\t\tsource1.collider.events?.onCollisionExit?.(collisionPayload1);\n\t\t\t\tsource2.collider.events?.onCollisionExit?.(collisionPayload2);\n\t\t\t}\n\n\t\t\t// Sensor Intersections\n\t\t\tif (started) {\n\t\t\t\tif (world.intersectionPair(source1.collider.object, source2.collider.object)) {\n\t\t\t\t\tsource1.rigidBody.events?.onIntersectionEnter?.(collisionPayload1);\n\t\t\t\t\tsource2.rigidBody.events?.onIntersectionEnter?.(collisionPayload2);\n\t\t\t\t\tsource1.collider.events?.onIntersectionEnter?.(collisionPayload1);\n\t\t\t\t\tsource2.collider.events?.onIntersectionEnter?.(collisionPayload2);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tsource1.rigidBody.events?.onIntersectionExit?.(collisionPayload1);\n\t\t\t\tsource2.rigidBody.events?.onIntersectionExit?.(collisionPayload2);\n\t\t\t\tsource1.collider.events?.onIntersectionExit?.(collisionPayload1);\n\t\t\t\tsource2.collider.events?.onIntersectionExit?.(collisionPayload2);\n\t\t\t}\n\t\t});\n\n\t\teventQueue.drainContactForceEvents((event) => {\n\t\t\tconst source1 = this.getSourceFromColliderHandle(event.collider1());\n\t\t\tconst source2 = this.getSourceFromColliderHandle(event.collider2());\n\n\t\t\t// Collision Events\n\t\t\tif (!source1?.collider.object || !source2?.collider.object) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst collisionPayload1 = this.getCollisionPayloadFromSource(source1, source2);\n\t\t\tconst collisionPayload2 = this.getCollisionPayloadFromSource(source2, source1);\n\n\t\t\tsource1.rigidBody.events?.onContactForce?.({\n\t\t\t\t...collisionPayload1,\n\t\t\t\ttotalForce: event.totalForce(),\n\t\t\t\ttotalForceMagnitude: event.totalForceMagnitude(),\n\t\t\t\tmaxForceDirection: event.maxForceDirection(),\n\t\t\t\tmaxForceMagnitude: event.maxForceMagnitude(),\n\t\t\t});\n\n\t\t\tsource2.rigidBody.events?.onContactForce?.({\n\t\t\t\t...collisionPayload2,\n\t\t\t\ttotalForce: event.totalForce(),\n\t\t\t\ttotalForceMagnitude: event.totalForceMagnitude(),\n\t\t\t\tmaxForceDirection: event.maxForceDirection(),\n\t\t\t\tmaxForceMagnitude: event.maxForceMagnitude(),\n\t\t\t});\n\n\t\t\tsource1.collider.events?.onContactForce?.({\n\t\t\t\t...collisionPayload1,\n\t\t\t\ttotalForce: event.totalForce(),\n\t\t\t\ttotalForceMagnitude: event.totalForceMagnitude(),\n\t\t\t\tmaxForceDirection: event.maxForceDirection(),\n\t\t\t\tmaxForceMagnitude: event.maxForceMagnitude(),\n\t\t\t});\n\n\t\t\tsource2.collider.events?.onContactForce?.({\n\t\t\t\t...collisionPayload2,\n\t\t\t\ttotalForce: event.totalForce(),\n\t\t\t\ttotalForceMagnitude: event.totalForceMagnitude(),\n\t\t\t\tmaxForceDirection: event.maxForceDirection(),\n\t\t\t\tmaxForceMagnitude: event.maxForceMagnitude(),\n\t\t\t});\n\t\t});\n\n\t\tworld.forEachActiveRigidBody(() => {\n\t\t\tthis.store.snapshot.invalidate();\n\t\t});\n\t}\n\n\tprivate getSourceFromColliderHandle(handle: ColliderHandle) {\n\t\tconst world = this.worldSingleton();\n\t\tif (!world) return;\n\n\t\tconst collider = world.proxy.getCollider(handle);\n\t\tconst colEvents = this.colliderEvents.get(handle);\n\t\tconst colliderState = this.colliderStates.get(handle);\n\n\t\tconst rigidBodyHandle = collider.parent()?.handle;\n\t\tconst rigidBody = rigidBodyHandle !== undefined ? world.proxy.getRigidBody(rigidBodyHandle) : undefined;\n\t\tconst rigidBodyEvents =\n\t\t\trigidBody && rigidBodyHandle !== undefined ? this.rigidBodyEvents.get(rigidBodyHandle) : undefined;\n\t\tconst rigidBodyState = rigidBodyHandle !== undefined ? this.rigidBodyStates.get(rigidBodyHandle) : undefined;\n\n\t\treturn {\n\t\t\tcollider: { object: collider, events: colEvents, state: colliderState },\n\t\t\trigidBody: { object: rigidBody, events: rigidBodyEvents, state: rigidBodyState },\n\t\t} as NgtrCollisionSource;\n\t}\n\n\tprivate getCollisionPayloadFromSource(target: NgtrCollisionSource, other: NgtrCollisionSource): NgtrCollisionPayload {\n\t\treturn {\n\t\t\ttarget: {\n\t\t\t\trigidBody: target.rigidBody.object,\n\t\t\t\tcollider: target.collider.object,\n\t\t\t\tcolliderObject: target.collider.state?.object,\n\t\t\t\trigidBodyObject: target.rigidBody.state?.object,\n\t\t\t},\n\t\t\tother: {\n\t\t\t\trigidBody: other.rigidBody.object,\n\t\t\t\tcollider: other.collider.object,\n\t\t\t\tcolliderObject: other.collider.state?.object,\n\t\t\t\trigidBodyObject: other.rigidBody.state?.object,\n\t\t\t},\n\t\t};\n\t}\n}\n","import {\n\tChangeDetectionStrategy,\n\tComponent,\n\tcomputed,\n\tCUSTOM_ELEMENTS_SCHEMA,\n\tDirective,\n\teffect,\n\tElementRef,\n\tinject,\n\tinput,\n\tmodel,\n\toutput,\n\tuntracked,\n} from '@angular/core';\nimport { ActiveEvents, Collider, ColliderDesc, RigidBody, RigidBodyDesc } from '@dimforge/rapier3d-compat';\nimport {\n\tapplyProps,\n\textend,\n\tgetEmitter,\n\tgetLocalState,\n\thasListener,\n\tNgtEuler,\n\tNgtObject3D,\n\tNgtQuaternion,\n\tNgtVector3,\n\tpick,\n} from 'angular-three';\nimport { mergeInputs } from 'ngxtension/inject-inputs';\nimport { Matrix4, Object3D, Vector3 } from 'three';\nimport { NgtrPhysics } from './physics';\nimport { _matrix4, _position, _rotation, _scale, _vector3 } from './shared';\nimport {\n\tNgtrColliderOptions,\n\tNgtrColliderShape,\n\tNgtrColliderState,\n\tNgtrCollisionEnterPayload,\n\tNgtrCollisionExitPayload,\n\tNgtrContactForcePayload,\n\tNgtrIntersectionEnterPayload,\n\tNgtrIntersectionExitPayload,\n\tNgtrRigidBodyOptions,\n\tNgtrRigidBodyState,\n\tNgtrRigidBodyType,\n} from './types';\nimport { createColliderOptions } from './utils';\n\nconst colliderDefaultOptions: NgtrColliderOptions = {\n\tcontactSkin: 0,\n};\n\n@Directive({ selector: 'ngt-object3D[ngtrCollider]' })\nexport class NgtrAnyCollider {\n\tposition = input<NgtVector3 | undefined>([0, 0, 0]);\n\trotation = input<NgtEuler | undefined>([0, 0, 0]);\n\tscale = input<NgtVector3 | undefined>([1, 1, 1]);\n\tquaternion = input<NgtQuaternion | undefined>([0, 0, 0, 1]);\n\tuserData = input<NgtObject3D['userData'] | undefined>({});\n\tname = input<NgtObject3D['name'] | undefined>();\n\toptions = input(colliderDefaultOptions, { transform: mergeInputs(rigidBodyDefaultOptions) });\n\n\tprivate object3DParameters = computed(() => {\n\t\treturn {\n\t\t\tposition: this.position(),\n\t\t\trotation: this.rotation(),\n\t\t\tscale: this.scale(),\n\t\t\tquaternion: this.quaternion(),\n\t\t\tuserData: this.userData(),\n\t\t\tname: this.name(),\n\t\t};\n\t});\n\n\t// TODO: change this to input required when Angular allows setting hostDirective input\n\tshape = model<NgtrColliderShape | undefined>(undefined, { alias: 'ngtrCollider' });\n\targs = model<unknown[]>([]);\n\n\tcollisionEnter = output<NgtrCollisionEnterPayload>();\n\tcollisionExit = output<NgtrCollisionExitPayload>();\n\tintersectionEnter = output<NgtrIntersectionEnterPayload>();\n\tintersectionExit = output<NgtrIntersectionExitPayload>();\n\tcontactForce = output<NgtrContactForcePayload>();\n\n\tprivate sensor = pick(this.options, 'sensor');\n\tprivate collisionGroups = pick(this.options, 'collisionGroups');\n\tprivate solverGroups = pick(this.options, 'solverGroups');\n\tprivate friction = pick(this.options, 'friction');\n\tprivate frictionCombineRule = pick(this.options, 'frictionCombineRule');\n\tprivate restitution = pick(this.options, 'restitution');\n\tprivate restitutionCombineRule = pick(this.options, 'restitutionCombineRule');\n\tprivate activeCollisionTypes = pick(this.options, 'activeCollisionTypes');\n\tprivate contactSkin = pick(this.options, 'contactSkin');\n\tprivate mass = pick(this.options, 'mass');\n\tprivate massProperties = pick(this.options, 'massProperties');\n\tprivate density = pick(this.options, 'density');\n\n\tprivate rigidBody = inject(NgtrRigidBody, { optional: true });\n\tprivate physics = inject(NgtrPhysics);\n\tprivate objectRef = inject<ElementRef<Object3D>>(ElementRef);\n\n\tprivate scaledArgs = computed(() => {\n\t\tconst [shape, args] = [\n\t\t\tthis.shape(),\n\t\t\tthis.args() as (number | ArrayLike<number> | { x: number; y: number; z: number })[],\n\t\t];\n\n\t\tconst cloned = args.slice();\n\n\t\t// Heightfield uses a vector\n\t\tif (shape === 'heightfield') {\n\t\t\tconst s = cloned[3] as { x: number; y: number; z: number };\n\t\t\ts.x *= this.worldScale.x;\n\t\t\ts.y *= this.worldScale.y;\n\t\t\ts.z *= this.worldScale.z;\n\n\t\t\treturn cloned;\n\t\t}\n\n\t\t// Trimesh and convex scale the vertices\n\t\tif (shape === 'trimesh' || shape === 'convexHull') {\n\t\t\tcloned[0] = this.scaleVertices(cloned[0] as ArrayLike<number>, this.worldScale);\n\t\t\treturn cloned;\n\t\t}\n\n\t\t// prefill with some extra\n\t\tconst scaleArray = [this.worldScale.x, this.worldScale.y, this.worldScale.z, this.worldScale.x, this.worldScale.x];\n\t\treturn cloned.map((arg, index) => scaleArray[index] * (arg as number));\n\t});\n\n\tprivate collider = computed(() => {\n\t\tconst worldSingleton = this.physics.worldSingleton();\n\t\tif (!worldSingleton) return null;\n\n\t\tconst [shape, args, rigidBody] = [this.shape(), this.scaledArgs(), this.rigidBody?.rigidBody()];\n\n\t\t// @ts-expect-error - we know the type of the data\n\t\tconst desc = ColliderDesc[shape](...args);\n\t\tif (!desc) return null;\n\n\t\treturn worldSingleton.proxy.createCollider(desc, rigidBody ?? undefined);\n\t});\n\n\tconstructor() {\n\t\textend({ Object3D });\n\n\t\teffect(() => {\n\t\t\tconst object3DParameters = this.object3DParameters();\n\t\t\tapplyProps(this.objectRef.nativeElement, object3DParameters);\n\t\t});\n\n\t\teffect((onCleanup) => {\n\t\t\tconst cleanup = this.createColliderStateEffect();\n\t\t\tonCleanup(() => cleanup?.());\n\t\t});\n\n\t\teffect((onCleanup) => {\n\t\t\tconst cleanup = this.createColliderEventsEffect();\n\t\t\tonCleanup(() => cleanup?.());\n\t\t});\n\n\t\teffect(() => {\n\t\t\tthis.updateColliderEffect();\n\t\t\tthis.updateMassPropertiesEffect();\n\t\t});\n\t}\n\n\tget worldScale() {\n\t\treturn this.objectRef.nativeElement.getWorldScale(new Vector3());\n\t}\n\n\tsetShape(shape: NgtrColliderShape) {\n\t\tthis.shape.set(shape);\n\t}\n\n\tsetArgs(args: unknown[]) {\n\t\tthis.args.set(args);\n\t}\n\n\tprivate createColliderStateEffect() {\n\t\tconst collider = this.collider();\n\t\tif (!collider) return;\n\n\t\tconst worldSingleton = this.physics.worldSingleton();\n\t\tif (!worldSingleton) return;\n\n\t\tconst localState = getLocalState(this.objectRef.nativeElement);\n\t\tif (!localState) return;\n\n\t\tconst parent = localState.parent();\n\t\tif (!parent || !(parent as Object3D).isObject3D) return;\n\n\t\tconst state = this.createColliderState(\n\t\t\tcollider,\n\t\t\tthis.objectRef.nativeElement,\n\t\t\tthis.rigidBody?.objectRef.nativeElement,\n\t\t);\n\t\tthis.physics.colliderStates.set(collider.handle, state);\n\n\t\treturn () => {\n\t\t\tthis.physics.colliderStates.delete(collider.handle);\n\t\t\tif (worldSingleton.proxy.getCollider(collider.handle)) {\n\t\t\t\tworldSingleton.proxy.removeCollider(collider, true);\n\t\t\t}\n\t\t};\n\t}\n\n\tprivate createColliderEventsEffect() {\n\t\tconst collider = this.collider();\n\t\tif (!collider) return;\n\n\t\tconst worldSingleton = this.physics.worldSingleton();\n\t\tif (!worldSingleton) return;\n\n\t\tconst collisionEnter = getEmitter(this.collisionEnter);\n\t\tconst collisionExit = getEmitter(this.collisionExit);\n\t\tconst intersectionEnter = getEmitter(this.intersectionEnter);\n\t\tconst intersectionExit = getEmitter(this.intersectionExit);\n\t\tconst contactForce = getEmitter(this.contactForce);\n\n\t\tconst hasCollisionEvent = hasListener(\n\t\t\tthis.collisionEnter,\n\t\t\tthis.collisionExit,\n\t\t\tthis.intersectionEnter,\n\t\t\tthis.intersectionExit,\n\t\t\tthis.rigidBody?.collisionEnter,\n\t\t\tthis.rigidBody?.collisionExit,\n\t\t\tthis.rigidBody?.intersectionEnter,\n\t\t\tthis.rigidBody?.intersectionExit,\n\t\t);\n\t\tconst hasContactForceEvent = hasListener(this.contactForce, this.rigidBody?.contactForce);\n\n\t\tif (hasCollisionEvent && hasContactForceEvent) {\n\t\t\tcollider.setActiveEvents(ActiveEvents.COLLISION_EVENTS | ActiveEvents.CONTACT_FORCE_EVENTS);\n\t\t} else if (hasCollisionEvent) {\n\t\t\tcollider.setActiveEvents(ActiveEvents.COLLISION_EVENTS);\n\t\t} else if (hasContactForceEvent) {\n\t\t\tcollider.setActiveEvents(ActiveEvents.CONTACT_FORCE_EVENTS);\n\t\t}\n\n\t\tthis.physics.colliderEvents.set(collider.handle, {\n\t\t\tonCollisionEnter: collisionEnter,\n\t\t\tonCollisionExit: collisionExit,\n\t\t\tonIntersectionEnter: intersectionEnter,\n\t\t\tonIntersectionExit: intersectionExit,\n\t\t\tonContactForce: contactForce,\n\t\t});\n\t\treturn () => {\n\t\t\tthis.physics.colliderEvents.delete(collider.handle);\n\t\t};\n\t}\n\n\tprivate updateColliderEffect() {\n\t\tconst collider = this.collider();\n\t\tif (!collider) return;\n\n\t\tconst worldSingleton = this.physics.worldSingleton();\n\t\tif (!worldSingleton) return;\n\n\t\tconst state = this.physics.colliderStates.get(collider.handle);\n\t\tif (!state) return;\n\n\t\t// Update collider position based on the object's position\n\t\tconst parentWorldScale = state.object.parent!.getWorldScale(_vector3);\n\t\tconst parentInvertedWorldMatrix = state.worldParent?.matrixWorld.clone().invert();\n\n\t\tstate.object.updateWorldMatrix(true, false);\n\n\t\t_matrix4.copy(state.object.matrixWorld);\n\n\t\tif (parentInvertedWorldMatrix) {\n\t\t\t_matrix4.premultiply(parentInvertedWorldMatrix);\n\t\t}\n\n\t\t_matrix4.decompose(_position, _rotation, _scale);\n\n\t\tif (collider.parent()) {\n\t\t\tcollider.setTranslationWrtParent({\n\t\t\t\tx: _position.x * parentWorldScale.x,\n\t\t\t\ty: _position.y * parentWorldScale.y,\n\t\t\t\tz: _position.z * parentWorldScale.z,\n\t\t\t});\n\t\t\tcollider.setRotationWrtParent(_rotation);\n\t\t} else {\n\t\t\tcollider.setTranslation({\n\t\t\t\tx: _position.x * parentWorldScale.x,\n\t\t\t\ty: _position.y * parentWorldScale.y,\n\t\t\t\tz: _position.z * parentWorldScale.z,\n\t\t\t});\n\t\t\tcollider.setRotation(_rotation);\n\t\t}\n\n\t\tconst [\n\t\t\tsensor,\n\t\t\tcollisionGroups,\n\t\t\tsolverGroups,\n\t\t\tfriction,\n\t\t\tfrictionCombineRule,\n\t\t\trestitution,\n\t\t\trestitutionCombineRule,\n\t\t\tactiveCollisionTypes,\n\t\t\tcontactSkin,\n\t\t] = [\n\t\t\tthis.sensor(),\n\t\t\tthis.collisionGroups(),\n\t\t\tthis.solverGroups(),\n\t\t\tthis.friction(),\n\t\t\tthis.frictionCombineRule(),\n\t\t\tthis.restitution(),\n\t\t\tthis.restitutionCombineRule(),\n\t\t\tthis.activeCollisionTypes(),\n\t\t\tthis.contactSkin(),\n\t\t];\n\n\t\tif (sensor !== undefined) collider.setSensor(sensor);\n\t\tif (collisionGroups !== undefined) collider.setCollisionGroups(collisionGroups);\n\t\tif (solverGroups !== undefined) collider.setSolverGroups(solverGroups);\n\t\tif (friction !== undefined) collider.setFriction(friction);\n\t\tif (frictionCombineRule !== undefined) collider.setFrictionCombineRule(frictionCombineRule);\n\t\tif (restitution !== undefined) collider.setRestitution(restitution);\n\t\tif (restitutionCombineRule !== undefined) collider.setRestitutionCombineRule(restitutionCombineRule);\n\t\tif (activeCollisionTypes !== undefined) collider.setActiveCollisionTypes(activeCollisionTypes);\n\t\tif (contactSkin !== undefined) collider.setContactSkin(contactSkin);\n\t}\n\n\tprivate updateMassPropertiesEffect() {\n\t\tconst collider = this.collider();\n\t\tif (!collider) return;\n\n\t\tconst [mass, massProperties, density] = [this.mass(), this.massProperties(), this.density()];\n\n\t\tif (density !== undefined) {\n\t\t\tif (mass !== undefined || massProperties !== undefined) {\n\t\t\t\tthrow new Error('[NGT Rapier] Cannot set mass and massProperties along with density');\n\t\t\t}\n\n\t\t\tcollider.setDensity(density);\n\t\t\treturn;\n\t\t}\n\n\t\tif (mass !== undefined) {\n\t\t\tif (massProperties !== undefined) {\n\t\t\t\tthrow new Error('[NGT Rapier] Cannot set massProperties along with mass');\n\t\t\t}\n\t\t\tcollider.setMass(mass);\n\t\t\treturn;\n\t\t}\n\n\t\tif (massProperties !== undefined) {\n\t\t\tcollider.setMassProperties(\n\t\t\t\tmassProperties.mass,\n\t\t\t\tmassProperties.centerOfMass,\n\t\t\t\tmassProperties.principalAngularInertia,\n\t\t\t\tmassProperties.angularInertiaLocalFrame,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t}\n\n\tprivate createColliderState(\n\t\tcollider: Collider,\n\t\tobject: Object3D,\n\t\trigidBodyObject?: Object3D | null,\n\t): NgtrColliderState {\n\t\treturn { collider, worldParent: rigidBodyObject || undefined, object };\n\t}\n\n\tprivate scaleVertices(vertices: ArrayLike<number>, scale: Vector3) {\n\t\tconst scaledVerts = Array.from(vertices);\n\n\t\tfor (let i = 0; i < vertices.length / 3; i++) {\n\t\t\tscaledVerts[i * 3] *= scale.x;\n\t\t\tscaledVerts[i * 3 + 1] *= scale.y;\n\t\t\tscaledVerts[i * 3 + 2] *= scale.z;\n\t\t}\n\n\t\treturn scaledVerts;\n\t}\n}\n\nconst RIGID_BODY_TYPE_MAP: Record<NgtrRigidBodyType, number> = {\n\tfixed: 1,\n\tdynamic: 0,\n\tkinematicPosition: 2,\n\tkinematicVelocity: 3,\n};\n\nexport const rigidBodyDefaultOptions: NgtrRigidBodyOptions = {\n\tcanSleep: true,\n\tlinearVelocity: [0, 0, 0],\n\tangularVelocity: [0, 0, 0],\n\tgravityScale: 1,\n\tdominanceGroup: 0,\n\tccd: false,\n\tsoftCcdPrediction: 0,\n\tcontactSkin: 0,\n};\n\n@Component({\n\tselector: 'ngt-object3D[ngtrRigidBody]',\n\texportAs: 'rigidBody',\n\ttemplate: `\n\t\t<ng-content />\n\t\t@for (childColliderOption of childColliderOptions(); track $index) {\n\t\t\t<ngt-object3D\n\t\t\t\t[ngtrCollider]=\"childColliderOption.shape\"\n\t\t\t\t[args]=\"childColliderOption.args\"\n\t\t\t\t[position]=\"childColliderOption.position\"\n\t\t\t\t[rotation]=\"childColliderOption.rotation\"\n\t\t\t\t[scale]=\"childColliderOption.scale\"\n\t\t\t\t[name]=\"objectRef.nativeElement.name + '-collider-' + $index\"\n\t\t\t\t[options]=\"childColliderOption.colliderOptions\"\n\t\t\t/>\n\t\t}\n\t`,\n\tschemas: [CUSTOM_ELEMENTS_SCHEMA],\n\tchangeDetection: ChangeDetectionStrategy.OnPush,\n\timports: [NgtrAnyCollider],\n})\nexport class NgtrRigidBody {\n\ttype = input('dynamic', {\n\t\talias: 'ngtrRigidBody',\n\t\ttransform: (value: NgtrRigidBodyType | '' | undefined) => {\n\t\t\tif (value === '' || value === undefined) return 'dynamic' as NgtrRigidBodyType;\n\t\t\treturn value;\n\t\t},\n\t});\n\tposition = input<NgtVector3 | undefined>([0, 0, 0]);\n\trotation = input<NgtEuler | undefined>([0, 0, 0]);\n\tscale = input<NgtVector3 | undefined>([1, 1, 1]);\n\tquaternion = input<NgtQuaternion | undefined>([0, 0, 0, 1]);\n\tuserData = input<NgtObject3D['userData'] | undefined>({});\n\toptions = input(rigidBodyDefaultOptions, { transform: mergeInputs(rigidBodyDefaultOptions) });\n\n\tprivate object3DParameters = computed(() => {\n\t\treturn {\n\t\t\tposition: this.position(),\n\t\t\trotation: this.rotation(),\n\t\t\tscale: this.scale(),\n\t\t\tquaternion: this.quaternion(),\n\t\t\tuserData: this.userData(),\n\t\t};\n\t});\n\n\twake = output<void>();\n\tsleep = output<void>();\n\tcollisionEnter = output<NgtrCollisionEnterPayload>();\n\tcollisionExit = output<NgtrCollisionExitPayload>();\n\tintersectionEnter = output<NgtrIntersectionEnterPayload>();\n\tintersectionExit = output<NgtrIntersectionExitPayload>();\n\tcontactForce = output<NgtrContactForcePayload>();\n\n\tprivate canSleep = pick(this.options, 'canSleep');\n\tprivate colliders = pick(this.options, 'colliders');\n\tprivate transformState = pick(this.options, 'transformState');\n\tprivate gravityScale = pick(this.options, 'gravityScale');\n\tprivate dominanceGroup = pick(this.options, 'dominanceGroup');\n\tprivate ccd = pick(this.options, 'ccd');\n\tprivate softCcdPrediction = pick(this.options, 'softCcdPrediction');\n\tprivate additionalSolverIterations = pick(this.options, 'additionalSolverIterations');\n\tprivate linearDamping = pick(this.options, 'linearDamping');\n\tprivate angularDamping = pick(this.options, 'angularDamping');\n\tprivate lockRotations = pick(this.options, 'lockRotations');\n\tprivate lockTranslations = pick(this.options, 'lockTranslations');\n\tprivate enabledRotations = pick(this.options, 'enabledRotations');\n\tprivate enabledTranslations = pick(this.options, 'enabledTranslations');\n\tprivate angularVelocity = pick(this.options, 'angularVelocity');\n\tprivate linearVelocity = pick(this.options, 'linearVelocity');\n\n\tobjectRef = inject<ElementRef<Object3D>>(ElementRef);\n\tprivate physics = inject(NgtrPhysics);\n\n\tprivate bodyType = computed(() => RIGID_BODY_TYPE_MAP[this.type()]);\n\tprivate bodyDesc = computed(() => {\n\t\tconst [canSleep, bodyType] = [this.canSleep(), untracked(this.bodyType), this.colliders()];\n\t\treturn new RigidBodyDesc(bodyType).setCanSleep(canSleep);\n\t});\n\trigidBody = computed(() => {\n\t\tconst worldSingleton = this.physics.worldSingleton();\n\t\tif (!worldSingleton) return null;\n\t\treturn worldSingleton.proxy.createRigidBody(this.bodyDesc());\n\t});\n\n\tprotected childColliderOptions = computed(() => {\n\t\tconst colliders = this.colliders();\n\t\t// if self colliders is false explicitly, disable auto colliders for this object entirely.\n\t\tif (colliders === false) return [];\n\n\t\tconst physicsColliders = this.physics.colliders();\n\t\t// if physics colliders is false explicitly AND colliders is not set, disable auto colliders for this object entirely.\n\t\tif (physicsColliders === false && colliders === undefined) return [];\n\n\t\tconst options = untracked(this.options);\n\t\t// if colliders on object is not set, use physics colliders\n\t\tif (!options.colliders) options.colliders = physicsColliders;\n\n\t\tconst objectLocalState = getLocalState(this.objectRef.nativeElement);\n\t\tif (!objectLocalState) return [];\n\n\t\t// track object's parent and non-object children\n\t\tconst [parent] = [objectLocalState.parent(), objectLocalState.nonObjects()];\n\t\tif (!parent || !(parent as Object3D).isObject3D) return [];\n\n\t\treturn createColliderOptions(this.objectRef.nativeElement, options, true);\n\t});\n\n\tconstructor() {\n\t\textend({ Object3D });\n\n\t\teffect(() => {\n\t\t\tconst object3DParameters = this.object3DParameters();\n\t\t\tapplyProps(this.objectRef.nativeElement, object3DParameters);\n\t\t});\n\n\t\teffect((onCleanup) => {\n\t\t\tconst cleanup = this.createRigidBodyStateEffect();\n\t\t\tonCleanup(() => cleanup?.());\n\t\t});\n\n\t\teffect((onCleanup) => {\n\t\t\tconst cleanup = this.createRigidBodyEventsEffect();\n\t\t\tonCleanup(() => cleanup?.());\n\t\t});\n\n\t\teffect(() => {\n\t\t\tthis.updateRigidBodyEffect();\n\t\t});\n\t}\n\n\tprivate createRigidBodyStateEffect() {\n\t\tconst worldSingleton = this.physics.worldSingleton();\n\t\tif (!worldSingleton) return;\n\n\t\tconst body = this.rigidBody();\n\t\tif (!body) return;\n\n\t\tconst transformState = untracked(this.transformState);\n\n\t\tconst localState = getLocalState(this.objectRef.nativeElement);\n\t\tif (!localState) return;\n\n\t\tconst parent = localState.parent();\n\t\tif (!parent || !(parent as Object3D).isObject3D) return;\n\n\t\tconst state = this.createRigidBodyState(body, this.objectRef.nativeElement);\n\t\tthis.physics.rigidBodyStates.set(body.handle, transformState ? transformState(state) : state);\n\n\t\treturn () => {\n\t\t\tthis.physics.rigidBodyStates.delete(body.handle);\n\t\t\tif (worldSingleton.proxy.getRigidBody(body.handle)) {\n\t\t\t\tworldSingleton.proxy.removeRigidBody(body);\n\t\t\t}\n\t\t};\n\t}\n\n\tprivate createRigidBodyEventsEffect() {\n\t\tconst worldSingleton = this.physics.worldSingleton();\n\t\tif (!worldSingleton) return;\n\n\t\tconst body = this.rigidBody();\n\t\tif (!body) return;\n\n\t\tconst wake = getEmitter(this.wake);\n\t\tconst sleep = getEmitter(this.sleep);\n\t\tconst collisionEnter = getEmitter(this.collisionEnter);\n\t\tconst collisionExit = getEmitter(this.collisionExit);\n\t\tconst intersectionEnter = getEmitter(this.intersectionEnter);\n\t\tconst intersectionExit = getEmitter(this.intersectionExit);\n\t\tconst contactForce = getEmitter(this.contactForce);\n\n\t\tthis.physics.rigidBodyEvents.set(body.handle, {\n\t\t\tonWake: wake,\n\t\t\tonSleep: sleep,\n\t\t\tonCollisionEnter: collisionEnter,\n\t\t\tonCollisionExit: collisionExit,\n\t\t\tonIntersectionEnter: intersectionEnter,\n\t\t\tonIntersectionExit: intersectionExit,\n\t\t\tonContactForce: contactForce,\n\t\t});\n\n\t\treturn () => {\n\t\t\tthis.physics.rigidBodyEvents.delete(body.handle);\n\t\t};\n\t}\n\n\tprivate updateRigidBodyEffect() {\n\t\tconst worldSingleton = this.physics.worldSingleton();\n\t\tif (!worldSingleton) return;\n\n\t\tconst body = this.rigidBody();\n\t\tif (!body) return;\n\n\t\tconst state = this.physics.rigidBodyStates.get(body.handle);\n\t\tif (!state) return;\n\n\t\tstate.object.updateWorldMatrix(true, false);\n\t\t_matrix4.copy(state.object.matrixWorld).decompose(_position, _rotation, _scale);\n\t\tbody.setTranslation(_position, true);\n\t\tbody.setRotation(_rotation, true);\n\n\t\tconst [\n\t\t\tgravityScale,\n\t\t\tadditionalSolverIterations,\n\t\t\tlinearDamping,\n\t\t\tangularDamping,\n\t\t\tlockRotations,\n\t\t\tlockTranslations,\n\t\t\tenabledRotations,\n\t\t\tenabledTranslations,\n\t\t\tangularVelocity,\n\t\t\tlinearVelocity,\n\t\t\tccd,\n\t\t\tsoftCcdPrediction,\n\t\t\tdominanceGroup,\n\t\t\tuserData,\n\t\t\tbodyType,\n\t\t] = [\n\t\t\tthis.gravityScale(),\n\t\t\tthis.additionalSolverIterations(),\n\t\t\tthis.linearDamping(),\n\t\t\tthis.angularDamping(),\n\t\t\tthis.lockRotations(),\n\t\t\tthis.lockTranslations(),\n\t\t\tthis.enabledRotations(),\n\t\t\tthis.enabledTranslations(),\n\t\t\tthis.angularVelocity(),\n\t\t\tthis.linearVelocity(),\n\t\t\tthis.ccd(),\n\t\t\tthis.softCcdPrediction(),\n\t\t\tthis.dominanceGroup(),\n\t\t\tthis.userData(),\n\t\t\tthis.bodyType(),\n\t\t];\n\n\t\tbody.setGravityScale(gravityScale, true);\n\t\tif (additionalSolverIterations !== undefined) body.setAdditionalSolverIterations(additionalSolverIterations);\n\t\tif (linearDamping !== undefined) body.setLinearDamping(linearDamping);\n\t\tif (angularDamping !== undefined) body.setAngularDamping(angularDamping);\n\t\tbody.setDominanceGroup(dominanceGroup);\n\t\tif (enabledRotations !== undefined) body.setEnabledRotations(...enabledRotations, true);\n\t\tif (enabledTranslations !== undefined) body.setEnabledTranslations(...enabledTranslations, true);\n\t\tif (lockRotations !== undefined) body.lockRotations(lockRotations, true);\n\t\tif (lockTranslations !== undefined) body.lockTranslations(lockTranslations, true);\n\t\tbody.setAngvel({ x: angularVelocity[0], y: angularVelocity[1], z: angularVelocity[2] }, true);\n\t\tbody.setLinvel({ x: linearVelocity[0], y: linearVelocity[1], z: linearVelocity[2] }, true);\n\t\tbody.enableCcd(ccd);\n\t\tbody.setSoftCcdPrediction(softCcdPrediction);\n\t\tif (userData !== undefined) body.userData = userData;\n\t\tif (bodyType !== body.bodyType()) body.setBodyType(bodyType, true);\n\t}\n\n\tprivate createRigidBodyState(\n\t\trigidBody: RigidBody,\n\t\tobject: Object3D,\n\t\tsetMatrix?: (matrix: Matrix4) => void,\n\t\tgetMatrix?: (matrix: Matrix4) => Matrix4,\n\t\tworldScale?: Vector3,\n\t\tmeshType: NgtrRigidBodyState['meshType'] = 'mesh',\n\t) {\n\t\tobject.updateWorldMatrix(true, false);\n\t\tconst invertedWorldMatrix = object.parent!.matrixWorld.clone().invert();\n\t\treturn {\n\t\t\tobject,\n\t\t\trigidBody,\n\t\t\tinvertedWorldMatrix,\n\t\t\tsetMatrix: setMatrix\n\t\t\t\t? setMatrix\n\t\t\t\t: (matrix: Matrix4) => {\n\t\t\t\t\t\tobject.matrix.copy(matrix);\n\t\t\t\t\t},\n\t\t\tgetMatrix: getMatrix ? getMatrix : (matrix: Matrix4) => matrix.copy(object.matrix),\n\t\t\tscale: worldScale || object.getWorldScale(_scale).clone(),\n\t\t\tisSleeping: false,\n\t\t\tmeshType,\n\t\t};\n\t}\n}\n","import { Directive, effect, inject, input } from '@angular/core';\nimport { NgtrAnyCollider } from './rigid-body';\nimport {\n\tNgtrBallArgs,\n\tNgtrCapsuleArgs,\n\tNgtrConeArgs,\n\tNgtrConvexHullArgs,\n\tNgtrConvexMeshArgs,\n\tNgtrCuboidArgs,\n\tNgtrCylinderArgs,\n\tNgtrHeightfieldArgs,\n\tNgtrPolylineArgs,\n\tNgtrRoundConeArgs,\n\tNgtrRoundConvexHullArgs,\n\tNgtrRoundConvexMeshArgs,\n\tNgtrRoundCuboidArgs,\n\tNgtrRoundCylinderArgs,\n\tNgtrTrimeshArgs,\n} from './types';\n\n// NOTE: this is ok to use here since we're not exporting this short-cut\nconst ANY_COLLIDER_HOST_DIRECTIVE = {\n\tdirective: NgtrAnyCollider,\n\tinputs: ['options', 'name', 'scale', 'position', 'quaternion', 'rotation', 'userData'],\n\toutputs: ['collisionEnter', 'collisionExit', 'intersectionEnter', 'intersectionExit', 'contactForce'],\n};\n\n@Directive({ selector: 'ngt-object3D[ngtrCuboidCollider]',