UNPKG

angular-three-rapier

Version:
225 lines (220 loc) 9.81 kB
import * as i0 from '@angular/core'; import { inject, computed, effect, input, ElementRef, linkedSignal, untracked, Directive } from '@angular/core'; import { injectStore, pick, beforeRender, applyProps } from 'angular-three'; import { NgtrPhysics, beforePhysicsStep, COLLISION_GROUPS_HANDLER } from 'angular-three-rapier'; import { mergeInputs } from 'ngxtension/inject-inputs'; import * as THREE from 'three'; import { assertInjector } from 'ngxtension/assert-injector'; import { VertexNormalsHelper } from 'three-stdlib'; const _v3 = new THREE.Vector3(); /** * Creates debug visualization for an attractor. * Shows a sphere at the attractor position with normals indicating the range. * Blue indicates attraction, red indicates repulsion. * * This is automatically called by NgtrAttractor when physics debug mode is enabled. * * @param object - The Object3D representing the attractor position * @param options - Getter function for attractor options * @param injector - Optional injector for dependency injection context * * @internal */ function attractorDebug(object, options, injector) { return assertInjector(attractorDebug, injector, () => { const physics = inject(NgtrPhysics); const store = injectStore(); const strength = pick(options, 'strength'); const range = pick(options, 'range'); const color = computed(() => (strength() > 0 ? 0x0000ff : 0xff0000), ...(ngDevMode ? [{ debugName: "color" }] : [])); let mesh; let normalsHelper; effect((onCleanup) => { if (!physics['debug']()) return; mesh = new THREE.Mesh(new THREE.SphereGeometry(0.2, 6, 6), new THREE.MeshBasicMaterial({ color: color(), wireframe: true })); normalsHelper = new VertexNormalsHelper(mesh, range(), color()); normalsHelper.frustumCulled = false; store.snapshot.scene.add(mesh); store.snapshot.scene.add(normalsHelper); onCleanup(() => { if (mesh) { store.snapshot.scene.remove(mesh); } if (normalsHelper) { store.snapshot.scene.remove(normalsHelper); } }); }); beforeRender(() => { if (!physics['debug']() || !mesh || !normalsHelper) return; const worldPosition = object.getWorldPosition(_v3); mesh.position.copy(worldPosition); normalsHelper.update(); }); }); } /** * Force calculation functions for different gravity types. * - static: Constant force regardless of distance * - linear: Force increases linearly as distance decreases * - newtonian: Force follows Newton's law of universal gravitation */ const calcForceByType = { static: (s, m2, r, d, G) => s, linear: (s, m2, r, d, G) => s * (d / r), newtonian: (s, m2, r, d, G) => (G * s * m2) / Math.pow(d, 2), }; const _position = new THREE.Vector3(); const _vector3 = new THREE.Vector3(); /** * Applies attractor force to a rigid body based on its distance from the attractor. * Used internally by NgtrAttractor but can be called manually for custom behavior. * * @param rigidBody - The rigid body to apply the force to * @param options - The attractor configuration including position, strength, and type * * @example * ```typescript * beforePhysicsStep((world) => { * world.bodies.forEach((body) => { * if (body.isDynamic()) { * applyAttractorForceOnRigidBody(body, { * object: attractorMesh, * strength: 10, * range: 20, * type: 'newtonian', * gravitationalConstant: 6.673e-11 * }); * } * }); * }); * ``` */ function applyAttractorForceOnRigidBody(rigidBody, { object, strength, range, gravitationalConstant, collisionGroups, type, }) { const rbPosition = rigidBody.translation(); _position.set(rbPosition.x, rbPosition.y, rbPosition.z); const worldPosition = object.getWorldPosition(new THREE.Vector3()); const distance = worldPosition.distanceTo(_position); if (distance < range) { let force = calcForceByType[type](strength, rigidBody.mass(), range, distance, gravitationalConstant); // Prevent wild forces when Attractors collide force = force === Infinity ? strength : force; // Naively test if the rigidBody contains a collider in one of the collision groups let isRigidBodyInCollisionGroup = collisionGroups === undefined; if (collisionGroups !== undefined) { for (let i = 0; i < rigidBody.numColliders(); i++) { const collider = rigidBody.collider(i); const colliderCollisionGroups = collider.collisionGroups(); if (((collisionGroups >> 16) & colliderCollisionGroups) != 0 && ((colliderCollisionGroups >> 16) & collisionGroups) != 0) { isRigidBodyInCollisionGroup = true; break; } } } if (isRigidBodyInCollisionGroup) { _vector3.set(0, 0, 0).subVectors(worldPosition, _position).normalize().multiplyScalar(force); rigidBody.applyImpulse(_vector3, true); } } } const defaultOptions = { strength: 1, range: 10, type: 'static', gravitationalConstant: 6.673e-11, }; /** * Directive that creates a gravitational attractor point in the physics world. * All dynamic rigid bodies within range will be attracted (or repelled) towards this point. * * The attractor can use different gravity models: * - Static: constant force * - Linear: force increases as objects get closer * - Newtonian: realistic inverse-square law * * @example * ```html * <!-- Simple attractor at origin with default options --> * <ngt-object3D attractor /> * * <!-- Attractor with custom options --> * <ngt-object3D [attractor]="{ strength: 5, range: 20 }" /> * * <!-- Repeller (negative strength) --> * <ngt-object3D [attractor]="{ strength: -10, range: 15 }" [position]="[5, 0, 0]" /> * * <!-- Newtonian gravity --> * <ngt-object3D [attractor]="{ * strength: 1000, * range: 50, * type: 'newtonian', * gravitationalConstant: 0.01 * }" /> * ``` */ class NgtrAttractor { position = input([0, 0, 0], ...(ngDevMode ? [{ debugName: "position" }] : [])); options = input(defaultOptions, { ...(ngDevMode ? { debugName: "options" } : {}), alias: 'attractor', transform: mergeInputs(defaultOptions) }); objectRef = inject(ElementRef); collisionGroups = pick(this.options, 'collisionGroups'); linkedCollisionGroups = linkedSignal(this.collisionGroups, ...(ngDevMode ? [{ debugName: "linkedCollisionGroups" }] : [])); constructor() { effect(() => { applyProps(this.objectRef.nativeElement, { position: this.position() }); }); beforePhysicsStep((world) => { const { strength, range, type, gravitationalConstant } = untracked(this.options); const collisionGroups = untracked(this.linkedCollisionGroups); world.bodies.forEach((body) => { if (body.isDynamic()) { applyAttractorForceOnRigidBody(body, { object: this.objectRef.nativeElement, strength, range, type, gravitationalConstant, collisionGroups, }); } }); }); attractorDebug(this.objectRef.nativeElement, this.options); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: NgtrAttractor, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.6", type: NgtrAttractor, isStandalone: true, selector: "ngt-object3D[attractor]", inputs: { position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "attractor", isSignal: true, isRequired: false, transformFunction: null } }, providers: [ { provide: COLLISION_GROUPS_HANDLER, useFactory: (attractor) => { return () => (interactionGroups) => { attractor.linkedCollisionGroups.set(interactionGroups); }; }, deps: [NgtrAttractor], }, ], ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: NgtrAttractor, decorators: [{ type: Directive, args: [{ selector: 'ngt-object3D[attractor]', providers: [ { provide: COLLISION_GROUPS_HANDLER, useFactory: (attractor) => { return () => (interactionGroups) => { attractor.linkedCollisionGroups.set(interactionGroups); }; }, deps: [NgtrAttractor], }, ], }] }], ctorParameters: () => [], propDecorators: { position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "attractor", required: false }] }] } }); /** * Generated bundle index. Do not edit. */ export { NgtrAttractor, applyAttractorForceOnRigidBody }; //# sourceMappingURL=angular-three-rapier-addons.mjs.map