Skip to content

Commit 30ebfb7

Browse files
committed
feat(soba/controls): add TrackballControls
1 parent 4bf8934 commit 30ebfb7

File tree

2 files changed

+133
-0
lines changed

2 files changed

+133
-0
lines changed

libs/soba/controls/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './lib/camera-controls';
22
export * from './lib/orbit-controls';
33
export * from './lib/scroll-controls';
4+
export * from './lib/trackball-controls';
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
computed,
5+
CUSTOM_ELEMENTS_SCHEMA,
6+
effect,
7+
input,
8+
output,
9+
} from '@angular/core';
10+
import {
11+
injectBeforeRender,
12+
injectStore,
13+
NgtArgs,
14+
NgtOverwrite,
15+
NgtThreeElement,
16+
NgtVector3,
17+
omit,
18+
pick,
19+
} from 'angular-three';
20+
import { mergeInputs } from 'ngxtension/inject-inputs';
21+
import * as THREE from 'three';
22+
import { TrackballControls } from 'three-stdlib';
23+
24+
export type NgtsTrackballControlsOptions = Omit<
25+
NgtOverwrite<
26+
NgtThreeElement<typeof TrackballControls>,
27+
{ target?: NgtVector3; camera?: THREE.Camera; domElement?: HTMLElement; regress: boolean; makeDefault: boolean }
28+
>,
29+
'attach' | 'addEventListener' | 'removeEventListener' | 'parameters' | '___ngt_args__'
30+
>;
31+
32+
const defaultOptions: NgtsTrackballControlsOptions = {
33+
regress: false,
34+
makeDefault: false,
35+
};
36+
37+
@Component({
38+
selector: 'ngts-trackball-controls',
39+
template: `
40+
<ngt-primitive *args="[controls()]" [parameters]="parameters()">
41+
<ng-content />
42+
</ngt-primitive>
43+
`,
44+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
45+
changeDetection: ChangeDetectionStrategy.OnPush,
46+
imports: [NgtArgs],
47+
})
48+
export class NgtsTrackballControls {
49+
options = input(defaultOptions, { transform: mergeInputs(defaultOptions) });
50+
protected parameters = omit(this.options, ['makeDefault', 'camera', 'regress', 'domElement']);
51+
52+
changed = output<THREE.Event>();
53+
started = output<THREE.Event>();
54+
ended = output<THREE.Event>();
55+
56+
private store = injectStore();
57+
58+
private camera = pick(this.options, 'camera');
59+
private regress = pick(this.options, 'regress');
60+
private domElement = pick(this.options, 'domElement');
61+
private makeDefault = pick(this.options, 'makeDefault');
62+
63+
protected controls = computed(() => {
64+
const camera = this.camera();
65+
if (camera) return new TrackballControls(camera as THREE.PerspectiveCamera);
66+
return new TrackballControls(this.store.camera());
67+
});
68+
69+
constructor() {
70+
injectBeforeRender(
71+
() => {
72+
const controls = this.controls();
73+
if (controls.enabled) controls.update();
74+
},
75+
{ priority: -1 },
76+
);
77+
78+
effect((onCleanup) => {
79+
const makeDefault = this.makeDefault();
80+
if (!makeDefault) return;
81+
82+
const controls = this.controls();
83+
const oldControls = this.store.snapshot.controls;
84+
this.store.update({ controls });
85+
onCleanup(() => void this.store.update({ controls: oldControls }));
86+
});
87+
88+
effect((onCleanup) => {
89+
const [controls, domElement] = [
90+
this.controls(),
91+
this.domElement() || this.store.snapshot.events.connected || this.store.gl.domElement(),
92+
this.store.invalidate(),
93+
this.regress(),
94+
];
95+
controls.connect(domElement);
96+
onCleanup(() => void controls.dispose());
97+
});
98+
99+
effect((onCleanup) => {
100+
const [controls, invalidate, performanceRegress, regress] = [
101+
this.controls(),
102+
this.store.invalidate(),
103+
this.store.performance.regress(),
104+
this.regress(),
105+
];
106+
107+
const changeCallback: (e: THREE.Event) => void = (e) => {
108+
invalidate();
109+
if (regress) performanceRegress();
110+
this.changed.emit(e);
111+
};
112+
113+
const startCallback = this.started.emit.bind(this.started);
114+
const endCallback = this.ended.emit.bind(this.ended);
115+
116+
controls.addEventListener('change', changeCallback);
117+
controls.addEventListener('start', startCallback);
118+
controls.addEventListener('end', endCallback);
119+
120+
onCleanup(() => {
121+
controls.removeEventListener('change', changeCallback);
122+
controls.removeEventListener('start', startCallback);
123+
controls.removeEventListener('end', endCallback);
124+
});
125+
});
126+
127+
effect(() => {
128+
const [controls] = [this.controls(), this.store.viewport()];
129+
controls.handleResize();
130+
});
131+
}
132+
}

0 commit comments

Comments
 (0)