Skip to content

Commit 2729b57

Browse files
committed
feat(soba): add pivot controls
1 parent c92c021 commit 2729b57

File tree

7 files changed

+1436
-0
lines changed

7 files changed

+1436
-0
lines changed

Diff for: libs/soba/controls/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './lib/camera-controls';
22
export * from './lib/orbit-controls';
3+
export * from './lib/pivot-controls/pivot-controls';
34
export * from './lib/scroll-controls';
+255
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
computed,
5+
CUSTOM_ELEMENTS_SCHEMA,
6+
ElementRef,
7+
inject,
8+
input,
9+
signal,
10+
Signal,
11+
viewChild,
12+
} from '@angular/core';
13+
import { extend, injectStore, NgtArgs, NgtThreeEvent } from 'angular-three';
14+
import { NgtsLine } from 'angular-three-soba/abstractions';
15+
import { NgtsHTML, NgtsHTMLContent } from 'angular-three-soba/misc';
16+
import {
17+
ConeGeometry,
18+
CylinderGeometry,
19+
DoubleSide,
20+
Group,
21+
Matrix4,
22+
Mesh,
23+
MeshBasicMaterial,
24+
Quaternion,
25+
Vector3,
26+
} from 'three';
27+
import { NgtsPivotControls } from './pivot-controls';
28+
29+
const vec1 = new Vector3();
30+
const vec2 = new Vector3();
31+
32+
export function calculateOffset(clickPoint: Vector3, normal: Vector3, rayStart: Vector3, rayDir: Vector3) {
33+
const e1 = normal.dot(normal);
34+
const e2 = normal.dot(clickPoint) - normal.dot(rayStart);
35+
const e3 = normal.dot(rayDir);
36+
37+
if (e3 === 0) {
38+
return -e2 / e1;
39+
}
40+
41+
vec1
42+
.copy(rayDir)
43+
.multiplyScalar(e1 / e3)
44+
.sub(normal);
45+
vec2
46+
.copy(rayDir)
47+
.multiplyScalar(e2 / e3)
48+
.add(rayStart)
49+
.sub(clickPoint);
50+
51+
return -vec1.dot(vec2) / vec1.dot(vec1);
52+
}
53+
54+
const upV = new Vector3(0, 1, 0);
55+
const offsetMatrix = new Matrix4();
56+
57+
@Component({
58+
selector: 'ngts-axis-arrow',
59+
standalone: true,
60+
template: `
61+
<ngt-group #group>
62+
<ngt-group
63+
[matrix]="matrixL()"
64+
[matrixAutoUpdate]="false"
65+
(pointerdown)="onPointerDown($any($event))"
66+
(pointerup)="onPointerUp($any($event))"
67+
(pointermove)="onPointerMove($any($event))"
68+
(pointerout)="onPointerOut($any($event))"
69+
>
70+
@if (pivotControls.annotations()) {
71+
<ngts-html [options]="{ position: [0, -coneLength(), 0] }">
72+
<div
73+
#annotation
74+
ngtsHTMLContent
75+
style="display: none; background: #151520; color: white; padding: 6px 8px; border-radius: 7px; white-space: nowrap;"
76+
[class]="pivotControls.annotationsClass()"
77+
></div>
78+
</ngts-html>
79+
}
80+
<ngt-mesh
81+
[visible]="false"
82+
[position]="[0, (cylinderLength() + coneLength()) / 2.0, 0]"
83+
[userData]="pivotControls.userData()"
84+
>
85+
<ngt-cylinder-geometry
86+
*args="[coneWidth() * 1.4, coneWidth() * 1.4, cylinderLength() + coneLength(), 8, 1]"
87+
/>
88+
</ngt-mesh>
89+
90+
<ngts-line
91+
[points]="[0, 0, 0, 0, cylinderLength(), 0]"
92+
[options]="{
93+
raycast: null,
94+
side: DoubleSide,
95+
polygonOffset: true,
96+
polygonOffsetFactor: -10,
97+
renderOrder: 1,
98+
fog: false,
99+
transparent: true,
100+
lineWidth: pivotControls.lineWidth(),
101+
color: color(),
102+
opacity: pivotControls.opacity(),
103+
depthTest: pivotControls.depthTest(),
104+
}"
105+
/>
106+
107+
<ngt-mesh [raycast]="null" [position]="[0, cylinderLength() + coneLength() / 2.0, 0]" [renderOrder]="500">
108+
<ngt-cone-geometry *args="[coneWidth(), coneLength(), 24, 1]" />
109+
<ngt-mesh-basic-material
110+
[transparent]="true"
111+
[depthTest]="pivotControls.depthTest()"
112+
[color]="color()"
113+
[opacity]="pivotControls.opacity()"
114+
[polygonOffset]="true"
115+
[polygonOffsetFactor]="-10"
116+
[fog]="false"
117+
/>
118+
</ngt-mesh>
119+
</ngt-group>
120+
</ngt-group>
121+
`,
122+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
123+
changeDetection: ChangeDetectionStrategy.OnPush,
124+
imports: [NgtArgs, NgtsLine, NgtsHTML, NgtsHTMLContent],
125+
})
126+
export class NgtsAxisArrow {
127+
protected readonly DoubleSide = DoubleSide;
128+
129+
direction = input.required<Vector3>();
130+
axis = input.required<0 | 1 | 2>();
131+
132+
groupRef = viewChild.required<ElementRef<Group>>('group');
133+
annotationRef = viewChild<unknown, ElementRef<HTMLDivElement>>('annotation', { read: ElementRef });
134+
135+
pivotControls = inject(NgtsPivotControls);
136+
private store = injectStore();
137+
private controls = this.store.select('controls') as unknown as Signal<{ enabled: boolean }>;
138+
139+
private hovered = signal(false);
140+
private clickInfo: { clickPoint: Vector3; dir: Vector3 } | null = null;
141+
private offset0 = 0;
142+
143+
color = computed(() =>
144+
this.hovered() ? this.pivotControls.hoveredColor() : this.pivotControls.axisColors()[this.axis()],
145+
);
146+
147+
coneWidth = computed(() =>
148+
this.pivotControls.fixed()
149+
? (this.pivotControls.lineWidth() / this.pivotControls.scale()) * 1.6
150+
: this.pivotControls.scale() / 20,
151+
);
152+
coneLength = computed(() => (this.pivotControls.fixed() ? 0.2 : this.pivotControls.scale() / 5));
153+
cylinderLength = computed(() =>
154+
this.pivotControls.fixed() ? 1 - this.coneLength() : this.pivotControls.scale() - this.coneLength(),
155+
);
156+
matrixL = computed(() => {
157+
const quaternion = new Quaternion().setFromUnitVectors(upV, this.direction().clone().normalize());
158+
return new Matrix4().makeRotationFromQuaternion(quaternion);
159+
});
160+
161+
constructor() {
162+
extend({ Group, Mesh, ConeGeometry, CylinderGeometry, MeshBasicMaterial });
163+
}
164+
165+
onPointerDown(event: NgtThreeEvent<PointerEvent>) {
166+
const [group, direction, axis, controls, annotation] = [
167+
this.groupRef().nativeElement,
168+
this.direction(),
169+
this.axis(),
170+
this.controls(),
171+
this.annotationRef()?.nativeElement,
172+
];
173+
174+
if (annotation) {
175+
annotation.innerText = `${this.pivotControls.translation[axis].toFixed(2)}`;
176+
annotation.style.display = 'block';
177+
}
178+
179+
event.stopPropagation();
180+
181+
const rotation = new Matrix4().extractRotation(group.matrixWorld);
182+
const origin = new Vector3().setFromMatrixPosition(group.matrixWorld);
183+
const clickPoint = event.point.clone();
184+
const dir = direction.clone().applyMatrix4(rotation).normalize();
185+
this.clickInfo = { clickPoint, dir };
186+
this.offset0 = this.pivotControls.translation[axis];
187+
this.pivotControls.onDragStart({ component: 'Arrow', axis, origin, directions: [dir] });
188+
if (controls) {
189+
controls.enabled = false;
190+
}
191+
192+
// @ts-expect-error - setPointerCapture is not in the type definition
193+
event.target.setPointerCapture(event.pointerId);
194+
}
195+
196+
onPointerUp(event: NgtThreeEvent<PointerEvent>) {
197+
const [annotation, controls] = [this.annotationRef()?.nativeElement, this.controls()];
198+
199+
if (annotation) {
200+
annotation.style.display = 'none';
201+
}
202+
203+
event.stopPropagation();
204+
this.clickInfo = null;
205+
this.pivotControls.onDragEnd();
206+
if (controls) {
207+
controls.enabled = true;
208+
}
209+
210+
// @ts-expect-error - setPointerCapture is not in the type definition
211+
event.target.releasePointerCapture(event.pointerId);
212+
}
213+
214+
onPointerMove(event: NgtThreeEvent<PointerEvent>) {
215+
event.stopPropagation();
216+
217+
if (!this.hovered()) {
218+
this.hovered.set(true);
219+
}
220+
221+
if (this.clickInfo) {
222+
const { clickPoint, dir } = this.clickInfo;
223+
const [translationLimits, annotation, axis] = [
224+
this.pivotControls.translationLimits(),
225+
this.annotationRef()?.nativeElement,
226+
this.axis(),
227+
];
228+
229+
const [min, max] = translationLimits?.[axis] || [undefined, undefined];
230+
231+
let offset = calculateOffset(clickPoint, dir, event.ray.origin, event.ray.direction);
232+
if (min !== undefined) {
233+
offset = Math.max(offset, min - this.offset0);
234+
}
235+
236+
if (max !== undefined) {
237+
offset = Math.min(offset, max - this.offset0);
238+
}
239+
240+
this.pivotControls.translation[axis] = this.offset0 + offset;
241+
242+
if (annotation) {
243+
annotation.innerText = `${this.pivotControls.translation[axis].toFixed(2)}`;
244+
}
245+
246+
offsetMatrix.makeTranslation(dir.x * offset, dir.y * offset, dir.z * offset);
247+
this.pivotControls.onDrag(offsetMatrix);
248+
}
249+
}
250+
251+
onPointerOut(event: NgtThreeEvent<PointerEvent>) {
252+
event.stopPropagation();
253+
this.hovered.set(false);
254+
}
255+
}

0 commit comments

Comments
 (0)