Skip to content

Commit b018754

Browse files
Chau TranChau Tran
Chau Tran
authored and
Chau Tran
committed
feat(cannon): add physics
1 parent 87e3bdb commit b018754

File tree

7 files changed

+384
-8
lines changed

7 files changed

+384
-8
lines changed

libs/cannon/ng-package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
"dest": "../../dist/libs/cannon",
44
"lib": {
55
"entryFile": "src/index.ts"
6-
}
6+
},
7+
"allowedNonPeerDependencies": ["@nx/devkit", "nx"]
78
}

libs/cannon/package.json

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,42 @@
11
{
22
"name": "angular-three-cannon",
3-
"version": "0.0.1",
3+
"version": "0.0.0-replace",
4+
"publishConfig": {
5+
"access": "public"
6+
},
7+
"repository": {
8+
"type": "git",
9+
"url": "https://github.com/angular-threejs/angular-three/tree/main/libs/cannon"
10+
},
11+
"author": {
12+
"name": "Chau Tran",
13+
"email": "nartc7789@gmail.com",
14+
"url": "https://nartc.me"
15+
},
16+
"description": "Cannon.js physics integration with Angular Three",
17+
"keywords": [
18+
"angular",
19+
"threejs",
20+
"renderer",
21+
"cannonjs",
22+
"physics"
23+
],
24+
"license": "MIT",
425
"peerDependencies": {
5-
"@angular/common": "^16.0.0",
6-
"@angular/core": "^16.0.0"
26+
"@angular/common": " ^16.0.0",
27+
"@angular/core": " ^16.0.0",
28+
"@pmndrs/cannon-worker-api": "^2.3.2",
29+
"angular-three": "^2.0.0",
30+
"cannon-es": "^0.20.0",
31+
"cannon-es-debugger": "^1.0.0",
32+
"three": "^0.148.0 || ^0.149.0 || ^0.150.0 || ^0.151.0 || ^0.152.0"
733
},
834
"dependencies": {
9-
"tslib": "^2.3.0"
35+
"tslib": "^2.3.0",
36+
"@nx/devkit": "^16.0.0",
37+
"nx": "^16.0.0"
1038
},
11-
"sideEffects": false
39+
"sideEffects": false,
40+
"generators": "./plugin/generators.json",
41+
"schematics": "./plugin/generators.json"
1242
}

libs/cannon/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1+
export * from './physics';

libs/cannon/src/physics.ts

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
import { Component, InjectionToken, Input, computed, effect, inject, untracked, type Signal } from '@angular/core';
2+
import {
3+
CannonWorkerAPI,
4+
type Broadphase,
5+
type CannonWorkerProps,
6+
type CollideBeginEvent,
7+
type CollideEndEvent,
8+
type CollideEvent,
9+
type ContactMaterialOptions,
10+
type RayhitEvent,
11+
type Refs,
12+
type Solver,
13+
type Subscriptions,
14+
type Triplet,
15+
type WorkerCollideBeginEvent,
16+
type WorkerCollideEndEvent,
17+
type WorkerCollideEvent,
18+
type WorkerFrameMessage,
19+
type WorkerRayhitEvent,
20+
} from '@pmndrs/cannon-worker-api';
21+
import {
22+
NgtSignalStore,
23+
NgtStore,
24+
injectBeforeRender,
25+
requestAnimationInInjectionContext,
26+
type NgtRenderState,
27+
} from 'angular-three';
28+
import * as THREE from 'three';
29+
30+
const v = new THREE.Vector3();
31+
const s = new THREE.Vector3(1, 1, 1);
32+
const q = new THREE.Quaternion();
33+
const m = new THREE.Matrix4();
34+
35+
function apply(index: number, positions: Float32Array, quaternions: Float32Array, scale = s, object?: THREE.Object3D) {
36+
if (index !== undefined) {
37+
m.compose(v.fromArray(positions, index * 3), q.fromArray(quaternions, index * 4), scale);
38+
if (object) {
39+
object.matrixAutoUpdate = false;
40+
object.matrix.copy(m);
41+
}
42+
return m;
43+
}
44+
return m.identity();
45+
}
46+
47+
export type NgtcPhysicsState = CannonWorkerProps & {
48+
isPaused?: boolean;
49+
maxSubSteps?: number;
50+
shouldInvalidate?: boolean;
51+
stepSize?: number;
52+
};
53+
54+
type NgtcCannonEvent = CollideBeginEvent | CollideEndEvent | CollideEvent | RayhitEvent;
55+
type NgtcCallbackByType<T extends { type: string }> = {
56+
[K in T['type']]?: T extends { type: K } ? (e: T) => void : never;
57+
};
58+
59+
export type NgtcCannonEvents = { [uuid: string]: Partial<NgtcCallbackByType<NgtcCannonEvent>> };
60+
61+
export type ScaleOverrides = { [uuid: string]: THREE.Vector3 };
62+
63+
export interface NgtcPhysicsApi {
64+
bodies: { [uuid: string]: number };
65+
events: NgtcCannonEvents;
66+
refs: Refs;
67+
scaleOverrides: ScaleOverrides;
68+
subscriptions: Subscriptions;
69+
worker: CannonWorkerAPI;
70+
}
71+
72+
export const NGTC_PHYSICS_API = new InjectionToken<Signal<NgtcPhysicsApi>>('NgtcPhysics API');
73+
74+
@Component({
75+
selector: 'ngtc-physics',
76+
standalone: true,
77+
template: `<ng-content />`,
78+
providers: [{ provide: NGTC_PHYSICS_API, useFactory: (physics: NgtcPhysics) => physics.api, deps: [NgtcPhysics] }],
79+
})
80+
export class NgtcPhysics extends NgtSignalStore<NgtcPhysicsState> {
81+
@Input() set isPaused(isPaused: boolean) {
82+
this.set({ isPaused });
83+
}
84+
85+
@Input() set maxSubSteps(maxSubSteps: number) {
86+
this.set({ maxSubSteps });
87+
}
88+
89+
@Input() set shouldInvalidate(shouldInvalidate: boolean) {
90+
this.set({ shouldInvalidate });
91+
}
92+
93+
@Input() set stepSize(stepSize: number) {
94+
this.set({ stepSize });
95+
}
96+
97+
@Input() set size(size: number) {
98+
this.set({ size });
99+
}
100+
101+
@Input() set allowSleep(allowSleep: boolean) {
102+
this.set({ allowSleep });
103+
}
104+
105+
@Input() set axisIndex(axisIndex: 0 | 1 | 2) {
106+
this.set({ axisIndex });
107+
}
108+
109+
@Input() set broadphase(broadphase: Broadphase) {
110+
this.set({ broadphase });
111+
}
112+
113+
@Input() set defaultContactMaterial(defaultContactMaterial: ContactMaterialOptions) {
114+
this.set({ defaultContactMaterial });
115+
}
116+
117+
@Input() set frictionGravity(frictionGravity: Triplet | null) {
118+
this.set({ frictionGravity });
119+
}
120+
121+
@Input() set gravity(gravity: Triplet) {
122+
this.set({ gravity });
123+
}
124+
125+
@Input() set iterations(iterations: number) {
126+
this.set({ iterations });
127+
}
128+
129+
@Input() set quatNormalizeFast(quatNormalizeFast: boolean) {
130+
this.set({ quatNormalizeFast });
131+
}
132+
133+
@Input() set quatNormalizeSkip(quatNormalizeSkip: number) {
134+
this.set({ quatNormalizeSkip });
135+
}
136+
137+
@Input() set solver(solver: Solver) {
138+
this.set({ solver });
139+
}
140+
141+
@Input() set tolerance(tolerance: number) {
142+
this.set({ tolerance });
143+
}
144+
145+
readonly #store = inject(NgtStore);
146+
147+
readonly #bodies: { [uuid: string]: number } = {};
148+
readonly #events: NgtcCannonEvents = {};
149+
readonly #refs: Refs = {};
150+
readonly #scaleOverrides: ScaleOverrides = {};
151+
readonly #subscriptions: Subscriptions = {};
152+
153+
readonly #allowSleep = this.select('allowSleep');
154+
readonly #defaultContactMaterial = this.select('defaultContactMaterial');
155+
readonly #frictionGravity = this.select('frictionGravity');
156+
readonly #quatNormalizeFast = this.select('quatNormalizeFast');
157+
readonly #quatNormalizeSkip = this.select('quatNormalizeSkip');
158+
readonly #size = this.select('size');
159+
readonly #solver = this.select('solver');
160+
161+
readonly #workerProps = computed(() => ({
162+
allowSleep: this.#allowSleep(),
163+
axisIndex: this.get('axisIndex'),
164+
broadphase: this.get('broadphase'),
165+
defaultContactMaterial: this.#defaultContactMaterial(),
166+
frictionGravity: this.#frictionGravity(),
167+
gravity: this.get('gravity'),
168+
iterations: this.get('iterations'),
169+
quatNormalizeFast: this.#quatNormalizeFast(),
170+
quatNormalizeSkip: this.#quatNormalizeSkip(),
171+
size: this.#size(),
172+
solver: this.#solver(),
173+
tolerance: this.get('tolerance'),
174+
}));
175+
176+
readonly worker = computed(() => {
177+
const workerProps = this.#workerProps();
178+
return new CannonWorkerAPI(workerProps);
179+
});
180+
181+
readonly api = computed(() => ({
182+
bodies: this.#bodies,
183+
events: this.#events,
184+
refs: this.#refs,
185+
scaleOverrides: this.#scaleOverrides,
186+
subscriptions: this.#subscriptions,
187+
worker: this.worker(),
188+
}));
189+
190+
constructor() {
191+
super({
192+
allowSleep: false,
193+
axisIndex: 0,
194+
broadphase: 'Naive',
195+
defaultContactMaterial: { contactEquationStiffness: 1e6 },
196+
frictionGravity: null,
197+
gravity: [0, -9.81, 0],
198+
isPaused: false,
199+
iterations: 5,
200+
maxSubSteps: 10,
201+
quatNormalizeFast: false,
202+
quatNormalizeSkip: 0,
203+
shouldInvalidate: true,
204+
size: 1000,
205+
solver: 'GS',
206+
stepSize: 1 / 60,
207+
tolerance: 0.001,
208+
});
209+
requestAnimationInInjectionContext(() => {
210+
this.#connectWorker();
211+
this.#updateWorkerProp('axisIndex');
212+
this.#updateWorkerProp('broadphase');
213+
this.#updateWorkerProp('gravity');
214+
this.#updateWorkerProp('iterations');
215+
this.#updateWorkerProp('tolerance');
216+
injectBeforeRender(this.#onBeforeRender.bind(this, 0));
217+
});
218+
}
219+
220+
#updateWorkerProp(key: keyof NgtcPhysicsState) {
221+
const compute = this.select(key);
222+
effect(() => {
223+
const worker = untracked(this.worker);
224+
const value = compute();
225+
// @ts-expect-error
226+
worker[key] = value;
227+
});
228+
}
229+
230+
#connectWorker() {
231+
effect((onCleanup) => {
232+
const worker = this.worker();
233+
234+
worker.connect();
235+
worker.init();
236+
237+
(worker as any).on('collide', this.#collideHandler.bind(this));
238+
(worker as any).on('collideBegin', this.#collideBeginHandler.bind(this));
239+
(worker as any).on('collideEnd', this.#collideEndHandler.bind(this));
240+
(worker as any).on('frame', this.#frameHandler.bind(this));
241+
(worker as any).on('rayhit', this.#rayhitHandler.bind(this));
242+
243+
onCleanup(() => {
244+
worker.terminate();
245+
(worker as any).removeAllListeners();
246+
});
247+
});
248+
}
249+
250+
#onBeforeRender(timeSinceLastCalled: number, { delta }: NgtRenderState) {
251+
const { isPaused, maxSubSteps, stepSize } = this.get();
252+
const worker = this.worker();
253+
if (isPaused) return;
254+
timeSinceLastCalled += delta;
255+
worker.step({ maxSubSteps, stepSize: stepSize!, timeSinceLastCalled });
256+
timeSinceLastCalled = 0;
257+
}
258+
259+
#collideHandler({ body, contact: { bi, bj, ...contactRest }, target, ...rest }: WorkerCollideEvent['data']) {
260+
const cb = this.#events[target]?.collide;
261+
262+
if (cb) {
263+
cb({
264+
body: this.#refs[body],
265+
contact: { bi: this.#refs[bi], bj: this.#refs[bj], ...contactRest },
266+
target: this.#refs[target],
267+
...rest,
268+
});
269+
}
270+
}
271+
272+
#collideBeginHandler({ bodyA, bodyB }: WorkerCollideBeginEvent['data']) {
273+
const cbA = this.#events[bodyA]?.collideBegin;
274+
if (cbA) cbA({ body: this.#refs[bodyB], op: 'event', target: this.#refs[bodyA], type: 'collideBegin' });
275+
const cbB = this.#events[bodyB]?.collideBegin;
276+
if (cbB) cbB({ body: this.#refs[bodyA], op: 'event', target: this.#refs[bodyB], type: 'collideBegin' });
277+
}
278+
279+
#collideEndHandler({ bodyA, bodyB }: WorkerCollideEndEvent['data']) {
280+
const cbA = this.#events[bodyA]?.collideEnd;
281+
if (cbA) cbA({ body: this.#refs[bodyB], op: 'event', target: this.#refs[bodyA], type: 'collideEnd' });
282+
const cbB = this.#events[bodyB]?.collideEnd;
283+
if (cbB) cbB({ body: this.#refs[bodyA], op: 'event', target: this.#refs[bodyB], type: 'collideEnd' });
284+
}
285+
286+
#frameHandler({ active, bodies: uuids = [], observations, positions, quaternions }: WorkerFrameMessage['data']) {
287+
const invalidate = this.#store.get('invalidate');
288+
const shouldInvalidate = this.get('shouldInvalidate');
289+
290+
for (let i = 0; i < uuids.length; i++) {
291+
this.#bodies[uuids[i]] = i;
292+
}
293+
294+
observations.forEach(([id, value, type]) => {
295+
const subscription = this.#subscriptions[id] || {};
296+
const cb = subscription[type];
297+
// HELP: We clearly know the type of the callback, but typescript can't deal with it
298+
cb && cb(value as never);
299+
});
300+
301+
if (active) {
302+
for (const ref of Object.values(this.#refs)) {
303+
if (ref instanceof THREE.InstancedMesh) {
304+
for (let i = 0; i < ref.count; i++) {
305+
const uuid = `${ref.uuid}/${i}`;
306+
const index = this.#bodies[uuid];
307+
if (index !== undefined) {
308+
ref.setMatrixAt(i, apply(index, positions, quaternions, this.#scaleOverrides[uuid]));
309+
ref.instanceMatrix.needsUpdate = true;
310+
}
311+
}
312+
} else {
313+
const scale = this.#scaleOverrides[ref.uuid] || ref.scale;
314+
apply(this.#bodies[ref.uuid], positions, quaternions, scale, ref);
315+
}
316+
}
317+
if (shouldInvalidate) invalidate();
318+
}
319+
}
320+
321+
#rayhitHandler({ body, ray: { uuid, ...rayRest }, ...rest }: WorkerRayhitEvent['data']) {
322+
const cb = this.#events[uuid]?.rayhit;
323+
if (cb) cb({ body: body ? this.#refs[body] : null, ray: { uuid, ...rayRest }, ...rest });
324+
}
325+
}

libs/cannon/tsconfig.lib.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@
88
"types": []
99
},
1010
"exclude": ["src/**/*.spec.ts", "src/test-setup.ts", "jest.config.ts", "src/**/*.test.ts"],
11-
"include": ["src/**/*.ts"]
11+
"include": ["**/*.ts"]
1212
}

0 commit comments

Comments
 (0)