UNPKG

angular-three-rapier

Version:
1 lines 187 kB
{"version":3,"file":"angular-three-rapier.mjs","sources":["../../../../libs/rapier/src/lib/interaction-groups.ts","../../../../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/lib/physics-hooks.ts","../../../../libs/rapier/src/angular-three-rapier.ts"],"sourcesContent":["import { computed, Directive, effect, inject, InjectionToken, input } from '@angular/core';\nimport { InteractionGroups } from '@dimforge/rapier3d-compat';\n\n/**\n * Calculates an InteractionGroup bitmask for use in the `collisionGroups` or `solverGroups`\n * properties of RigidBody or Collider components. The first argument represents a list of\n * groups the entity is in (expressed as numbers from 0 to 15). The second argument is a list\n * of groups that will be filtered against. When it is omitted, all groups are filtered against.\n *\n * @example\n * A RigidBody that is member of group 0 and will collide with everything from groups 0 and 1:\n *\n * ```tsx\n * <RigidBody collisionGroups={interactionGroups([0], [0, 1])} />\n * ```\n *\n * A RigidBody that is member of groups 0 and 1 and will collide with everything else:\n *\n * ```tsx\n * <RigidBody collisionGroups={interactionGroups([0, 1])} />\n * ```\n *\n * A RigidBody that is member of groups 0 and 1 and will not collide with anything:\n *\n * ```tsx\n * <RigidBody collisionGroups={interactionGroups([0, 1], [])} />\n * ```\n *\n * Please note that Rapier needs interaction filters to evaluate to true between _both_ colliding\n * entities for collision events to trigger.\n *\n * @param memberships Groups the collider is a member of. (Values can range from 0 to 15.)\n * @param filters Groups the interaction group should filter against. (Values can range from 0 to 15.)\n * @returns An InteractionGroup bitmask.\n */\nexport function interactionGroups(memberships: number | number[], filters?: number | number[]): InteractionGroups {\n\treturn (bitmask(memberships) << 16) + (filters !== undefined ? bitmask(filters) : 0b1111_1111_1111_1111);\n}\n\nfunction bitmask(groups: number | number[]): InteractionGroups {\n\treturn [groups].flat().reduce((acc, layer) => acc | (1 << layer), 0);\n}\n\n/**\n * Injection token for handlers that set collision groups on colliders.\n * Used internally by rigid bodies and attractors to apply interaction groups.\n */\nexport const COLLISION_GROUPS_HANDLER = new InjectionToken<\n\t() => undefined | ((interactionGroups: InteractionGroups) => void)\n>('COLLISION_GROUPS_HANDLER');\n\n/**\n * Directive for setting collision/solver groups on a rigid body or collider.\n * This allows filtering which objects can collide with each other.\n *\n * @example\n * ```html\n * <!-- Object in group 0, collides with groups 0 and 1 -->\n * <ngt-object3D rigidBody [interactionGroups]=\"[[0], [0, 1]]\">\n * <ngt-mesh>\n * <ngt-box-geometry />\n * </ngt-mesh>\n * </ngt-object3D>\n *\n * <!-- Object in groups 0 and 1, collides with everything -->\n * <ngt-object3D rigidBody [interactionGroups]=\"[[0, 1]]\">\n * <ngt-mesh>\n * <ngt-sphere-geometry />\n * </ngt-mesh>\n * </ngt-object3D>\n * ```\n */\n@Directive({ selector: 'ngt-object3D[interactionGroups]' })\nexport class NgtrInteractionGroups {\n\tinputs = input.required<[number | number[], (number | number[])?]>({ alias: 'interactionGroups' });\n\tinteractionGroups = computed(() => {\n\t\tconst [memberships, filters] = this.inputs();\n\t\treturn interactionGroups(memberships, filters);\n\t});\n\n\tconstructor() {\n\t\tconst collisionGroupsHandlerFn = inject(COLLISION_GROUPS_HANDLER, { host: true, optional: true });\n\n\t\teffect(() => {\n\t\t\tif (!collisionGroupsHandlerFn) return;\n\t\t\tconst handler = collisionGroupsHandlerFn();\n\t\t\tif (!handler) return;\n\t\t\thandler(this.interactionGroups());\n\t\t});\n\t}\n}\n","import {\n\tChangeDetectionStrategy,\n\tComponent,\n\tCUSTOM_ELEMENTS_SCHEMA,\n\tElementRef,\n\tinject,\n\tviewChild,\n} from '@angular/core';\nimport { beforeRender, extend } from 'angular-three';\nimport * as THREE from 'three';\nimport { Group, LineBasicMaterial, LineSegments } from 'three';\nimport { NgtrPhysics } from './physics';\n\n/**\n * Debug visualization component for the physics world.\n * Renders wireframe outlines of all colliders in the physics simulation.\n *\n * This component is automatically rendered when the `debug` option is set to `true`\n * on the `NgtrPhysics` component. It updates every frame to show current collider positions.\n *\n * @example\n * ```html\n * <ngtr-physics [options]=\"{ debug: true }\">\n * <ng-template>\n * <!-- Your physics scene -->\n * </ng-template>\n * </ngtr-physics>\n * ```\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 />\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\tprivate physics = inject(NgtrPhysics);\n\tprivate lineSegmentsRef = viewChild.required<ElementRef<THREE.LineSegments>>('lineSegments');\n\n\tconstructor() {\n\t\textend({ Group, LineSegments, LineBasicMaterial });\n\n\t\tbeforeRender(() => {\n\t\t\tconst worldSingleton = this.physics.worldSingleton();\n\t\t\tif (!worldSingleton) return;\n\n\t\t\tconst lineSegments = this.lineSegmentsRef().nativeElement;\n\t\t\tif (!lineSegments) return;\n\n\t\t\tconst buffers = worldSingleton.proxy.debugRender();\n\t\t\tconst geometry = new THREE.BufferGeometry();\n\t\t\tgeometry.setAttribute('position', new THREE.BufferAttribute(buffers.vertices, 3));\n\t\t\tgeometry.setAttribute('color', new THREE.BufferAttribute(buffers.colors, 4));\n\n\t\t\tlineSegments.geometry.dispose();\n\t\t\tlineSegments.geometry = geometry;\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/**\n * Internal directive that manages the physics simulation loop.\n * Handles both \"follow\" mode (tied to render loop) and \"independent\" mode (separate loop).\n *\n * This directive is used internally by NgtrPhysics and should not be used directly.\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 * as THREE from 'three';\n\n/**\n * Shared reusable Three.js objects for internal calculations.\n * These are used to avoid creating new objects on every frame,\n * which would cause garbage collection pressure.\n *\n * @internal\n */\n\n/** Shared quaternion for internal calculations */\nexport const _quaternion = new THREE.Quaternion();\n/** Shared euler angles for internal calculations */\nexport const _euler = new THREE.Euler();\n/** Shared vector3 for internal calculations */\nexport const _vector3 = new THREE.Vector3();\n/** Shared Object3D for internal calculations */\nexport const _object3d = new THREE.Object3D();\n/** Shared matrix4 for internal calculations */\nexport const _matrix4 = new THREE.Matrix4();\n/** Shared position vector for internal calculations */\nexport const _position = new THREE.Vector3();\n/** Shared rotation quaternion for internal calculations */\nexport const _rotation = new THREE.Quaternion();\n/** Shared scale vector for internal calculations */\nexport const _scale = new THREE.Vector3();\n","import { Quaternion as RapierQuaternion, Vector3 as RapierVector3 } from '@dimforge/rapier3d-compat';\nimport { is, NgtEuler, NgtQuaternion, NgtVector3 } from 'angular-three';\nimport * as THREE 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. This is useful for lazy initialization\n * of expensive objects like physics worlds.\n *\n * @template SingletonClass - The type of the singleton instance\n * @template CreationFn - The type of the factory function\n * @param createInstance - A function that returns a new instance of the class\n * @returns An object containing the proxy, reset function, and set function\n *\n * @example\n * ```typescript\n * const { proxy, reset } = createSingletonProxy(() => new World(gravity));\n * // Access the world lazily\n * proxy.step();\n * // Reset when done\n * reset();\n * ```\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\t/** The lazy proxy to the singleton instance */\n\tproxy: SingletonClass;\n\t/** Resets the singleton, allowing a new instance to be created */\n\treset: () => void;\n\t/** Sets the singleton to a specific instance */\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\n/**\n * Converts a Rapier quaternion to a Three.js quaternion.\n *\n * @param quaternion - The Rapier quaternion to convert\n * @returns A Three.js Quaternion with the same values\n */\nexport function rapierQuaternionToQuaternion({ x, y, z, w }: RapierQuaternion) {\n\treturn _quaternion.set(x, y, z, w);\n}\n\n/**\n * Converts an Angular Three vector representation to a Rapier Vector3.\n * Supports arrays, numbers (uniform scale), and objects with x, y, z properties.\n *\n * @param v - The vector to convert (can be [x, y, z], a number, or {x, y, z})\n * @returns A Rapier Vector3 instance\n *\n * @example\n * ```typescript\n * vector3ToRapierVector([1, 2, 3]); // RapierVector3(1, 2, 3)\n * vector3ToRapierVector(5); // RapierVector3(5, 5, 5)\n * vector3ToRapierVector({ x: 1, y: 2, z: 3 }); // RapierVector3(1, 2, 3)\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\n\treturn new RapierVector3(v.x, v.y, v.z);\n}\n\n/**\n * Converts an Angular Three quaternion representation to a Rapier Quaternion.\n * Supports arrays and objects with x, y, z, w properties.\n *\n * @param v - The quaternion to convert (can be [x, y, z, w] or {x, y, z, w})\n * @returns A Rapier Quaternion instance\n *\n * @example\n * ```typescript\n * quaternionToRapierQuaternion([0, 0, 0, 1]); // Identity quaternion\n * quaternionToRapierQuaternion({ x: 0, y: 0, z: 0, w: 1 }); // Identity quaternion\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\n\treturn new RapierQuaternion(v.x, v.y, v.z, v.w);\n}\n\nfunction isChildOfMeshCollider(child: THREE.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: THREE.BufferGeometry,\n\tcolliders: NgtrRigidBodyAutoCollider,\n): { args: unknown[]; offset: THREE.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 THREE.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 THREE.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: [\n\t\t\t\t\tclonedGeometry.attributes['position'].array as Float32Array,\n\t\t\t\t\tclonedGeometry.index?.array as Uint32Array,\n\t\t\t\t],\n\t\t\t\toffset: new THREE.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 THREE.Vector3(),\n\t\t\t};\n\t\t}\n\t}\n\n\treturn { args: [], offset: new THREE.Vector3() };\n}\n\n/**\n * Creates collider options by traversing child meshes of an object and generating\n * appropriate collider configurations based on their geometries.\n *\n * @param object - The parent Object3D to traverse for meshes\n * @param options - The rigid body options containing collider type and other settings\n * @param ignoreMeshColliders - Whether to skip meshes that are children of mesh colliders\n * @returns Array of collider configurations with shape, args, position, rotation, and scale\n *\n * @example\n * ```typescript\n * const colliderOptions = createColliderOptions(rigidBodyObject, { colliders: 'cuboid' });\n * // Returns array of collider configs for each mesh child\n * ```\n */\nexport function createColliderOptions(\n\tobject: THREE.Object3D,\n\toptions: NgtrRigidBodyOptions,\n\tignoreMeshColliders = true,\n) {\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: THREE.Object3D) => {\n\t\tif (is.three<THREE.Mesh>(child, 'isMesh')) {\n\t\t\tif (ignoreMeshColliders && isChildOfMeshCollider(child)) 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\n\t\t\t\t.copy(child.matrixWorld)\n\t\t\t\t.premultiply(invertedParentMatrixWorld)\n\t\t\t\t.decompose(_position, _rotation, _scale);\n\n\t\t\tconst rotationEuler = new THREE.Euler().setFromQuaternion(_rotation, 'XYZ');\n\n\t\t\tconst { args, offset } = getColliderArgsFromGeometry(child.geometry, options.colliders || 'cuboid');\n\t\t\tconst { mass, linearDamping, angularDamping, canSleep, ccd, gravityScale, softCcdPrediction, ...rest } =\n\t\t\t\toptions;\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, PhysicsHooks, Rotation, Vector, World } from '@dimforge/rapier3d-compat';\nimport { injectStore, pick, vector3 } from 'angular-three';\nimport { mergeInputs } from 'ngxtension/inject-inputs';\nimport * as THREE 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\tNgtrFilterContactPairCallback,\n\tNgtrFilterIntersectionPairCallback,\n\tNgtrPhysicsOptions,\n\tNgtrRigidBodyStateMap,\n\tNgtrWorldStepCallback,\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\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/**\n * Directive for providing a fallback template when Rapier fails to load.\n *\n * @example\n * ```html\n * <ngtr-physics>\n * <ng-template>\n * <!-- Physics scene content -->\n * </ng-template>\n * <ng-template rapierFallback let-error=\"error\">\n * <p>Failed to load physics: {{ error }}</p>\n * </ng-template>\n * </ngtr-physics>\n * ```\n */\n@Directive({ selector: 'ng-template[rapierFallback]' })\nexport class NgtrPhysicsFallback {\n\t/** Type guard for template context */\n\tstatic ngTemplateContextGuard(_: NgtrPhysicsFallback, ctx: unknown): ctx is { error: string } {\n\t\treturn true;\n\t}\n}\n\n/**\n * Main physics component that creates and manages a Rapier physics world.\n * Wrap your 3D scene content in this component to enable physics simulation.\n *\n * The component lazily loads the Rapier WASM module and provides the physics\n * context to all child components.\n *\n * @example\n * ```html\n * <ngtr-physics [options]=\"{ gravity: [0, -9.81, 0], debug: true }\">\n * <ng-template>\n * <ngt-object3D rigidBody>\n * <ngt-mesh>\n * <ngt-box-geometry />\n * <ngt-mesh-standard-material />\n * </ngt-mesh>\n * </ngt-object3D>\n * </ng-template>\n * </ngtr-physics>\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 />\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\t/** Physics configuration options */\n\toptions = input(defaultOptions, { transform: mergeInputs(defaultOptions) });\n\n\tprotected content = contentChild.required(TemplateRef);\n\tprotected fallbackContent = 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 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\t/** Whether the physics simulation is paused */\n\tpaused = pick(this.options, 'paused');\n\tprotected debug = pick(this.options, 'debug');\n\t/** The default collider type for automatic collider generation */\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\t/** The loaded Rapier module, null if not yet loaded */\n\trapier = this.rapierConstruct.asReadonly();\n\n\tprotected ready = computed(() => !!this.rapier());\n\t/** Singleton proxy to the Rapier physics world */\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\t/** Map of rigid body states indexed by handle */\n\trigidBodyStates: NgtrRigidBodyStateMap = new Map();\n\t/** Map of collider states indexed by handle */\n\tcolliderStates: NgtrColliderStateMap = new Map();\n\t/** Map of rigid body event handlers indexed by handle */\n\trigidBodyEvents: NgtrEventMap = new Map();\n\t/** Map of collider event handlers indexed by handle */\n\tcolliderEvents: NgtrEventMap = new Map();\n\t/** Callbacks to run before each physics step */\n\tbeforeStepCallbacks = new Set<NgtrWorldStepCallback>();\n\t/** Callbacks to run after each physics step */\n\tafterStepCallbacks = new Set<NgtrWorldStepCallback>();\n\t/** Callbacks to filter contact pairs */\n\tfilterContactPairCallbacks = new Set<NgtrFilterContactPairCallback>();\n\t/** Callbacks to filter intersection pairs */\n\tfilterIntersectionPairCallbacks = new Set<NgtrFilterIntersectionPairCallback>();\n\n\tprivate hooks: PhysicsHooks = {\n\t\tfilterContactPair: (...args) => {\n\t\t\tfor (const callback of this.filterContactPairCallbacks) {\n\t\t\t\tconst result = callback(...args);\n\t\t\t\tif (result !== null) return result;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t\tfilterIntersectionPair: (...args) => {\n\t\t\tfor (const callback of this.filterIntersectionPairCallbacks) {\n\t\t\t\tconst result = callback(...args);\n\t\t\t\tif (result === false) return false;\n\t\t\t}\n\t\t\treturn true;\n\t\t},\n\t};\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\t/**\n\t * Steps the physics simulation forward by the given delta time.\n\t * This is called automatically by the frame stepper, but can be called manually\n\t * if you need custom control over the simulation timing.\n\t *\n\t * @param delta - Time in seconds since the last step\n\t */\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.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 = THREE.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\tconst hasHooks = this.filterContactPairCallbacks.size > 0 || this.filterIntersectionPairCallbacks.size > 0;\n\t\t\tworld.step(eventQueue, hasHooks ? this.hooks : undefined);\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 THREE.Vector3;\n\t\t\tlet r = rigidBody.rotation() as THREE.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(\n\t\t\t\t\t\tpreviousState.position as THREE.Vector3,\n\t\t\t\t\t\trapierQuaternionToQuaternion(previousState.rotation),\n\t\t\t\t\t\tstate.scale,\n\t\t\t\t\t)\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(\n\t\ttarget: NgtrCollisionSource,\n\t\tother: NgtrCollisionSource,\n\t): 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\tviewChildren,\n} from '@angular/core';\nimport {\n\tActiveEvents,\n\tCollider,\n\tColliderDesc,\n\tInteractionGroups,\n\tRigidBody,\n\tRigidBodyDesc,\n} from '@dimforge/rapier3d-compat';\nimport {\n\tapplyProps,\n\textend,\n\tgetEmitter,\n\tgetInstanceState,\n\thasListener,\n\tis,\n\tNgtEuler,\n\tNgtQuaternion,\n\tNgtThreeElements,\n\tNgtVector3,\n\tpick,\n} from 'angular-three';\nimport { mergeInputs } from 'ngxtension/inject-inputs';\nimport * as THREE from 'three';\nimport { Object3D } from 'three';\nimport { COLLISION_GROUPS_HANDLER } from './interaction-groups';\nimport { NgtrPhysics } from './physics';\nimport { _matrix4, _position, _rotation, _scale, _vector3 } from './shared';\nimport type {\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/**\n * Base directive for creating colliders in the physics simulation.\n * This directive can be used to create any type of collider by specifying the shape.\n *\n * For convenience, use the specific collider directives like `NgtrCuboidCollider`,\n * `NgtrBallCollider`, etc., which provide type-safe arguments.\n *\n * @example\n * ```html\n * <ngt-object3D [collider]=\"'cuboid'\" [args]=\"[1, 1, 1]\" [position]=\"[0, 5, 0]\" />\n * ```\n */\n@Directive({ selector: 'ngt-object3D[collider]' })\nexport class NgtrAnyCollider {\n\tposition = input<NgtVector3>([0, 0, 0]);\n\trotation = input<NgtEuler>();\n\tscale = input<NgtVector3>([1, 1, 1]);\n\tquaternion = input<NgtQuaternion>();\n\tuserData = input<NgtThreeElements['ngt-object3D']['userData']>();\n\tname = input<NgtThreeElements['ngt-object3D']['name']>();\n\toptions = input(colliderDefaultOptions, { transform: mergeInputs(rigidBodyDefaultOptions) });\n\n\tprivate object3DParameters = computed(() => {\n\t\tconst [position, rotation, scale, quaternion, userData, name] = [\n\t\t\tthis.position(),\n\t\t\tthis.rotation(),\n\t\t\tthis.scale(),\n\t\t\tthis.quaternion(),\n\t\t\tthis.userData(),\n\t\t\tthis.name(),\n\t\t];\n\n\t\tconst parameters = { position, scale, userData, name: name || `${untracked(this.shape)}-${Date.now()}` };\n\n\t\tif (quaternion) {\n\t\t\tObject.assign(parameters, { quaternion });\n\t\t} else if (rotation) {\n\t\t\tObject.assign(parameters, { rotation });\n\t\t} else {\n\t\t\tObject.assign(parameters, { rotation: [0, 0, 0] });\n\t\t}\n\n\t\treturn parameters;\n\t});\n\n\t// TODO: change this to input required when Angular allows setting hostDirective input\n\tshape = model<NgtrColliderShape | undefined>(undefined, { alias: 'collider' });\n\t// NOTE: this will be typed by individual collider\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<THREE.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\tif (cloned.length === 0) return [];\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 = [\n\t\t\tthis.worldScale.x,\n\t\t\tthis.worldScale.y,\n\t\t\tthis.worldScale.z,\n\t\t\tthis.worldScale.x,\n\t\t\tthis.worldScale.x,\n\t\t];\n\t\treturn cloned.map((arg, index) => scaleArray[index] * (arg as number));\n\t});\n\n\tcollider = computed(() => {\n\t\tconst worldSingleton = this.physics.worldSingleton();\n\t\tif (!worldSingleton) return null;\n\n\t\tconst args = this.scaledArgs();\n\t\tif (!args.length) return null;\n\n\t\tconst [shape, rigidBody] = [this.shape(), 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 THREE.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 instanceState = getInstanceState(this.objectRef.nativeElement);\n\t\tif (!instanceState) return;\n\n\t\tconst parent = instanceState.parent();\n\t\tif (!parent || !is.three<THREE.Object3D>(parent, '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: THREE.Object3D,\n\t\trigidBodyObject?: THREE.Object3D | null,\n\t): NgtrColliderState {\n\t\treturn { collider, worldParent: rigidBodyObject || undefined, object };\n\t}\n\n\tprivate scaleVertices(vertices: ArrayLike<number>, scale: THREE.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\n/** Maps rigid body type strings to Rapier bod