Skip to content

Commit 0ece015

Browse files
committed
add cursor docs
1 parent 276aaf7 commit 0ece015

File tree

8 files changed

+160
-9
lines changed

8 files changed

+160
-9
lines changed

apps/astro-docs/astro.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ export default defineConfig({
132132
label: 'Advanced',
133133
collapsed: true,
134134
items: [
135+
{ label: 'Directives', slug: 'core/advanced/directives' },
135136
{ label: 'Portals', slug: 'core/advanced/portals' },
136137
{ label: 'Routed Scene', slug: 'core/advanced/routed-scene' },
137138
{ label: 'Performance', slug: 'core/advanced/performance' },
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { DOCUMENT } from '@angular/common';
2+
import {
3+
ChangeDetectionStrategy,
4+
Component,
5+
CUSTOM_ELEMENTS_SCHEMA,
6+
Directive,
7+
ElementRef,
8+
inject,
9+
signal,
10+
viewChild,
11+
} from '@angular/core';
12+
import { extend, getLocalState, injectBeforeRender, injectObjectEvents, NgtCanvas } from 'angular-three';
13+
import { NgtsEnvironment } from 'angular-three-soba/staging';
14+
import * as THREE from 'three';
15+
import { type Mesh, Object3D } from 'three';
16+
17+
extend(THREE);
18+
19+
@Directive({ selector: '[cursor]', standalone: true })
20+
export class Cursor {
21+
constructor() {
22+
const elementRef = inject<ElementRef<Object3D>>(ElementRef);
23+
const nativeElement = elementRef.nativeElement;
24+
25+
if (!nativeElement.isObject3D) return;
26+
27+
const localState = getLocalState(nativeElement);
28+
if (!localState) return;
29+
30+
const document = inject(DOCUMENT);
31+
32+
injectObjectEvents(() => nativeElement, {
33+
pointerover: () => {
34+
document.body.style.cursor = 'pointer';
35+
},
36+
pointerout: () => {
37+
document.body.style.cursor = 'default';
38+
},
39+
});
40+
}
41+
}
42+
43+
@Component({
44+
standalone: true,
45+
template: `
46+
<ngt-spot-light [position]="[5, 5, 5]" [intensity]="Math.PI" [decay]="0" />
47+
<ngt-point-light [position]="[-10, -10, -10]" [decay]="0" />
48+
49+
<ngt-mesh #mesh cursor (pointerover)="hovered.set(true)" (pointerout)="hovered.set(false)">
50+
<ngt-box-geometry />
51+
<ngt-mesh-standard-material [color]="hovered() ? 'mediumpurple' : 'maroon'" />
52+
</ngt-mesh>
53+
`,
54+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
55+
changeDetection: ChangeDetectionStrategy.OnPush,
56+
imports: [Cursor, NgtsEnvironment],
57+
})
58+
export class SceneGraph {
59+
hovered = signal(false);
60+
61+
meshRef = viewChild.required<ElementRef<Mesh>>('mesh');
62+
63+
constructor() {
64+
injectBeforeRender(() => {
65+
const mesh = this.meshRef().nativeElement;
66+
mesh.rotation.x += 0.01;
67+
mesh.rotation.y += 0.01;
68+
});
69+
}
70+
71+
protected readonly Math = Math;
72+
}
73+
74+
@Component({
75+
standalone: true,
76+
template: `
77+
<ngt-canvas [sceneGraph]="sceneGraph" [camera]="{ position: [0, 0, 2] }" />
78+
`,
79+
imports: [NgtCanvas],
80+
changeDetection: ChangeDetectionStrategy.OnPush,
81+
host: { class: 'cursor-scene' },
82+
})
83+
export default class CursorScene {
84+
sceneGraph = SceneGraph;
85+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
---
2+
title: Extending Functionality with Directives
3+
description: Details about extending functionality with Directives
4+
---
5+
6+
import CursorScene from '../../../../components/cursor/cursor';
7+
8+
Angular Three elements are like regular DOM elements; they are just rendered to the Canvas instead of the DOM.
9+
With that in mind, we can extend the functionality of Angular Three elements by using Directives like we do with regular DOM elements.
10+
11+
## Attribute Directives in Angular
12+
13+
When we attach an **Attribute Directive** to an element, we have access to the element's host instance via `ElementRef` token. Angular Three elements
14+
return the actual THREE.js entity instance as the host instance so that we can access the THREE.js APIs to extend the functionality of the element.
15+
16+
## Build a `cursor` Directive
17+
18+
Let's build a `cursor` directive that will change the cursor to a `pointer` when the element is hovered.
19+
20+
```angular-ts {'inject ElementRef<Object3D>': 4-5} {'Get localState': 9-10} {'Attach pointer events to host element': 14-15}
21+
@Directive({selector: '[cursor]', standalone: true})
22+
export class Cursor {
23+
constructor() {
24+
25+
const elementRef = inject<ElementRef<Object3D>>(ElementRef);
26+
const nativeElement = elementRef.nativeElement;
27+
28+
if (!nativeElement.isObject3D) return;
29+
30+
const localState = getLocalState(nativeElement);
31+
if (!localState) return;
32+
33+
const document = inject(DOCUMENT);
34+
35+
injectObjectEvents(() => nativeElement, {
36+
pointerover: () => {
37+
document.body.style.cursor = 'pointer';
38+
},
39+
pointerout: () => {
40+
document.body.style.cursor = 'default';
41+
},
42+
});
43+
}
44+
}
45+
```
46+
47+
Now, we can use the `cursor` directive on any element to change the cursor to a `pointer` when the element is hovered.
48+
49+
:::note
50+
51+
We do not constraint the type of the element that the `cursor` directive can be attached to but it only works for elements that
52+
are subjected to the events system like `Mesh` etc...
53+
54+
:::
55+
56+
```angular-html 'cursor'
57+
<ngt-mesh cursor (pointerover)="hovered.set(true)" (pointerout)="hovered.set(false)">
58+
<ngt-box-geometry />
59+
<ngt-mesh-standard-material [color]="hovered() ? 'mediumpurple' : 'maroon'" />
60+
</ngt-mesh>
61+
```
62+
63+
<div class="h-96 w-full border border-dashed border-accent-500 rounded">
64+
<CursorScene client:only />
65+
</div>

apps/astro-docs/src/content/docs/core/testing/advance.mdx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import { NgtTestBed } from 'angular-three/testing';
2929
describe('SceneGraph', () => {
3030
it('should render', async () => {
3131
const { scene, fireEvent, advance } = NgtTestBed.create(SceneGraph);
32-
fireEvent.setAutoDetectChanges(true);
3332

3433
expect(scene.children.length).toEqual(1);
3534
const mesh = scene.children[0] as Mesh;

apps/astro-docs/src/content/docs/core/testing/fire-event.mdx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,19 @@ fixture.detectChanges();
2525

2626
#### `fireEvent.setAutoDetectChanges(auto: boolean)`
2727

28-
After firing an event, we need to call `fixture.detectChanges()` to flush any changes that may have occurred (e.g: signal state changes).
28+
After firing an event, a Change Detection is needed with `fixture.detectChanges()` to flush any changes that may have occurred (e.g: signal state changes).
2929

30-
We can use `fireEvent.setAutoDetectChanges(true)` to automatically call `fixture.detectChanges()` after firing an event.
31-
This toggles an internal flag whose life-cycle is tied to the scope of the test. If we want to disable auto-detection anytime, we can call `fireEvent.setAutoDetectChanges(false)`.
30+
`fireEvent` does this automatically, but we can disable it by calling `fireEvent.setAutoDetectChanges(false)`.
3231

3332
```ts
34-
const { fireEvent } = NgtTestBed.create(SceneGraph);
35-
fireEvent.setAutoDetectChanges(true);
33+
const { fixture, fireEvent } = NgtTestBed.create(SceneGraph);
34+
fireEvent.setAutoDetectChanges(false);
3635

3736
await fireEvent(mesh, 'click');
37+
fixture.detectChanges();
38+
3839
await fireEvent(mesh, 'pointerover');
40+
fixture.detectChanges();
3941
```
4042

4143
## Example Scenario
@@ -49,7 +51,6 @@ import { NgtTestBed } from 'angular-three/testing';
4951
describe('SceneGraph', () => {
5052
it('should render', async () => {
5153
const { scene, fireEvent, advance } = NgtTestBed.create(SceneGraph);
52-
+ fireEvent.setAutoDetectChanges(true);
5354

5455
expect(scene.children.length).toEqual(1);
5556
const mesh = scene.children[0] as Mesh;

libs/cannon/src/lib/physics.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ export class NgtcPhysics {
183183

184184
this.autoEffect(() => {
185185
const [worker, value] = [untracked(this.worker), computedValue()];
186+
// @ts-expect-error
186187
worker[key] = value;
187188
});
188189
}

libs/core/testing/src/lib/test-bed.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ export class NgtTestBed {
143143
}
144144

145145
static createEventFirer(store: NgtSignalStore<NgtState>, fixture: ComponentFixture<NgtTestCanvas>) {
146-
let autoDetectChanges = false;
146+
let autoDetectChanges = true;
147147

148148
async function fireEvent(el: NgtInstanceNode, eventName: keyof NgtEventHandlers, eventData: NgtAnyRecord = {}) {
149149
const localState = getLocalState(el);

libs/core/testing/src/lib/test.spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ describe('test canvas', () => {
6060

6161
it('should test', async () => {
6262
const { scene, fireEvent, advance } = NgtTestBed.create(SceneGraph);
63-
fireEvent.setAutoDetectChanges(true);
6463

6564
expect(scene.children.length).toEqual(2);
6665

0 commit comments

Comments
 (0)