Skip to content

Commit f99d043

Browse files
committed
feat(soba): add gizmo helpers
1 parent 1457ce7 commit f99d043

File tree

6 files changed

+903
-1
lines changed

6 files changed

+903
-1
lines changed

libs/soba/gizmos/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
1+
export * from './lib/gizmo-helper/gizmo-helper';
2+
export { NgtsGizmoViewcube, NgtsGizmoViewcubeOptions } from './lib/gizmo-helper/gizmo-viewcube';
3+
export { NgtsGizmoViewport, NgtsGizmoViewportOptions } from './lib/gizmo-helper/gizmo-viewport';
14
export * from './lib/pivot-controls/pivot-controls';
25
export * from './lib/transform-controls';
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import { NgTemplateOutlet } from '@angular/common';
2+
import {
3+
ChangeDetectionStrategy,
4+
Component,
5+
computed,
6+
contentChild,
7+
CUSTOM_ELEMENTS_SCHEMA,
8+
Directive,
9+
effect,
10+
ElementRef,
11+
input,
12+
output,
13+
Signal,
14+
TemplateRef,
15+
viewChild,
16+
} from '@angular/core';
17+
import { extend, hasListener, injectBeforeRender, injectStore, NgtPortal, NgtPortalContent, pick } from 'angular-three';
18+
import { NgtsOrthographicCamera } from 'angular-three-soba/cameras';
19+
import CameraControls from 'camera-controls';
20+
import { mergeInputs } from 'ngxtension/inject-inputs';
21+
import { Group, Matrix4, Object3D, Quaternion, Scene, Vector3 } from 'three';
22+
import { OrbitControls } from 'three-stdlib';
23+
24+
const turnRate = 2 * Math.PI; // turn rate in angles per second
25+
const dummy = new Object3D();
26+
const matrix = new Matrix4();
27+
const [q1, q2] = [new Quaternion(), new Quaternion()];
28+
const target = new Vector3();
29+
const targetPosition = new Vector3();
30+
31+
@Directive({ selector: 'ng-template[gizmoHelperContent]', standalone: true })
32+
export class NgtsGizmoHelperContent {
33+
static ngTemplateContextGuard(_: NgtsGizmoHelperContent, ctx: unknown): ctx is { container: Object3D } {
34+
return true;
35+
}
36+
}
37+
38+
type ControlsProto = { update(delta?: number): void; target: Vector3 };
39+
40+
export interface NgtsGizmoHelperOptions {
41+
alignment:
42+
| 'top-left'
43+
| 'top-right'
44+
| 'bottom-right'
45+
| 'bottom-left'
46+
| 'bottom-center'
47+
| 'center-right'
48+
| 'center-left'
49+
| 'center-center'
50+
| 'top-center';
51+
margin: [number, number];
52+
renderPriority: number;
53+
autoClear?: boolean;
54+
}
55+
56+
const defaultOptions: NgtsGizmoHelperOptions = {
57+
alignment: 'bottom-right',
58+
margin: [80, 80],
59+
renderPriority: 1,
60+
};
61+
62+
@Component({
63+
selector: 'ngts-gizmo-helper',
64+
standalone: true,
65+
template: `
66+
<ngt-portal
67+
[container]="scene"
68+
[autoRender]="true"
69+
[autoRenderPriority]="renderPriority()"
70+
[state]="{ events: { priority: renderPriority() + 1 } }"
71+
>
72+
<ng-template portalContent let-injector="injector" let-container="container">
73+
<ngts-orthographic-camera [options]="{ makeDefault: true, position: [0, 0, 200] }" />
74+
<ngt-group #gizmo [position]="[x(), y(), 0]">
75+
<ng-container
76+
[ngTemplateOutlet]="content()"
77+
[ngTemplateOutletContext]="{ container, injector }"
78+
[ngTemplateOutletInjector]="injector"
79+
/>
80+
</ngt-group>
81+
</ng-template>
82+
</ngt-portal>
83+
`,
84+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
85+
changeDetection: ChangeDetectionStrategy.OnPush,
86+
imports: [NgtPortal, NgtPortalContent, NgtsOrthographicCamera, NgTemplateOutlet],
87+
})
88+
export class NgtsGizmoHelper {
89+
options = input(defaultOptions, { transform: mergeInputs(defaultOptions) });
90+
update = output<void>();
91+
92+
protected renderPriority = pick(this.options, 'renderPriority');
93+
protected margin = pick(this.options, 'margin');
94+
protected alignment = pick(this.options, 'alignment');
95+
96+
protected scene = new Scene();
97+
98+
protected content = contentChild.required(NgtsGizmoHelperContent, { read: TemplateRef });
99+
100+
private gizmoRef = viewChild<ElementRef<Group>>('gizmo');
101+
private virtualCameraRef = viewChild(NgtsOrthographicCamera);
102+
103+
private store = injectStore();
104+
private size = this.store.select('size');
105+
private mainCamera = this.store.select('camera');
106+
private defaultControls = this.store.select('controls') as unknown as Signal<ControlsProto>;
107+
private invalidate = this.store.select('invalidate');
108+
109+
protected x = computed(() => {
110+
const alignment = this.alignment();
111+
if (alignment.endsWith('-center')) return 0;
112+
113+
const [{ width }, [marginX]] = [this.size(), this.margin()];
114+
return alignment.endsWith('-left') ? -width / 2 + marginX : width / 2 - marginX;
115+
});
116+
protected y = computed(() => {
117+
const alignment = this.alignment();
118+
if (alignment.startsWith('center-')) return 0;
119+
120+
const [{ height }, [marginY]] = [this.size(), this.margin()];
121+
return alignment.startsWith('top-') ? height / 2 - marginY : -height / 2 + marginY;
122+
});
123+
124+
private animating = false;
125+
private radius = 0;
126+
private focusPoint = new Vector3(0, 0, 0);
127+
private defaultUp = new Vector3(0, 0, 0);
128+
129+
constructor() {
130+
extend({ Group });
131+
effect(() => {
132+
this.updateDefaultUpEffect();
133+
});
134+
135+
injectBeforeRender(({ delta }) => {
136+
const [virtualCamera, gizmo] = [
137+
this.virtualCameraRef()?.cameraRef()?.nativeElement,
138+
this.gizmoRef()?.nativeElement,
139+
];
140+
141+
if (!virtualCamera || !gizmo) return;
142+
143+
const [defaultControls, mainCamera, invalidate] = [this.defaultControls(), this.mainCamera(), this.invalidate()];
144+
145+
// Animate step
146+
if (this.animating) {
147+
if (q1.angleTo(q2) < 0.01) {
148+
this.animating = false;
149+
// Orbit controls uses UP vector as the orbit axes,
150+
// so we need to reset it after the animation is done
151+
// moving it around for the controls to work correctly
152+
if (this.isOrbitControls(defaultControls)) {
153+
mainCamera.up.copy(this.defaultUp);
154+
}
155+
} else {
156+
const step = delta * turnRate;
157+
// animate position by doing a slerp and then scaling the position on the unit sphere
158+
q1.rotateTowards(q2, step);
159+
// animate orientation
160+
mainCamera.position.set(0, 0, 1).applyQuaternion(q1).multiplyScalar(this.radius).add(this.focusPoint);
161+
mainCamera.up.set(0, 1, 0).applyQuaternion(q1).normalize();
162+
mainCamera.quaternion.copy(q1);
163+
164+
if (this.isCameraControls(defaultControls)) {
165+
void defaultControls.setPosition(mainCamera.position.x, mainCamera.position.y, mainCamera.position.z);
166+
}
167+
168+
if (hasListener(this.update)) this.update.emit();
169+
else if (defaultControls) defaultControls.update(delta);
170+
171+
invalidate();
172+
}
173+
}
174+
175+
// Sync Gizmo with main camera orientation
176+
matrix.copy(mainCamera.matrix).invert();
177+
gizmo.quaternion.setFromRotationMatrix(matrix);
178+
});
179+
}
180+
181+
tweenCamera(direction: Vector3) {
182+
const [defaultControls, invalidate, mainCamera] = [this.defaultControls(), this.invalidate(), this.mainCamera()];
183+
184+
this.animating = true;
185+
if (defaultControls) {
186+
this.focusPoint = this.isCameraControls(defaultControls)
187+
? defaultControls.getTarget(this.focusPoint)
188+
: defaultControls.target;
189+
}
190+
this.radius = mainCamera.position.distanceTo(target);
191+
192+
// Rotate from current camera orientation
193+
q1.copy(mainCamera.quaternion);
194+
195+
// To new current camera orientation
196+
targetPosition.copy(direction).multiplyScalar(this.radius).add(target);
197+
dummy.lookAt(targetPosition);
198+
q2.copy(dummy.quaternion);
199+
200+
invalidate();
201+
}
202+
203+
private updateDefaultUpEffect() {
204+
const mainCamera = this.mainCamera();
205+
this.defaultUp.copy(mainCamera.up);
206+
}
207+
208+
private isOrbitControls(controls: ControlsProto): controls is OrbitControls {
209+
return 'minPolarAngle' in (controls as OrbitControls);
210+
}
211+
212+
private isCameraControls(controls: CameraControls | ControlsProto): controls is CameraControls {
213+
return 'getTarget' in (controls as CameraControls);
214+
}
215+
}

0 commit comments

Comments
 (0)