Skip to content

Commit 4b69aa3

Browse files
committed
orbit controls
1 parent 4d22f99 commit 4b69aa3

File tree

15 files changed

+31245
-23343
lines changed

15 files changed

+31245
-23343
lines changed

apps/examples/src/app/pages/cubes/scene.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@
66
(pointerout)="hover.set(false)"
77
>
88
<ngt-box-geometry />
9-
<ngt-mesh-phong-material [color]="hover() ? 'hotpink' : 'orange'" />
9+
<ngt-mesh-lambert-material [color]="hover() ? 'hotpink' : 'orange'" />
1010
</ngt-mesh>

libs/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export * from './lib/canvas';
33
export * from './lib/directives/args';
44
export * from './lib/directives/parent';
55
export * from './lib/directives/repeat';
6+
export { type NgtCamera } from './lib/events';
67
export * from './lib/instance';
78
export * from './lib/loader';
89
export { addAfterEffect, addEffect, addTail } from './lib/loop';

libs/core/src/lib/loop.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,6 @@ function render(timestamp: number, store: NgtSignalStore<NgtState>, frame?: XRFr
7878
}
7979

8080
function createLoop<TCanvas>(roots: Map<TCanvas, NgtSignalStore<NgtState>>) {
81-
console.log('createLoop', roots);
82-
8381
let running = false;
8482
let repeat: number;
8583
let frame: number;

libs/soba/controls/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# angular-three-soba/controls
2+
3+
Secondary entry point of `angular-three-soba`. It can be used by importing from `angular-three-soba/controls`.

libs/soba/controls/ng-package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"lib": {
3+
"entryFile": "src/index.ts"
4+
}
5+
}

libs/soba/controls/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './orbit-controls/orbit-controls';
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { Component, computed, CUSTOM_ELEMENTS_SCHEMA, effect, EventEmitter, Input, Output } from '@angular/core';
2+
import {
3+
injectBeforeRender,
4+
injectNgtRef,
5+
injectNgtStore,
6+
NgtArgs,
7+
signalStore,
8+
type NgtCamera,
9+
type NgtVector3,
10+
} from 'angular-three';
11+
import { OrbitControls } from 'three-stdlib';
12+
13+
export type NgtsOrbitControlsState = {
14+
camera?: THREE.Camera;
15+
domElement?: HTMLElement;
16+
target?: NgtVector3;
17+
makeDefault: boolean;
18+
regress: boolean;
19+
enableDamping: boolean;
20+
keyEvents: boolean | HTMLElement;
21+
};
22+
23+
declare global {
24+
interface HTMLElementTagNameMap {
25+
/**
26+
* @extends three-stdlib|OrbitControls
27+
*/
28+
'ngts-orbit-controls': OrbitControls & NgtsOrbitControlsState;
29+
}
30+
}
31+
32+
@Component({
33+
selector: 'ngts-orbit-controls',
34+
standalone: true,
35+
template: ` <ngt-primitive *args="args()" ngtCompound [enableDamping]="damping()" /> `,
36+
imports: [NgtArgs],
37+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
38+
})
39+
export class NgtsOrbitControls {
40+
private inputs = signalStore<NgtsOrbitControlsState>({
41+
enableDamping: true,
42+
regress: false,
43+
makeDefault: false,
44+
keyEvents: false,
45+
});
46+
47+
@Input() controlsRef = injectNgtRef<OrbitControls>();
48+
49+
@Input() set camera(camera: THREE.Camera) {
50+
this.inputs.set({ camera });
51+
}
52+
53+
@Input() set domElement(domElement: HTMLElement) {
54+
this.inputs.set({ domElement });
55+
}
56+
57+
@Input() set makeDefault(makeDefault: boolean) {
58+
this.inputs.set({ makeDefault });
59+
}
60+
61+
@Input() set regress(regress: boolean) {
62+
this.inputs.set({ regress });
63+
}
64+
65+
@Input() set target(target: THREE.Vector3 | Parameters<THREE.Vector3['set']>) {
66+
this.inputs.set({ target });
67+
}
68+
69+
@Input() set enableDamping(enableDamping: boolean) {
70+
this.inputs.set({ enableDamping });
71+
}
72+
73+
@Input() set keyEvents(keyEvents: boolean) {
74+
this.inputs.set({ keyEvents });
75+
}
76+
77+
@Output() change = new EventEmitter<THREE.Event>();
78+
@Output() start = new EventEmitter<THREE.Event>();
79+
@Output() end = new EventEmitter<THREE.Event>();
80+
81+
private store = injectNgtStore();
82+
83+
readonly args = computed(() => [this.controlsRef.nativeElement]);
84+
readonly damping = this.inputs.select('enableDamping');
85+
86+
constructor() {
87+
injectBeforeRender(
88+
() => {
89+
const controls = this.controlsRef.untracked;
90+
if (controls && controls.enabled) {
91+
controls.update();
92+
}
93+
},
94+
{ priority: -1 },
95+
);
96+
97+
this.setControls();
98+
this.connectElement();
99+
this.makeControlsDefault();
100+
this.setEvents();
101+
}
102+
103+
private setControls() {
104+
const camera = this.inputs.select('camera');
105+
const defaultCamera = this.store.select('camera');
106+
const trigger = computed(() => ({ camera: camera(), defaultCamera: defaultCamera() }));
107+
108+
effect(() => {
109+
const { camera, defaultCamera } = trigger();
110+
const controlsCamera = camera || defaultCamera;
111+
const controls = this.controlsRef.nativeElement;
112+
if (!controls || controls.object !== controlsCamera) {
113+
this.controlsRef.nativeElement = new OrbitControls(controlsCamera as NgtCamera);
114+
}
115+
});
116+
}
117+
118+
private connectElement() {
119+
const glDomElement = this.store.select('gl', 'domElement');
120+
const domElement = this.inputs.select('domElement');
121+
const regress = this.inputs.select('regress');
122+
const invalidate = this.store.select('invalidate');
123+
const keyEvents = this.inputs.select('keyEvents');
124+
125+
const trigger = computed(() => {
126+
const eventsSource = this.store.get('events', 'connected');
127+
return {
128+
keyEvents: keyEvents(),
129+
controls: this.controlsRef.nativeElement,
130+
domElement: domElement() || eventsSource || glDomElement(),
131+
regress: regress(),
132+
invalidate: invalidate(),
133+
};
134+
});
135+
136+
effect((onCleanup) => {
137+
const { domElement, controls, keyEvents } = trigger();
138+
if (!controls) return;
139+
if (keyEvents) {
140+
controls.connect(keyEvents === true ? domElement : keyEvents);
141+
} else {
142+
controls.connect(domElement);
143+
}
144+
onCleanup(() => void controls.dispose());
145+
});
146+
}
147+
148+
private makeControlsDefault() {
149+
const makeDefault = this.inputs.select('makeDefault');
150+
const trigger = computed(() => ({ controls: this.controlsRef.nativeElement, makeDefault: makeDefault() }));
151+
152+
effect((onCleanup) => {
153+
const { controls, makeDefault } = trigger();
154+
if (!controls) return;
155+
if (makeDefault) {
156+
const oldControls = this.store.get('controls');
157+
this.store.set({ controls });
158+
onCleanup(() => void this.store.set({ controls: oldControls }));
159+
}
160+
});
161+
}
162+
163+
private setEvents() {
164+
const invalidate = this.store.select('invalidate');
165+
const performance = this.store.select('performance');
166+
const regress = this.inputs.select('regress');
167+
168+
const trigger = computed(() => ({
169+
invalidate: invalidate(),
170+
performance: performance(),
171+
regress: regress(),
172+
controls: this.controlsRef.nativeElement,
173+
}));
174+
effect((onCleanup) => {
175+
const { controls, invalidate, performance, regress } = trigger();
176+
if (!controls) return;
177+
const changeCallback: (e: THREE.Event) => void = (e) => {
178+
invalidate();
179+
if (regress) performance.regress();
180+
if (this.change.observed) this.change.emit(e);
181+
};
182+
183+
const startCallback = this.start.observed ? this.start.emit.bind(this.start) : null;
184+
const endCallback = this.end.observed ? this.end.emit.bind(this.end) : null;
185+
186+
controls.addEventListener('change', changeCallback);
187+
if (startCallback) controls.addEventListener('start', startCallback);
188+
if (endCallback) controls.addEventListener('end', endCallback);
189+
190+
onCleanup(() => {
191+
controls.removeEventListener('change', changeCallback);
192+
if (startCallback) controls.removeEventListener('start', startCallback);
193+
if (endCallback) controls.removeEventListener('end', endCallback);
194+
});
195+
});
196+
}
197+
}

libs/soba/project.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
},
2323
"defaultConfiguration": "production"
2424
},
25-
2625
"package": {
2726
"executor": "nx:run-commands",
2827
"options": {
@@ -53,7 +52,9 @@
5352
"libs/soba/**/*.html",
5453
"libs/soba/package.json",
5554
"libs/soba/abstractions/**/*.ts",
56-
"libs/soba/abstractions/**/*.html"
55+
"libs/soba/abstractions/**/*.html",
56+
"libs/soba/controls/**/*.ts",
57+
"libs/soba/controls/**/*.html"
5758
]
5859
}
5960
},

libs/soba/src/abstractions/billboard.stories.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Component, CUSTOM_ELEMENTS_SCHEMA, Input } from '@angular/core';
22
import { Meta, moduleMetadata } from '@storybook/angular';
33
import { NgtArgs } from 'angular-three';
44
import { NgtsBillboard, NgtsText } from 'angular-three-soba/abstractions';
5-
// import { NgtsOrbitControls } from 'angular-three-soba/controls';
5+
import { NgtsOrbitControls } from 'angular-three-soba/controls';
66
import { BoxGeometry, ConeGeometry, PlaneGeometry } from 'three';
77
import { makeStoryObject, StorybookSetup } from '../setup-canvas';
88

@@ -90,9 +90,9 @@ class Plane {
9090
</BillboardPlane>
9191
</ngts-billboard>
9292
93-
<!-- <ngts-orbit-controls [enablePan]="true" [zoomSpeed]="0.5" /> -->
93+
<ngts-orbit-controls [enablePan]="true" [zoomSpeed]="0.5" />
9494
`,
95-
imports: [NgtsBillboard, NgtsText, Cone, Box, Plane],
95+
imports: [NgtsBillboard, NgtsText, Cone, Box, Plane, NgtsOrbitControls],
9696
schemas: [CUSTOM_ELEMENTS_SCHEMA],
9797
})
9898
class TextBillboardStory {
@@ -121,9 +121,9 @@ class TextBillboardStory {
121121
<BillboardPlane [args]="[3, 2]" color="yellow" />
122122
</ngts-billboard>
123123
124-
<!-- <ngts-orbit-controls [enablePan]="true" [zoomSpeed]="0.5" /> -->
124+
<ngts-orbit-controls [enablePan]="true" [zoomSpeed]="0.5" />
125125
`,
126-
imports: [NgtsBillboard, Plane],
126+
imports: [NgtsBillboard, Plane, NgtsOrbitControls],
127127
schemas: [CUSTOM_ELEMENTS_SCHEMA],
128128
})
129129
class DefaultBillboardStory {

libs/soba/src/setup-canvas.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
} from '@angular/core';
2424
import type { Args } from '@storybook/angular';
2525
import { NgtArgs, NgtCanvas, extend, safeDetectChanges, type NgtPerformance } from 'angular-three';
26-
// import { NgtsOrbitControls } from 'angular-three-soba/controls';
26+
import { NgtsOrbitControls } from 'angular-three-soba/controls';
2727
// import { NgtsLoader } from 'angular-three-soba/loaders';
2828
import * as THREE from 'three';
2929

@@ -81,12 +81,12 @@ const STORY_INPUTS = new InjectionToken<Signal<Record<string, unknown>>>('story
8181
</ng-container>
8282
8383
<ng-container *ngIf="canvasOptions.controls">
84-
<!-- <ngts-orbit-controls [makeDefault]="canvasOptions.controls?.makeDefault" /> -->
84+
<ngts-orbit-controls [makeDefault]="canvasOptions.controls?.makeDefault" />
8585
</ng-container>
8686
8787
<ng-container #anchor />
8888
`,
89-
imports: [NgIf, NgtArgs],
89+
imports: [NgIf, NgtArgs, NgtsOrbitControls],
9090
schemas: [CUSTOM_ELEMENTS_SCHEMA],
9191
})
9292
class StorybookScene implements OnInit {

0 commit comments

Comments
 (0)