Skip to content

Commit a0fbf4c

Browse files
committed
docs(examples): update routed rocks routes
1 parent 50512c4 commit a0fbf4c

File tree

9 files changed

+311
-9
lines changed

9 files changed

+311
-9
lines changed

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

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,15 @@ export const appRoutes: Route[] = [
3737
loadChildren: () => import('./routed/routed.routes'),
3838
title: 'Routed - Angular Three Demo',
3939
},
40-
// {
41-
// path: 'routed-rocks',
42-
// loadComponent: () => import('./routed-rocks/routed-rocks'),
43-
// loadChildren: () => import('./routed-rocks/routed-rocks.routes'),
44-
// title: 'Routed Rocks - Angular Three Demo',
45-
// },
40+
{
41+
path: 'routed-rocks',
42+
loadComponent: () => import('./routed-rocks/routed-rocks'),
43+
loadChildren: () => import('./routed-rocks/routed-rocks.routes'),
44+
title: 'Routed Rocks - Angular Three Demo',
45+
},
4646
{
4747
path: '',
48-
// redirectTo: 'cannon',
49-
// redirectTo: 'postprocessing',
50-
redirectTo: 'misc',
48+
redirectTo: 'soba',
5149
pathMatch: 'full',
5250
},
5351
];
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, inject } from '@angular/core';
2+
import { NgtArgs, NgtEuler, NgtParent, NgtVector3 } from 'angular-three';
3+
import { NgtsText } from 'angular-three-soba/abstractions';
4+
import { FrontSide } from 'three';
5+
import { RockStore } from './store';
6+
7+
@Component({
8+
template: `
9+
@if (selectedRock(); as rock) {
10+
<ngt-group *parent="rock.name">
11+
<ngt-mesh castShadow receiveShadow [rotation]="[0, Math.PI / 4, 0]" [position]="[0, 7, 0]" [scale]="0.5">
12+
<ngt-box-geometry *args="[0.7, 0.7, 0.7]" />
13+
<ngt-mesh-phong-material [color]="rock.color" [side]="FrontSide" />
14+
</ngt-mesh>
15+
16+
<ngt-group [position]="[0, 5, 0]">
17+
@for (text of texts; track $index) {
18+
<ngts-text
19+
font="https://fonts.gstatic.com/s/raleway/v14/1Ptrg8zYS_SKggPNwK4vaqI.woff"
20+
[text]="rock.label"
21+
[options]="{
22+
color: rock.color,
23+
font: 'https://fonts.gstatic.com/s/raleway/v14/1Ptrg8zYS_SKggPNwK4vaqI.woff',
24+
fontSize: 0.5,
25+
position: text.position,
26+
rotation: text.rotation,
27+
castShadow: true,
28+
}"
29+
/>
30+
}
31+
</ngt-group>
32+
</ngt-group>
33+
}
34+
`,
35+
imports: [NgtArgs, NgtsText, NgtParent],
36+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
37+
changeDetection: ChangeDetectionStrategy.OnPush,
38+
host: { class: 'colored-rock' },
39+
})
40+
export default class ColoredRock {
41+
protected readonly Math = Math;
42+
protected readonly FrontSide = FrontSide;
43+
44+
protected readonly texts = Array.from({ length: 3 }, (_, index) => ({
45+
rotation: [0, ((360 / 3) * index * Math.PI) / 180, 0] as NgtEuler,
46+
position: [
47+
5 * Math.cos(((360 / 3) * index * Math.PI) / 180),
48+
0,
49+
5 * Math.sin(((360 / 3) * index * Math.PI) / 180),
50+
] as NgtVector3,
51+
}));
52+
53+
private rockStore = inject(RockStore);
54+
protected readonly selectedRock = this.rockStore.selectedRock;
55+
56+
constructor() {
57+
// NOTE: we can use ng-template for this use-case as well.
58+
// Just a little more involved than `NgtParent`
59+
//
60+
// effect(() => {
61+
// const colorId = this.colorId();
62+
// if (!colorId) return;
63+
//
64+
// const templateRef = this.templateRef();
65+
// this.rockStore.coloredRockTemplateRefs.set({
66+
// [colorId]: templateRef,
67+
// });
68+
// });
69+
//
70+
// inject(DestroyRef).onDestroy(() => {
71+
// this.rockStore.coloredRockTemplateRefs.set({});
72+
// });
73+
}
74+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const colors = [
2+
{ color: '#042A2B', label: 'Rich Black', slug: 'rich-black' },
3+
{ color: '#5EB1BF', label: 'Maximum Blue', slug: 'maximum-blue' },
4+
{ color: '#CDEDF6', label: 'Light Cyan', slug: 'light-cyan' },
5+
{ color: '#EF7B45', label: 'Mandarin', slug: 'mandarin' },
6+
{ color: '#D84727', label: 'Vermilion', slug: 'vermilion' },
7+
] as const;
8+
9+
export const menus = colors.map((color, index) => ({
10+
id: index + 1,
11+
slug: color.slug,
12+
label: color.label,
13+
name: `rock-${color.slug}`,
14+
path: `/routed-rocks/rocks/${color.slug}`,
15+
color: color.color,
16+
angle: ((360 / colors.length) * index * Math.PI) / 180,
17+
}));
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { DOCUMENT } from '@angular/common';
2+
import { Directive, ElementRef, inject } from '@angular/core';
3+
import { getInstanceState, injectObjectEvents } from 'angular-three';
4+
import { Object3D } from 'three';
5+
6+
@Directive({ selector: '[cursor]' })
7+
export class Cursor {
8+
constructor() {
9+
const elementRef = inject<ElementRef<Object3D>>(ElementRef);
10+
const nativeElement = elementRef.nativeElement;
11+
12+
if (!nativeElement.isObject3D) return;
13+
14+
const instanceState = getInstanceState(nativeElement);
15+
if (!instanceState) return;
16+
17+
const document = inject(DOCUMENT);
18+
19+
injectObjectEvents(() => nativeElement, {
20+
pointerover: () => {
21+
document.body.style.cursor = 'pointer';
22+
},
23+
pointerout: () => {
24+
document.body.style.cursor = 'default';
25+
},
26+
});
27+
}
28+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Routes } from '@angular/router';
2+
3+
const rockRoutes: Routes = [
4+
{
5+
path: ':colorId',
6+
loadComponent: () => import('./colored-rock'),
7+
},
8+
];
9+
10+
export default rockRoutes;
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, effect, inject } from '@angular/core';
2+
import { Router, RouterOutlet } from '@angular/router';
3+
import { injectStore, NgtArgs } from 'angular-three';
4+
import { NgtsCameraControls } from 'angular-three-soba/controls';
5+
import { injectGLTF } from 'angular-three-soba/loaders';
6+
import CameraControls from 'camera-controls';
7+
import { DoubleSide, FrontSide, Mesh, MeshStandardMaterial } from 'three';
8+
import { GLTF } from 'three-stdlib';
9+
import { menus } from './constants';
10+
import { Cursor } from './cursor';
11+
import { RockStore } from './store';
12+
13+
interface RockGLTF extends GLTF {
14+
nodes: { defaultMaterial: Mesh };
15+
materials: { '08___Default': MeshStandardMaterial };
16+
}
17+
18+
@Component({
19+
template: `
20+
<ngt-fog *args="['white', 15, 50]" attach="fog" />
21+
22+
<ngt-grid-helper *args="[50, 10]" />
23+
24+
<ngt-mesh receiveShadow [rotation]="[Math.PI / 2, 0, 0]">
25+
<ngt-plane-geometry *args="[100, 100]" />
26+
<ngt-mesh-phong-material color="white" [side]="DoubleSide" [depthWrite]="false" />
27+
</ngt-mesh>
28+
29+
<ngt-hemisphere-light [position]="10" [intensity]="Math.PI * 0.2" />
30+
31+
<ngt-point-light [position]="10" [decay]="0" castShadow>
32+
<ngt-vector2 *args="[1024, 1024]" attach="shadow.mapSize" />
33+
<ngt-value [rawValue]="4" attach="shadow.radius" />
34+
<ngt-value [rawValue]="-0.0005" attach="shadow.bias" />
35+
</ngt-point-light>
36+
37+
@if (gltf(); as gltf) {
38+
<ngt-group [position]="[0, 2.6, 0]" [scale]="3">
39+
<ngt-group [rotation]="[-Math.PI / 2, 0, 0]">
40+
<ngt-group [rotation]="[Math.PI / 2, 0, 0]">
41+
<ngt-mesh
42+
cursor
43+
castShadow
44+
receiveShadow
45+
[geometry]="gltf.nodes.defaultMaterial.geometry"
46+
[material]="gltf.materials['08___Default']"
47+
(click)="router.navigate(['/routed-rocks/rocks'])"
48+
/>
49+
</ngt-group>
50+
</ngt-group>
51+
</ngt-group>
52+
}
53+
54+
<ngts-camera-controls
55+
[options]="{ makeDefault: true, minDistance: 12, maxDistance: 12, minPolarAngle: 0, maxPolarAngle: Math.PI / 2 }"
56+
/>
57+
58+
<ngt-icosahedron-geometry #geometry attach="none" />
59+
@for (menu of menus; track menu.id) {
60+
<ngt-group [name]="menu.name" [position]="[15 * Math.cos(menu.angle), 0, 15 * Math.sin(menu.angle)]">
61+
<ngt-mesh
62+
cursor
63+
castShadow
64+
receiveShadow
65+
[position]="[0, 5, 0]"
66+
[geometry]="geometry"
67+
(click)="router.navigate([menu.path])"
68+
>
69+
<ngt-mesh-phong-material [color]="menu.color" [side]="FrontSide" />
70+
71+
<!-- NOTE: we can use ng-template for this use-case as well. -->
72+
<!-- @let templateRefs = coloredRockTemplateRefs(); -->
73+
<!-- @let template = templateRefs[menu.slug]; -->
74+
<!---->
75+
<!-- @if (template) { -->
76+
<!-- <ng-container [ngTemplateOutlet]="template" [ngTemplateOutletContext]="{ $implicit: menu }" /> -->
77+
<!-- } -->
78+
</ngt-mesh>
79+
</ngt-group>
80+
}
81+
82+
<router-outlet />
83+
`,
84+
imports: [RouterOutlet, NgtArgs, NgtsCameraControls, Cursor],
85+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
86+
changeDetection: ChangeDetectionStrategy.OnPush,
87+
host: { class: 'rocks' },
88+
})
89+
export default class Rocks {
90+
protected readonly Math = Math;
91+
protected readonly FrontSide = FrontSide;
92+
protected readonly DoubleSide = DoubleSide;
93+
94+
protected readonly menus = menus;
95+
96+
protected router = inject(Router);
97+
private rockStore = inject(RockStore);
98+
private store = injectStore();
99+
100+
protected gltf = injectGLTF<RockGLTF>(() => './rock2/scene.gltf');
101+
102+
constructor() {
103+
effect(() => {
104+
const controls = this.store.controls() as CameraControls;
105+
if (!controls) return;
106+
107+
const gltf = this.gltf();
108+
if (!gltf) return;
109+
110+
const scene = this.store.scene();
111+
const rock = this.rockStore.selectedRock();
112+
113+
const obj = rock ? scene.getObjectByName(rock.name) : gltf.scene;
114+
if (obj) {
115+
void controls.fitToBox(obj, true, { paddingTop: 5 });
116+
}
117+
});
118+
}
119+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Routes } from '@angular/router';
2+
3+
const routes: Routes = [
4+
{
5+
path: 'rocks',
6+
loadComponent: () => import('./rocks'),
7+
loadChildren: () => import('./rocks.routes'),
8+
},
9+
{
10+
path: '',
11+
redirectTo: 'rocks',
12+
pathMatch: 'full',
13+
},
14+
];
15+
16+
export default routes;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { ChangeDetectionStrategy, Component } from '@angular/core';
2+
import { extend, NgtRoutedScene } from 'angular-three';
3+
import { NgtCanvas, NgtCanvasContent } from 'angular-three/dom';
4+
import * as THREE from 'three';
5+
import { RockStore } from './store';
6+
7+
extend(THREE);
8+
9+
@Component({
10+
template: `
11+
<ngt-canvas [camera]="{ position: [8.978, 1.426, 2.766] }" shadows>
12+
<ngt-routed-scene *canvasContent />
13+
</ngt-canvas>
14+
`,
15+
imports: [NgtCanvas, NgtRoutedScene, NgtCanvasContent],
16+
providers: [RockStore],
17+
changeDetection: ChangeDetectionStrategy.OnPush,
18+
host: { class: 'routed-rocks block h-svh' },
19+
})
20+
export default class RoutedRocks {}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { inject, Injectable } from '@angular/core';
2+
import { toSignal } from '@angular/core/rxjs-interop';
3+
import { NavigationEnd, Router } from '@angular/router';
4+
import { filter, map, startWith } from 'rxjs';
5+
import { menus } from './constants';
6+
7+
@Injectable()
8+
export class RockStore {
9+
private router = inject(Router);
10+
11+
selectedRock = toSignal(
12+
this.router.events.pipe(
13+
filter((ev): ev is NavigationEnd => ev instanceof NavigationEnd),
14+
map((ev) => ev.urlAfterRedirects),
15+
startWith(this.router.url),
16+
map((url) => menus.find((menu) => menu.path === url) || null),
17+
),
18+
{ initialValue: null },
19+
);
20+
}

0 commit comments

Comments
 (0)