Skip to content

Commit f52de02

Browse files
committed
wip: routed rock (need to work on refresh page logic)
1 parent abe0100 commit f52de02

File tree

10 files changed

+363
-0
lines changed

10 files changed

+363
-0
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { filter } from 'rxjs';
1616
<option value="rapier">/rapier</option>
1717
<option value="misc">/misc</option>
1818
<option value="routed">/routed</option>
19+
<option value="routed-rocks">/routed-rocks</option>
1920
</select>
2021
2122
<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
@@ -37,6 +37,12 @@ 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+
},
4046
{
4147
path: '',
4248
// redirectTo: 'cannon',
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
computed,
5+
CUSTOM_ELEMENTS_SCHEMA,
6+
effect,
7+
ElementRef,
8+
inject,
9+
viewChild,
10+
} from '@angular/core';
11+
import { injectStore, NgtArgs, NgtEuler, NgtVector3 } from 'angular-three';
12+
import { NgtsText } from 'angular-three-soba/abstractions';
13+
import { FrontSide, Group } from 'three';
14+
import { RockStore } from './store';
15+
16+
@Component({
17+
template: `
18+
<ngt-group #group attach="none">
19+
@if (selectedRock(); as rock) {
20+
<ngt-mesh
21+
[castShadow]="true"
22+
[receiveShadow]="true"
23+
[rotation]="[0, Math.PI / 4, 0]"
24+
[position]="[0, 2, 0]"
25+
[scale]="0.5"
26+
>
27+
<ngt-box-geometry *args="[0.7, 0.7, 0.7]" />
28+
<ngt-mesh-phong-material [color]="rock.color" [side]="FrontSide" />
29+
</ngt-mesh>
30+
31+
<ngt-group>
32+
@for (text of texts; track $index) {
33+
<ngts-text
34+
font="https://fonts.gstatic.com/s/raleway/v14/1Ptrg8zYS_SKggPNwK4vaqI.woff"
35+
[text]="rock.label"
36+
[options]="{
37+
color: rock.color,
38+
font: 'https://fonts.gstatic.com/s/raleway/v14/1Ptrg8zYS_SKggPNwK4vaqI.woff',
39+
fontSize: 0.5,
40+
position: text.position,
41+
rotation: text.rotation,
42+
}"
43+
/>
44+
}
45+
</ngt-group>
46+
}
47+
</ngt-group>
48+
`,
49+
imports: [NgtArgs, NgtsText],
50+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
51+
changeDetection: ChangeDetectionStrategy.OnPush,
52+
host: { class: 'colored-rock' },
53+
})
54+
export default class ColoredRock {
55+
protected readonly Math = Math;
56+
protected readonly FrontSide = FrontSide;
57+
58+
protected readonly texts = Array.from({ length: 3 }, (_, index) => ({
59+
rotation: [0, ((360 / 3) * index * Math.PI) / 180, 0] as NgtEuler,
60+
position: [
61+
5 * Math.cos(((360 / 3) * index * Math.PI) / 180),
62+
0,
63+
5 * Math.sin(((360 / 3) * index * Math.PI) / 180),
64+
] as NgtVector3,
65+
}));
66+
67+
private groupRef = viewChild.required<ElementRef<Group>>('group');
68+
69+
private rockStore = inject(RockStore);
70+
protected readonly selectedRock = this.rockStore.selectedRock;
71+
72+
private store = injectStore();
73+
private scene = this.store.select('scene');
74+
75+
private parent = computed(() => {
76+
const selected = this.selectedRock();
77+
if (!selected) return null;
78+
79+
const parent = this.scene().getObjectByName(selected.name);
80+
if (!parent) return null;
81+
82+
return parent;
83+
});
84+
85+
constructor() {
86+
effect((onCleanup) => {
87+
const parent = this.parent();
88+
if (!parent) return;
89+
90+
const group = this.groupRef().nativeElement;
91+
92+
parent.add(group);
93+
onCleanup(() => {
94+
parent.remove(group);
95+
});
96+
});
97+
}
98+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
const colors = [
2+
{
3+
color: '#042A2B',
4+
label: 'Rich Black',
5+
slug: 'rich-black',
6+
},
7+
{
8+
color: '#5EB1BF',
9+
label: 'Maximum Blue',
10+
slug: 'maximum-blue',
11+
},
12+
{
13+
color: '#CDEDF6',
14+
label: 'Light Cyan',
15+
slug: 'light-cyan',
16+
},
17+
{
18+
color: '#EF7B45',
19+
label: 'Mandarin',
20+
slug: 'mandarin',
21+
},
22+
{
23+
color: '#D84727',
24+
label: 'Vermilion',
25+
slug: 'vermilion',
26+
},
27+
] as const;
28+
29+
export const menus = colors.map((color, index) => ({
30+
id: index + 1,
31+
label: color.label,
32+
name: `rock-${color.slug}`,
33+
path: `/routed-rocks/rocks/${color.slug}`,
34+
contentId: index + 1,
35+
color: color.color,
36+
angle: ((360 / colors.length) * index * Math.PI) / 180,
37+
}));
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 { getLocalState, 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 localState = getLocalState(nativeElement);
15+
if (!localState) 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: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, effect, inject, Signal } from '@angular/core';
2+
import { Router } from '@angular/router';
3+
import { injectStore, NgtArgs, NgtRouterOutlet } 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]="true" [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]="true">
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+
name="theRock"
44+
[castShadow]="true"
45+
[receiveShadow]="true"
46+
[geometry]="gltf.nodes.defaultMaterial.geometry"
47+
[material]="gltf.materials['08___Default']"
48+
(click)="onRockClicked()"
49+
/>
50+
</ngt-group>
51+
</ngt-group>
52+
</ngt-group>
53+
}
54+
55+
<ngts-camera-controls
56+
[options]="{ makeDefault: true, minDistance: 12, maxDistance: 12, minPolarAngle: 0, maxPolarAngle: Math.PI / 2 }"
57+
/>
58+
59+
<ngt-icosahedron-geometry #geometry attach="none" />
60+
@for (menu of menus; track menu.id) {
61+
<ngt-group [position]="[15 * Math.cos(menu.angle), 0, 15 * Math.sin(menu.angle)]" [name]="'group-' + menu.id">
62+
<ngt-mesh
63+
cursor
64+
[name]="menu.name"
65+
[position]="[0, 5, 0]"
66+
[castShadow]="true"
67+
[receiveShadow]="true"
68+
[geometry]="geometry"
69+
(click)="onColoredRockClicked(menu)"
70+
>
71+
<ngt-mesh-phong-material [color]="menu.color" [side]="FrontSide" />
72+
</ngt-mesh>
73+
</ngt-group>
74+
}
75+
76+
<ngt-router-outlet />
77+
`,
78+
imports: [NgtRouterOutlet, NgtArgs, NgtsCameraControls, Cursor],
79+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
80+
changeDetection: ChangeDetectionStrategy.OnPush,
81+
host: { class: 'rocks' },
82+
})
83+
export default class Rocks {
84+
protected readonly Math = Math;
85+
protected readonly FrontSide = FrontSide;
86+
protected readonly DoubleSide = DoubleSide;
87+
88+
protected readonly menus = menus;
89+
90+
private router = inject(Router);
91+
private rockStore = inject(RockStore);
92+
private store = injectStore();
93+
94+
private scene = this.store.select('scene');
95+
private controls = this.store.select('controls') as Signal<CameraControls>;
96+
97+
protected gltf = injectGLTF<RockGLTF>(() => './rock2/scene.gltf');
98+
99+
constructor() {
100+
effect(() => {
101+
const controls = this.controls();
102+
if (!controls) return;
103+
104+
const gltf = this.gltf();
105+
if (!gltf) return;
106+
107+
const scene = this.scene();
108+
const rock = this.rockStore.selectedRock();
109+
110+
const obj = rock ? scene.getObjectByName(rock.name) : gltf.scene;
111+
if (obj) {
112+
void controls.fitToBox(obj, true, {
113+
paddingTop: 5,
114+
});
115+
}
116+
});
117+
}
118+
119+
onColoredRockClicked(menu: (typeof menus)[number]) {
120+
this.rockStore.selectedRock.set(menu);
121+
this.router.navigate([menu.path]);
122+
}
123+
124+
onRockClicked() {
125+
this.rockStore.selectedRock.set(null);
126+
this.router.navigate(['/routed-rocks/rocks']);
127+
}
128+
}
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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { ChangeDetectionStrategy, Component } from '@angular/core';
2+
import { extend, NgtCanvas } from 'angular-three';
3+
import * as THREE from 'three';
4+
import { RockStore } from './store';
5+
6+
extend(THREE);
7+
8+
@Component({
9+
template: `
10+
<ngt-canvas sceneGraph="routed" [camera]="{ position: [8.978, 1.426, 2.766] }" shadows />
11+
`,
12+
imports: [NgtCanvas],
13+
providers: [RockStore],
14+
changeDetection: ChangeDetectionStrategy.OnPush,
15+
host: { class: 'routed-rocks block h-svh' },
16+
})
17+
export default class RoutedRocks {}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { inject, Injectable, linkedSignal } 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+
private initialRock = 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+
21+
selectedRock = linkedSignal(this.initialRock);
22+
}

0 commit comments

Comments
 (0)