angular-three-rapier
Version:
Physics Rapier for Angular Three
225 lines (220 loc) • 9.81 kB
JavaScript
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