Skip to content

Commit 0186d03

Browse files
committed
feat: add routing capability
1 parent f9bf4d7 commit 0186d03

File tree

9 files changed

+246
-27
lines changed

9 files changed

+246
-27
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
---
2+
id: routing
3+
title: Routing
4+
sidebar_label: Routing
5+
---
6+
7+
import Tabs from '@theme/Tabs';
8+
import TabItem from '@theme/TabItem';
9+
10+
Sometimes, we might have use-cases where we need to load different Scene graph while keeping the Canvas alive on the route level.
11+
To help with this, `angular-three` provides the component `NgtRoutedScene`
12+
13+
To start, we can create two Scene components: `RedScene` and `BlueScene`
14+
15+
<Tabs>
16+
<TabItem value="red" label="red-scene.component.ts" default>
17+
18+
```ts
19+
import { Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, ViewChild } from '@angular/core';
20+
import { injectBeforeRender } from 'angular-three';
21+
22+
@Component({
23+
standalone: true,
24+
template: `
25+
<ngt-mesh #cube>
26+
<ngt-box-geometry />
27+
<ngt-mesh-basic-material color="red" />
28+
</ngt-mesh>
29+
`,
30+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
31+
})
32+
export default class RedScene {
33+
@ViewChild('cube', { static: true }) cube!: ElementRef<THREE.Mesh>;
34+
35+
constructor() {
36+
injectBeforeRender(({ clock }) => {
37+
this.cube.nativeElement.rotation.x = clock.elapsedTime;
38+
this.cube.nativeElement.rotation.y = clock.elapsedTime;
39+
});
40+
}
41+
}
42+
```
43+
44+
</TabItem>
45+
<TabItem value="blue" label="blue-scene.component.ts">
46+
47+
```ts
48+
import { Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, ViewChild } from '@angular/core';
49+
import { injectBeforeRender } from 'angular-three';
50+
51+
@Component({
52+
standalone: true,
53+
template: `
54+
<ngt-mesh #cube>
55+
<ngt-box-geometry />
56+
<ngt-mesh-basic-material color="blue" />
57+
</ngt-mesh>
58+
`,
59+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
60+
})
61+
export default class BlueScene {
62+
@ViewChild('cube', { static: true }) cube!: ElementRef<THREE.Mesh>;
63+
64+
constructor() {
65+
injectBeforeRender(({ clock }) => {
66+
this.cube.nativeElement.rotation.x = clock.elapsedTime;
67+
this.cube.nativeElement.rotation.y = clock.elapsedTime;
68+
});
69+
}
70+
}
71+
```
72+
73+
</TabItem>
74+
</Tabs>
75+
76+
Next, we'll provide `RedScene` and `BlueScene` as lazy-load component in our route configuration.
77+
78+
```ts title="main.ts"
79+
import { bootstrapApplication } from '@angular/platform-browser';
80+
import { provideRouter } from '@angular/router';
81+
import { AppComponent } from './app/app.component';
82+
83+
bootstrapApplication(AppComponent, {
84+
providers: [
85+
provideRouter([
86+
{
87+
path: '',
88+
loadComponent: () => import('./app/red-scene.component'),
89+
},
90+
{
91+
path: 'blue',
92+
loadComponent: () => import('./app/blue-scene.component'),
93+
},
94+
]),
95+
],
96+
}).catch((err) => console.error(err));
97+
```
98+
99+
Finally, we'll use `NgtRoutedScene` as the `[sceneGraph]`
100+
101+
```ts title="app.component.ts"
102+
import { Component } from '@angular/core';
103+
import { RouterLink } from '@angular/router';
104+
import { extend, NgtCanvas, NgtRoutedScene } from 'angular-three';
105+
import * as THREE from 'three';
106+
107+
extend(THREE);
108+
109+
@Component({
110+
standalone: true,
111+
selector: 'angular-three-root',
112+
template: `
113+
<ul>
114+
<li>
115+
<a routerLink="/">Red</a>
116+
</li>
117+
<li>
118+
<a routerLink="/blue">Blue</a>
119+
</li>
120+
</ul>
121+
<ngt-canvas [sceneGraph]="scene" />
122+
`,
123+
imports: [NgtCanvas, RouterLink],
124+
})
125+
export class AppComponent {
126+
readonly scene = NgtRoutedScene;
127+
}
128+
```

apps/documentation/sidebars.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const sidebars = {
4545
{
4646
type: 'category',
4747
label: 'Advanced',
48-
items: ['advanced/compound', 'advanced/performance'],
48+
items: ['advanced/routing', 'advanced/compound', 'advanced/performance'],
4949
},
5050
],
5151
// By default, Docusaurus generates a sidebar from the docs folder structure

apps/example/src/app/app.component.ts

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,45 @@
1-
import { Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, ViewChild } from '@angular/core';
2-
import { extend, injectBeforeRender, NgtCanvas } from 'angular-three';
1+
import { Component } from '@angular/core';
2+
import { RouterLink, RouterLinkActive } from '@angular/router';
3+
import { extend, NgtCanvas, NgtRoutedScene } from 'angular-three';
34
import * as THREE from 'three';
45

56
extend(THREE);
67

78
@Component({
89
standalone: true,
10+
selector: 'angular-three-root',
911
template: `
10-
<ngt-mesh #cube>
11-
<ngt-box-geometry />
12-
<ngt-mesh-basic-material color="red" />
13-
</ngt-mesh>
12+
<ul>
13+
<li>
14+
<a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Red</a>
15+
</li>
16+
<li>
17+
<a routerLink="/blue" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Blue</a>
18+
</li>
19+
</ul>
20+
<ngt-canvas [sceneGraph]="scene" />
1421
`,
15-
schemas: [CUSTOM_ELEMENTS_SCHEMA],
16-
})
17-
export class Scene {
18-
@ViewChild('cube', { static: true }) cube!: ElementRef<THREE.Mesh>;
22+
imports: [NgtCanvas, RouterLink, RouterLinkActive],
23+
styles: [
24+
`
25+
ul {
26+
display: flex;
27+
gap: 1rem;
28+
}
1929
20-
constructor() {
21-
injectBeforeRender(({ clock }) => {
22-
this.cube.nativeElement.rotation.x = clock.elapsedTime;
23-
this.cube.nativeElement.rotation.y = clock.elapsedTime;
24-
});
25-
}
26-
}
30+
li {
31+
list-style: none;
32+
}
2733
28-
@Component({
29-
standalone: true,
30-
selector: 'angular-three-root',
31-
template: ` <ngt-canvas [sceneGraph]="scene" /> `,
32-
imports: [NgtCanvas],
34+
a.active {
35+
color: blue;
36+
text-decoration: underline;
37+
border: 1px solid;
38+
padding: 0.25rem;
39+
}
40+
`,
41+
],
3342
})
3443
export class AppComponent {
35-
readonly scene = Scene;
44+
readonly scene = NgtRoutedScene;
3645
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, ViewChild } from '@angular/core';
2+
import { injectBeforeRender } from 'angular-three';
3+
4+
@Component({
5+
standalone: true,
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+
})
14+
export default class BlueScene {
15+
@ViewChild('cube', { static: true }) cube!: ElementRef<THREE.Mesh>;
16+
17+
constructor() {
18+
injectBeforeRender(({ clock }) => {
19+
this.cube.nativeElement.rotation.x = clock.elapsedTime;
20+
this.cube.nativeElement.rotation.y = clock.elapsedTime;
21+
});
22+
}
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, ViewChild } from '@angular/core';
2+
import { injectBeforeRender } from 'angular-three';
3+
4+
@Component({
5+
standalone: true,
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+
})
14+
export default class RedScene {
15+
@ViewChild('cube', { static: true }) cube!: ElementRef<THREE.Mesh>;
16+
17+
constructor() {
18+
injectBeforeRender(({ clock }) => {
19+
this.cube.nativeElement.rotation.x = clock.elapsedTime;
20+
this.cube.nativeElement.rotation.y = clock.elapsedTime;
21+
});
22+
}
23+
}

apps/example/src/main.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,18 @@
11
import { bootstrapApplication } from '@angular/platform-browser';
2+
import { provideRouter } from '@angular/router';
23
import { AppComponent } from './app/app.component';
34

4-
bootstrapApplication(AppComponent).catch((err) => console.error(err));
5+
bootstrapApplication(AppComponent, {
6+
providers: [
7+
provideRouter([
8+
{
9+
path: '',
10+
loadComponent: () => import('./app/red-scene.component'),
11+
},
12+
{
13+
path: 'blue',
14+
loadComponent: () => import('./app/blue-scene.component'),
15+
},
16+
]),
17+
],
18+
}).catch((err) => console.error(err));

libs/angular-three/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export * from './lib/loader';
1010
export * from './lib/loop';
1111
export * from './lib/pipes/push';
1212
export * from './lib/portal';
13+
export * from './lib/routed-scene';
1314
export * from './lib/stores/rx-store';
1415
export * from './lib/stores/store';
1516
export * from './lib/types';

libs/angular-three/src/lib/renderer/renderer.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export class NgtRendererFactory implements RendererFactory2 {
2828
private readonly document = inject(DOCUMENT);
2929

3030
private rendererMap = new Map<string, Renderer2>();
31+
private routedSet = new Set<string>();
3132
private portals: NgtRendererNode[] = [];
3233
private rendererStore = new NgtRendererStore({
3334
store: this.store,
@@ -40,10 +41,18 @@ export class NgtRendererFactory implements RendererFactory2 {
4041
createRenderer(hostElement: any, type: RendererType2 | null): Renderer2 {
4142
const delegateRenderer = this.delegateRendererFactory.createRenderer(hostElement, type);
4243
if (!type) return delegateRenderer;
43-
44+
if ((type as NgtAnyRecord)['type']['isRoutedScene']) {
45+
this.routedSet.add(type.id);
46+
}
4447
let renderer = this.rendererMap.get(type.id);
4548
if (!renderer) {
46-
renderer = new NgtRenderer(delegateRenderer, this.rendererStore, this.catalogue, !hostElement);
49+
renderer = new NgtRenderer(
50+
delegateRenderer,
51+
this.rendererStore,
52+
this.catalogue,
53+
// setting root scene if there's no routed scene OR this component is the routed Scene
54+
!hostElement && (this.routedSet.size === 0 || this.routedSet.has(type.id))
55+
);
4756
this.rendererMap.set(type.id, renderer);
4857
}
4958
return renderer;
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 { RouterOutlet } from '@angular/router';
3+
4+
@Component({
5+
standalone: true,
6+
selector: 'ngt-routed-scene',
7+
template: `<router-outlet />`,
8+
imports: [RouterOutlet],
9+
})
10+
export class NgtRoutedScene {
11+
static isRoutedScene = true;
12+
}

0 commit comments

Comments
 (0)