Skip to content

Commit 6e76304

Browse files
committed
fix(core): routed scene should work
1 parent be09add commit 6e76304

File tree

12 files changed

+267
-30
lines changed

12 files changed

+267
-30
lines changed

apps/astro-docs/src/content/docs/core/advanced/routed-scene.mdx

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ description: Details about the Angular Three `NgtRoutedScene`
55

66
import { Tabs, TabItem } from '@astrojs/starlight/components';
77

8-
:::danger
8+
:::caution
99

10-
This is no longer working due to [a fix in Angular Router](https://github.com/angular/angular/commit/3839cfbb18fcc70cae5a6ba4ba7676b1c4acf7a0).
11-
We will re-enable this feature once a workaround is found or a new fix is released.
10+
Due to [a fix in Angular Router](https://github.com/angular/angular/commit/3839cfbb18fcc70cae5a6ba4ba7676b1c4acf7a0), Angular Three
11+
implements a custom `RouterOutlet` to enable this feature. Use with caution. File an issue if you encounter any problems.
1212

1313
:::
1414

@@ -95,17 +95,42 @@ import { provideRouter } from '@angular/router';
9595
import { AppComponent } from './app/app.component';
9696

9797
bootstrapApplication(AppComponent, {
98-
providers: [
99-
provideRouter([
100-
{
101-
path: '',
102-
loadComponent: () => import('./app/red-scene.component'),
103-
},
104-
{
105-
path: 'blue',
106-
loadComponent: () => import('./app/blue-scene.component'),
107-
},
108-
]),
109-
],
98+
providers: [
99+
provideRouter([
100+
{
101+
path: '',
102+
loadComponent: () => import('./app/red-scene.component'),
103+
},
104+
{
105+
path: 'blue',
106+
loadComponent: () => import('./app/blue-scene.component'),
107+
},
108+
]),
109+
],
110110
}).catch((err) => console.error(err));
111111
```
112+
113+
## Custom routed scene
114+
115+
Using `'routed'` will use the default `NgtRoutedScene` component provided by Angular Three. It is also possible to have your own routed scene component.
116+
117+
```html
118+
<ngt-canvas [sceneGraph]="CustomRoutedScene" />
119+
```
120+
121+
```angular-ts
122+
@Component({
123+
template: `
124+
<ngt-router-outlet />
125+
<!-- All routed scenes will have a CameraControls instead of each routed rendering their own camera controls -->
126+
<ngts-camara-controls />
127+
`,
128+
imports: [
129+
NgtRouterOutlet, // a custom router-outlet
130+
NgtsCameraControls,
131+
],
132+
})
133+
export class CustomRoutedScene {
134+
static [ROUTED_SCENE] = true; // flag for `NgtRenderer`
135+
}
136+
```

apps/kitchen-sink/src/app/app.component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { filter } from 'rxjs';
1515
<option value="postprocessing">/postprocessing</option>
1616
<option value="rapier">/rapier</option>
1717
<option value="misc">/misc</option>
18+
<option value="routed">/routed</option>
1819
</select>
1920
2021
<div class="bg-white rounded-full p-2 text-black border border-white border-dashed">

apps/kitchen-sink/src/app/app.routes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ export const appRoutes: Route[] = [
3131
loadChildren: () => import('./misc/misc.routes'),
3232
title: 'Misc - Angular Three Demo',
3333
},
34+
{
35+
path: 'routed',
36+
loadComponent: () => import('./routed/routed'),
37+
loadChildren: () => import('./routed/routed.routes'),
38+
title: 'Routed - Angular Three Demo',
39+
},
3440
{
3541
path: '',
3642
// redirectTo: 'cannon',
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, viewChild } from '@angular/core';
2+
import { injectBeforeRender } from 'angular-three';
3+
import { Mesh } from 'three';
4+
5+
@Component({
6+
template: `
7+
<ngt-mesh #cube>
8+
<ngt-box-geometry />
9+
<ngt-mesh-basic-material color="blue" />
10+
</ngt-mesh>
11+
`,
12+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
13+
changeDetection: ChangeDetectionStrategy.OnPush,
14+
})
15+
export default class BlueCube {
16+
cubeRef = viewChild.required<ElementRef<Mesh>>('cube');
17+
18+
constructor() {
19+
injectBeforeRender(({ clock }) => {
20+
this.cubeRef().nativeElement.rotation.x = clock.elapsedTime;
21+
this.cubeRef().nativeElement.rotation.y = clock.elapsedTime;
22+
});
23+
}
24+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Component } from '@angular/core';
2+
import { NgtRouterOutlet } from 'angular-three';
3+
import { NgtsCameraControls } from 'angular-three-soba/controls';
4+
5+
@Component({
6+
template: `
7+
<ngt-router-outlet />
8+
<ngts-camera-controls />
9+
`,
10+
imports: [NgtRouterOutlet, NgtsCameraControls],
11+
})
12+
export class CustomRoutedScene {}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, viewChild } from '@angular/core';
2+
import { injectBeforeRender } from 'angular-three';
3+
import { Mesh } from 'three';
4+
5+
@Component({
6+
template: `
7+
<ngt-mesh #cube>
8+
<ngt-box-geometry />
9+
<ngt-mesh-basic-material color="red" />
10+
</ngt-mesh>
11+
`,
12+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
13+
changeDetection: ChangeDetectionStrategy.OnPush,
14+
})
15+
export default class RedCube {
16+
private cubeRef = viewChild.required<ElementRef<Mesh>>('cube');
17+
18+
constructor() {
19+
injectBeforeRender(({ clock }) => {
20+
this.cubeRef().nativeElement.rotation.x = clock.elapsedTime;
21+
this.cubeRef().nativeElement.rotation.y = clock.elapsedTime;
22+
});
23+
}
24+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Routes } from '@angular/router';
2+
3+
const routes: Routes = [
4+
{
5+
path: 'red',
6+
loadComponent: () => import('./red-cube'),
7+
},
8+
{
9+
path: 'blue',
10+
loadComponent: () => import('./blue-cube'),
11+
},
12+
{
13+
path: '',
14+
redirectTo: 'red',
15+
pathMatch: 'full',
16+
},
17+
];
18+
19+
export default routes;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { ChangeDetectionStrategy, Component } from '@angular/core';
2+
import { RouterLink, RouterLinkActive } from '@angular/router';
3+
import { extend, NgtCanvas } from 'angular-three';
4+
import * as THREE from 'three';
5+
import { CustomRoutedScene } from './custom-routed-scene';
6+
7+
extend(THREE);
8+
9+
@Component({
10+
template: `
11+
<div class="h-svh">
12+
<ngt-canvas [sceneGraph]="routedScene" />
13+
</div>
14+
15+
<ul class="absolute bottom-0 left-0 flex items-center gap-2">
16+
<li>
17+
<a
18+
routerLink="red"
19+
class="underline"
20+
routerLinkActive="text-blue-500"
21+
[routerLinkActiveOptions]="{ exact: true }"
22+
>
23+
Red Cube
24+
</a>
25+
</li>
26+
<li>
27+
<a
28+
routerLink="blue"
29+
class="underline"
30+
routerLinkActive="text-blue-500"
31+
[routerLinkActiveOptions]="{ exact: true }"
32+
>
33+
Blue Cube
34+
</a>
35+
</li>
36+
</ul>
37+
`,
38+
imports: [NgtCanvas, RouterLink, RouterLinkActive],
39+
changeDetection: ChangeDetectionStrategy.OnPush,
40+
host: { class: 'routed' },
41+
})
42+
export default class Routed {
43+
routedScene = CustomRoutedScene;
44+
}

libs/core/src/lib/canvas.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,7 @@ export class NgtCanvas {
6565

6666
sceneGraph = input.required<Type<any>, Type<any> | 'routed'>({
6767
transform: (value) => {
68-
if (value === 'routed') {
69-
console.warn(`[NGT] 'routed' sceneGraph is not working properly.`);
70-
return NgtRoutedScene;
71-
}
68+
if (value === 'routed') return NgtRoutedScene;
7269
return value;
7370
},
7471
});

libs/core/src/lib/renderer/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,11 @@ export class NgtRenderer implements Renderer2 {
296296
if (cRS[NgtRendererClassId.type] === 'three') {
297297
removeThreeChild(oldChild, undefined, true);
298298
}
299+
300+
// if the child is the root scene, we don't want to destroy it
301+
if (is.scene(oldChild) && oldChild.name === '__ngt_root_scene__') return;
302+
303+
// otherwise, we'll destroy it
299304
this.destroyInternal(oldChild, undefined);
300305
}
301306

libs/core/src/lib/roots.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ export function injectCanvasRootInitializer(injector?: Injector) {
162162
}
163163

164164
applyProps(scene, {
165+
name: '__ngt_root_scene__',
165166
setAttribute: (name: string, value: string) => {
166167
if (canvas instanceof HTMLCanvasElement) {
167168
if (canvas.parentElement) {

libs/core/src/lib/routed-scene.ts

Lines changed: 90 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,104 @@
1-
import { ChangeDetectorRef, Component } from '@angular/core';
2-
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
3-
import { ActivationEnd, Router, RouterOutlet } from '@angular/router';
1+
import {
2+
ChangeDetectorRef,
3+
Component,
4+
Directive,
5+
effect,
6+
EnvironmentInjector,
7+
inject,
8+
InjectFlags,
9+
InjectOptions,
10+
ProviderToken,
11+
RendererFactory2,
12+
runInInjectionContext,
13+
} from '@angular/core';
14+
import { ActivatedRoute, ActivationEnd, Router, RouterOutlet } from '@angular/router';
415
import { filter } from 'rxjs';
516
import { ROUTED_SCENE } from './renderer/constants';
617

18+
/**
19+
* This is a custom EnvironmentInjector that returns the RendererFactory2 from the `ngtEnvironmentInjector`
20+
* for `NgtRendererFactory`
21+
*/
22+
class NgtOutletEnvironmentInjector extends EnvironmentInjector {
23+
constructor(
24+
private readonly routeEnvInjector: EnvironmentInjector,
25+
private readonly ngtEnvInjector: EnvironmentInjector,
26+
) {
27+
super();
28+
}
29+
30+
override get<T>(
31+
token: ProviderToken<T>,
32+
notFoundValue?: unknown,
33+
flags: InjectFlags | InjectOptions = InjectFlags.Default,
34+
): any {
35+
const options: InjectOptions = {};
36+
37+
if (typeof flags === 'object') {
38+
Object.assign(options, flags);
39+
} else {
40+
Object.assign(options, {
41+
optional: !!(flags & InjectFlags.Optional),
42+
host: !!(flags & InjectFlags.Host),
43+
self: !!(flags & InjectFlags.Self),
44+
skipSelf: !!(flags & InjectFlags.SkipSelf),
45+
});
46+
}
47+
48+
if (token === RendererFactory2) {
49+
return this.ngtEnvInjector.get(token, notFoundValue, options);
50+
}
51+
52+
return this.routeEnvInjector.get(token, notFoundValue, options);
53+
}
54+
55+
override runInContext<ReturnT>(fn: () => ReturnT): ReturnT {
56+
try {
57+
return runInInjectionContext(this.routeEnvInjector, fn);
58+
} catch {}
59+
60+
return runInInjectionContext(this.ngtEnvInjector, fn);
61+
}
62+
63+
override destroy(): void {
64+
this.routeEnvInjector.destroy();
65+
}
66+
}
67+
68+
@Directive({ selector: 'ngt-router-outlet' })
69+
/**
70+
* This is a custom RouterOutlet that modifies `activateWith` to inherit the `EnvironmentInjector`
71+
* that contains the custom `NgtRendererFactory`.
72+
*
73+
* Use this with extreme caution.
74+
*/
75+
export class NgtRouterOutlet extends RouterOutlet {
76+
private environmentInjector = inject(EnvironmentInjector);
77+
78+
override activateWith(activatedRoute: ActivatedRoute, environmentInjector: EnvironmentInjector): void {
79+
return super.activateWith(
80+
activatedRoute,
81+
new NgtOutletEnvironmentInjector(environmentInjector, this.environmentInjector),
82+
);
83+
}
84+
}
85+
786
@Component({
887
selector: 'ngt-routed-scene',
988
template: `
10-
<router-outlet />
89+
<ngt-router-outlet />
1190
`,
12-
imports: [RouterOutlet],
91+
imports: [NgtRouterOutlet],
1392
})
1493
export class NgtRoutedScene {
1594
static [ROUTED_SCENE] = true;
1695

1796
constructor(router: Router, cdr: ChangeDetectorRef) {
18-
router.events
19-
.pipe(
20-
filter((event) => event instanceof ActivationEnd),
21-
takeUntilDestroyed(),
22-
)
23-
.subscribe(cdr.detectChanges.bind(cdr));
97+
effect((onCleanup) => {
98+
const sub = router.events
99+
.pipe(filter((event) => event instanceof ActivationEnd))
100+
.subscribe(cdr.detectChanges.bind(cdr));
101+
onCleanup(() => sub.unsubscribe());
102+
});
24103
}
25104
}

0 commit comments

Comments
 (0)