Skip to content

Commit 8f336df

Browse files
committed
feat(rapier): support fallback content when physics fails to load RAPIER wasm
BREAKING CHANGE: in order to support fallback content, physics content has to be wrapped with `ng-template` to defer the render of RigidBodies/Colliders within the `NgtrPhysics` component. ```html <ngtr-physics> <!-- template defers the rendering until RAPIER finishes initializing --> <ng-template> <ngt-object3D ngtrRigidBody /> <!-- ... --> </ng-template> </ngtr-physics> ``` This breaking change is for a dev preview (undocumented as well) Rapier package so we'll only bump a minor version instead of major
1 parent 925e894 commit 8f336df

File tree

5 files changed

+97
-53
lines changed

5 files changed

+97
-53
lines changed

apps/kitchen-sink/src/app/rapier/basic/basic.ts

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,31 +10,33 @@ import { NgtsOrbitControls } from 'angular-three-soba/controls';
1010
<ngts-perspective-camera [options]="{ makeDefault: true, position: [5, 5, 5] }" />
1111
1212
<ngtr-physics [options]="{ debug: true }">
13-
<ngt-object3D ngtrRigidBody [options]="{ colliders: 'hull' }" [position]="[0, 5, 0]">
14-
<ngt-mesh>
15-
<ngt-torus-geometry />
16-
</ngt-mesh>
17-
</ngt-object3D>
13+
<ng-template>
14+
<ngt-object3D ngtrRigidBody [options]="{ colliders: 'hull' }" [position]="[0, 5, 0]">
15+
<ngt-mesh>
16+
<ngt-torus-geometry />
17+
</ngt-mesh>
18+
</ngt-object3D>
1819
19-
@if (currentCollider() === 1) {
20-
<ngt-object3D ngtrCuboidCollider [args]="[1, 0.5, 1]" (collisionExit)="currentCollider.set(2)" />
21-
} @else if (currentCollider() === 2) {
22-
<ngt-object3D
23-
ngtrCuboidCollider
24-
[position]="[0, -1, 0]"
25-
[args]="[3, 0.5, 3]"
26-
(collisionExit)="currentCollider.set(3)"
27-
/>
28-
} @else if (currentCollider() === 3) {
29-
<ngt-object3D
30-
ngtrCuboidCollider
31-
[position]="[0, -3, 0]"
32-
[args]="[6, 0.5, 6]"
33-
(collisionExit)="currentCollider.set(4)"
34-
/>
35-
} @else {
36-
<ngt-object3D ngtrCuboidCollider [position]="[0, -6, 0]" [args]="[20, 0.5, 20]" />
37-
}
20+
@if (currentCollider() === 1) {
21+
<ngt-object3D ngtrCuboidCollider [args]="[1, 0.5, 1]" (collisionExit)="currentCollider.set(2)" />
22+
} @else if (currentCollider() === 2) {
23+
<ngt-object3D
24+
ngtrCuboidCollider
25+
[position]="[0, -1, 0]"
26+
[args]="[3, 0.5, 3]"
27+
(collisionExit)="currentCollider.set(3)"
28+
/>
29+
} @else if (currentCollider() === 3) {
30+
<ngt-object3D
31+
ngtrCuboidCollider
32+
[position]="[0, -3, 0]"
33+
[args]="[6, 0.5, 6]"
34+
(collisionExit)="currentCollider.set(4)"
35+
/>
36+
} @else {
37+
<ngt-object3D ngtrCuboidCollider [position]="[0, -6, 0]" [args]="[20, 0.5, 20]" />
38+
}
39+
</ng-template>
3840
</ngtr-physics>
3941
4042
<ngts-orbit-controls />

apps/kitchen-sink/src/app/rapier/wrapper-default.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,21 +42,23 @@ export class Floor {}
4242
<ng-container *ngComponentOutlet="component()" />
4343
} @else {
4444
<ngtr-physics [options]="{ debug: debug(), interpolate: interpolate(), paused: paused() }">
45-
<ngt-directional-light [castShadow]="true" [position]="10">
46-
<ngt-value [rawValue]="-40" attach="shadow.camera.bottom" />
47-
<ngt-value [rawValue]="40" attach="shadow.camera.top" />
48-
<ngt-value [rawValue]="-40" attach="shadow.camera.left" />
49-
<ngt-value [rawValue]="40" attach="shadow.camera.right" />
50-
<ngt-value [rawValue]="1024" attach="shadow.mapSize.width" />
51-
<ngt-value [rawValue]="-0.0001" attach="shadow.bias" />
52-
</ngt-directional-light>
45+
<ng-template>
46+
<ngt-directional-light [castShadow]="true" [position]="10">
47+
<ngt-value [rawValue]="-40" attach="shadow.camera.bottom" />
48+
<ngt-value [rawValue]="40" attach="shadow.camera.top" />
49+
<ngt-value [rawValue]="-40" attach="shadow.camera.left" />
50+
<ngt-value [rawValue]="40" attach="shadow.camera.right" />
51+
<ngt-value [rawValue]="1024" attach="shadow.mapSize.width" />
52+
<ngt-value [rawValue]="-0.0001" attach="shadow.bias" />
53+
</ngt-directional-light>
5354
54-
<ngts-environment [options]="{ preset: 'apartment' }" />
55-
<ngts-orbit-controls />
55+
<ngts-environment [options]="{ preset: 'apartment' }" />
56+
<ngts-orbit-controls />
5657
57-
<ng-container *ngComponentOutlet="component()" />
58+
<ng-container *ngComponentOutlet="component()" />
5859
59-
<app-floor />
60+
<app-floor />
61+
</ng-template>
6062
</ngtr-physics>
6163
}
6264
`,

apps/kitchen-sink/src/app/soba/bruno-simons-20k/experience.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,13 @@ export class Model {
121121
</ngts-environment>
122122
123123
<ngtr-physics [options]="{ debug: debug(), gravity: [0, -4, 0] }">
124-
<app-model [position]="[1, 0, -1.5]" />
125-
<app-hats />
126-
<ngt-object3D ngtrRigidBody="fixed" [options]="{ colliders: false }" [position]="[0, -1, 0]">
127-
<ngt-object3D ngtrCuboidCollider [args]="[1000, 1, 1000]" />
128-
</ngt-object3D>
124+
<ng-template>
125+
<app-model [position]="[1, 0, -1.5]" />
126+
<app-hats />
127+
<ngt-object3D ngtrRigidBody="fixed" [options]="{ colliders: false }" [position]="[0, -1, 0]">
128+
<ngt-object3D ngtrCuboidCollider [args]="[1000, 1, 1000]" />
129+
</ngt-object3D>
130+
</ng-template>
129131
</ngtr-physics>
130132
131133
<ngts-accumulative-shadows

libs/rapier/src/lib/physics.ts

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1+
import { NgTemplateOutlet } from '@angular/common';
12
import {
23
ChangeDetectionStrategy,
34
Component,
45
computed,
6+
contentChild,
57
DestroyRef,
8+
Directive,
69
effect,
710
inject,
811
input,
912
signal,
13+
TemplateRef,
1014
untracked,
1115
} from '@angular/core';
1216
import RAPIER, { ColliderHandle, EventQueue, Rotation, Vector, World } from '@dimforge/rapier3d-compat';
@@ -46,27 +50,43 @@ const defaultOptions: NgtrPhysicsOptions = {
4650
debug: false,
4751
};
4852

53+
@Directive({ selector: 'ng-template[rapierFallback]', standalone: true })
54+
export class NgtrPhysicsFallback {
55+
static ngTemplateContextGuard(_: NgtrPhysicsFallback, ctx: unknown): ctx is { error: string } {
56+
return true;
57+
}
58+
}
59+
4960
@Component({
5061
selector: 'ngtr-physics',
5162
standalone: true,
5263
template: `
53-
@if (debug()) {
54-
<ngtr-debug [world]="worldSingleton()?.proxy" />
64+
@if (rapierConstruct()) {
65+
@if (debug()) {
66+
<ngtr-debug [world]="worldSingleton()?.proxy" />
67+
}
68+
69+
<ngtr-frame-stepper
70+
[ready]="ready()"
71+
[stepFn]="step.bind(this)"
72+
[type]="updateLoop()"
73+
[updatePriority]="updatePriority()"
74+
/>
75+
76+
<ng-container [ngTemplateOutlet]="content()" />
77+
} @else if (rapierError() && !!fallbackContent()) {
78+
<ng-container [ngTemplateOutlet]="$any(fallbackContent())" [ngTemplateOutletContext]="{ error: rapierError() }" />
5579
}
56-
<ngtr-frame-stepper
57-
[ready]="ready()"
58-
[stepFn]="step.bind(this)"
59-
[type]="updateLoop()"
60-
[updatePriority]="updatePriority()"
61-
/>
62-
<ng-content />
6380
`,
6481
changeDetection: ChangeDetectionStrategy.OnPush,
65-
imports: [NgtrDebug, NgtrFrameStepper],
82+
imports: [NgtrDebug, NgtrFrameStepper, NgTemplateOutlet],
6683
})
6784
export class NgtrPhysics {
6885
options = input(defaultOptions, { transform: mergeInputs(defaultOptions) });
6986

87+
content = contentChild.required(TemplateRef);
88+
fallbackContent = contentChild(NgtrPhysicsFallback);
89+
7090
protected updatePriority = pick(this.options, 'updatePriority');
7191
protected updateLoop = pick(this.options, 'updateLoop');
7292

@@ -90,7 +110,8 @@ export class NgtrPhysics {
90110
private store = injectStore();
91111
private destroyRef = inject(DestroyRef);
92112

93-
private rapierConstruct = signal<typeof RAPIER | null>(null);
113+
protected rapierConstruct = signal<typeof RAPIER | null>(null);
114+
protected rapierError = signal<string | null>(null);
94115
rapier = this.rapierConstruct.asReadonly();
95116

96117
ready = computed(() => !!this.rapier());
@@ -124,7 +145,7 @@ export class NgtrPhysics {
124145
.then(this.rapierConstruct.set.bind(this.rapierConstruct))
125146
.catch((err) => {
126147
console.error(`[NGT] Failed to load rapier3d-compat`, err);
127-
return Promise.reject(err);
148+
this.rapierError.set(err?.message ?? err.toString());
128149
});
129150

130151
effect(() => {

libs/rapier/src/lib/rigid-body.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,12 @@ export class NgtrAnyCollider {
175175
const worldSingleton = this.physics.worldSingleton();
176176
if (!worldSingleton) return;
177177

178+
const localState = getLocalState(this.objectRef.nativeElement);
179+
if (!localState) return;
180+
181+
const parent = localState.parent();
182+
if (!parent || !(parent as Object3D).isObject3D) return;
183+
178184
const state = this.createColliderState(
179185
collider,
180186
this.objectRef.nativeElement,
@@ -478,6 +484,11 @@ export class NgtrRigidBody {
478484
if (!options.colliders) options.colliders = physicsColliders;
479485

480486
const objectLocalState = getLocalState(this.objectRef.nativeElement);
487+
if (!objectLocalState) return [];
488+
489+
const parent = objectLocalState.parent();
490+
if (!parent || !(parent as Object3D).isObject3D) return [];
491+
481492
// track object's children
482493
objectLocalState?.nonObjects();
483494

@@ -511,6 +522,12 @@ export class NgtrRigidBody {
511522

512523
const transformState = untracked(this.transformState);
513524

525+
const localState = getLocalState(this.objectRef.nativeElement);
526+
if (!localState) return;
527+
528+
const parent = localState.parent();
529+
if (!parent || !(parent as Object3D).isObject3D) return;
530+
514531
const state = this.createRigidBodyState(body, this.objectRef.nativeElement);
515532
this.physics.rigidBodyStates.set(body.handle, transformState ? transformState(state) : state);
516533

0 commit comments

Comments
 (0)