@buley/hexgrid-3d 1.1.1 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/math/HexCoordinates.ts +19 -16
- package/src/math/Vector3.ts +443 -20
package/package.json
CHANGED
|
@@ -1,22 +1,6 @@
|
|
|
1
1
|
import { Vector3 } from './Vector3';
|
|
2
2
|
|
|
3
|
-
export const AXIAL_DIRECTIONS: Axial[] = [
|
|
4
|
-
new Axial(1, 0),
|
|
5
|
-
new Axial(1, -1),
|
|
6
|
-
new Axial(0, -1),
|
|
7
|
-
new Axial(-1, 0),
|
|
8
|
-
new Axial(-1, 1),
|
|
9
|
-
new Axial(0, 1),
|
|
10
|
-
];
|
|
11
3
|
|
|
12
|
-
export const CUBE_DIRECTIONS: Cube[] = [
|
|
13
|
-
new Cube(1, -1, 0),
|
|
14
|
-
new Cube(1, 0, -1),
|
|
15
|
-
new Cube(0, 1, -1),
|
|
16
|
-
new Cube(-1, 1, 0),
|
|
17
|
-
new Cube(-1, 0, 1),
|
|
18
|
-
new Cube(0, -1, 1),
|
|
19
|
-
];
|
|
20
4
|
|
|
21
5
|
export class Axial {
|
|
22
6
|
q: number;
|
|
@@ -358,6 +342,25 @@ export class Cube {
|
|
|
358
342
|
}
|
|
359
343
|
}
|
|
360
344
|
|
|
345
|
+
|
|
346
|
+
export const AXIAL_DIRECTIONS: Axial[] = [
|
|
347
|
+
new Axial(1, 0),
|
|
348
|
+
new Axial(1, -1),
|
|
349
|
+
new Axial(0, -1),
|
|
350
|
+
new Axial(-1, 0),
|
|
351
|
+
new Axial(-1, 1),
|
|
352
|
+
new Axial(0, 1),
|
|
353
|
+
];
|
|
354
|
+
|
|
355
|
+
export const CUBE_DIRECTIONS: Cube[] = [
|
|
356
|
+
new Cube(1, -1, 0),
|
|
357
|
+
new Cube(1, 0, -1),
|
|
358
|
+
new Cube(0, 1, -1),
|
|
359
|
+
new Cube(-1, 1, 0),
|
|
360
|
+
new Cube(-1, 0, 1),
|
|
361
|
+
new Cube(0, -1, 1),
|
|
362
|
+
];
|
|
363
|
+
|
|
361
364
|
export class GeodesicHexGrid {
|
|
362
365
|
subdivisions: number;
|
|
363
366
|
vertices: Vector3[] = [];
|
package/src/math/Vector3.ts
CHANGED
|
@@ -7,6 +7,22 @@ export class Vector2 {
|
|
|
7
7
|
this.y = y;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
static zero(): Vector2 {
|
|
11
|
+
return new Vector2(0, 0);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static one(): Vector2 {
|
|
15
|
+
return new Vector2(1, 1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static fromAngle(angle: number, length: number = 1): Vector2 {
|
|
19
|
+
return new Vector2(Math.cos(angle) * length, Math.sin(angle) * length);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
clone(): Vector2 {
|
|
23
|
+
return new Vector2(this.x, this.y);
|
|
24
|
+
}
|
|
25
|
+
|
|
10
26
|
add(other: Vector2): Vector2 {
|
|
11
27
|
return new Vector2(this.x + other.x, this.y + other.y);
|
|
12
28
|
}
|
|
@@ -19,15 +35,25 @@ export class Vector2 {
|
|
|
19
35
|
return new Vector2(this.x * factor, this.y * factor);
|
|
20
36
|
}
|
|
21
37
|
|
|
22
|
-
|
|
38
|
+
dot(other: Vector2): number {
|
|
39
|
+
return this.x * other.x + this.y * other.y;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
cross(other: Vector2): number {
|
|
43
|
+
return this.x * other.y - this.y * other.x;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
magnitude(): number {
|
|
23
47
|
return Math.sqrt(this.x * this.x + this.y * this.y);
|
|
24
48
|
}
|
|
25
49
|
|
|
50
|
+
magnitudeSquared(): number {
|
|
51
|
+
return this.x * this.x + this.y * this.y;
|
|
52
|
+
}
|
|
53
|
+
|
|
26
54
|
normalize(): Vector2 {
|
|
27
|
-
const len = this.
|
|
28
|
-
if (len === 0)
|
|
29
|
-
return new Vector2(0, 0);
|
|
30
|
-
}
|
|
55
|
+
const len = this.magnitude();
|
|
56
|
+
if (len === 0) return new Vector2(0, 0);
|
|
31
57
|
return new Vector2(this.x / len, this.y / len);
|
|
32
58
|
}
|
|
33
59
|
|
|
@@ -36,6 +62,50 @@ export class Vector2 {
|
|
|
36
62
|
const dy = this.y - other.y;
|
|
37
63
|
return Math.sqrt(dx * dx + dy * dy);
|
|
38
64
|
}
|
|
65
|
+
|
|
66
|
+
angle(): number {
|
|
67
|
+
return Math.atan2(this.y, this.x);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
angleTo(other: Vector2): number {
|
|
71
|
+
return Math.acos(Math.max(-1, Math.min(1, this.dot(other) / (this.magnitude() * other.magnitude()))));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
rotate(angle: number): Vector2 {
|
|
75
|
+
const cos = Math.cos(angle);
|
|
76
|
+
const sin = Math.sin(angle);
|
|
77
|
+
return new Vector2(this.x * cos - this.y * sin, this.x * sin + this.y * cos);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
perpendicular(): Vector2 {
|
|
81
|
+
return new Vector2(-this.y, this.x);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
lerp(other: Vector2, t: number): Vector2 {
|
|
85
|
+
return new Vector2(
|
|
86
|
+
this.x + (other.x - this.x) * t,
|
|
87
|
+
this.y + (other.y - this.y) * t
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
toArray(): [number, number] {
|
|
92
|
+
return [this.x, this.y];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
toVector3(z: number = 0): Vector3 {
|
|
96
|
+
return new Vector3(this.x, this.y, z);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
equals(other: Vector2, epsilon: number = Number.EPSILON): boolean {
|
|
100
|
+
return (
|
|
101
|
+
Math.abs(this.x - other.x) < epsilon &&
|
|
102
|
+
Math.abs(this.y - other.y) < epsilon
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
toString(): string {
|
|
107
|
+
return `Vector2(${this.x.toFixed(4)}, ${this.y.toFixed(4)})`;
|
|
108
|
+
}
|
|
39
109
|
}
|
|
40
110
|
|
|
41
111
|
export class Vector3 {
|
|
@@ -49,25 +119,104 @@ export class Vector3 {
|
|
|
49
119
|
this.z = z;
|
|
50
120
|
}
|
|
51
121
|
|
|
52
|
-
static
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
): Vector3 {
|
|
122
|
+
static zero(): Vector3 {
|
|
123
|
+
return new Vector3(0, 0, 0);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
static one(): Vector3 {
|
|
127
|
+
return new Vector3(1, 1, 1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
static up(): Vector3 {
|
|
131
|
+
return new Vector3(0, 1, 0);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
static down(): Vector3 {
|
|
135
|
+
return new Vector3(0, -1, 0);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
static right(): Vector3 {
|
|
139
|
+
return new Vector3(1, 0, 0);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
static left(): Vector3 {
|
|
143
|
+
return new Vector3(-1, 0, 0);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
static forward(): Vector3 {
|
|
147
|
+
return new Vector3(0, 0, 1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
static back(): Vector3 {
|
|
151
|
+
return new Vector3(0, 0, -1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
static fromArray(arr: number[]): Vector3 {
|
|
155
|
+
return new Vector3(arr[0], arr[1], arr[2]);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
static fromSpherical(phi: number, theta: number, radius: number): Vector3 {
|
|
159
|
+
// Matches test expectation where (0,0,1) -> (1,0,0)
|
|
160
|
+
// Assuming phi is azimuth (longitude), theta is elevation (latitude), radius is magnitude.
|
|
161
|
+
// x = r * cos(theta) * cos(phi)
|
|
162
|
+
// y = r * cos(theta) * sin(phi)
|
|
163
|
+
// z = r * sin(theta)
|
|
164
|
+
const x = radius * Math.cos(theta) * Math.cos(phi);
|
|
165
|
+
const y = radius * Math.cos(theta) * Math.sin(phi);
|
|
166
|
+
const z = radius * Math.sin(theta);
|
|
167
|
+
return new Vector3(x, y, z);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
static fromLatLng(latitude: number, longitude: number, radius: number = 1): Vector3 {
|
|
57
171
|
const latRad = (latitude * Math.PI) / 180;
|
|
58
172
|
const lonRad = (longitude * Math.PI) / 180;
|
|
173
|
+
return Vector3.fromSpherical(lonRad, latRad, radius);
|
|
174
|
+
}
|
|
59
175
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
176
|
+
static random(): Vector3 {
|
|
177
|
+
// Random vector on unit sphere
|
|
178
|
+
const theta = Math.asin(2 * Math.random() - 1); // Elevation [-PI/2, PI/2]
|
|
179
|
+
const phi = 2 * Math.PI * Math.random(); // Azimuth [0, 2PI]
|
|
180
|
+
return Vector3.fromSpherical(phi, theta, 1);
|
|
181
|
+
}
|
|
63
182
|
|
|
64
|
-
|
|
183
|
+
static randomInSphere(radius: number): Vector3 {
|
|
184
|
+
const r = radius * Math.cbrt(Math.random());
|
|
185
|
+
return Vector3.random().scale(r);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
static catmullRom(p0: Vector3, p1: Vector3, p2: Vector3, p3: Vector3, t: number): Vector3 {
|
|
189
|
+
const t2 = t * t;
|
|
190
|
+
const t3 = t2 * t;
|
|
191
|
+
|
|
192
|
+
const f0 = -0.5 * t3 + t2 - 0.5 * t;
|
|
193
|
+
const f1 = 1.5 * t3 - 2.5 * t2 + 1.0;
|
|
194
|
+
const f2 = -1.5 * t3 + 2.0 * t2 + 0.5 * t;
|
|
195
|
+
const f3 = 0.5 * t3 - 0.5 * t2;
|
|
196
|
+
|
|
197
|
+
return p0.scale(f0).add(p1.scale(f1)).add(p2.scale(f2)).add(p3.scale(f3));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
static bezier(points: Vector3[], t: number): Vector3 {
|
|
201
|
+
if (points.length === 0) return Vector3.zero();
|
|
202
|
+
if (points.length === 1) return points[0].clone();
|
|
203
|
+
|
|
204
|
+
let temp = points.map(p => p.clone());
|
|
205
|
+
let n = temp.length - 1;
|
|
206
|
+
|
|
207
|
+
for (let i = 0; i < n; i++) {
|
|
208
|
+
for (let j = 0; j < n - i; j++) {
|
|
209
|
+
temp[j] = temp[j].lerp(temp[j+1], t);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return temp[0];
|
|
65
213
|
}
|
|
66
214
|
|
|
67
215
|
clone(): Vector3 {
|
|
68
216
|
return new Vector3(this.x, this.y, this.z);
|
|
69
217
|
}
|
|
70
218
|
|
|
219
|
+
// Immutable operations return new vectors
|
|
71
220
|
add(other: Vector3): Vector3 {
|
|
72
221
|
return new Vector3(this.x + other.x, this.y + other.y, this.z + other.z);
|
|
73
222
|
}
|
|
@@ -80,6 +229,59 @@ export class Vector3 {
|
|
|
80
229
|
return new Vector3(this.x * factor, this.y * factor, this.z * factor);
|
|
81
230
|
}
|
|
82
231
|
|
|
232
|
+
multiply(other: Vector3): Vector3 {
|
|
233
|
+
return new Vector3(this.x * other.x, this.y * other.y, this.z * other.z);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
divide(other: Vector3): Vector3 {
|
|
237
|
+
return new Vector3(
|
|
238
|
+
other.x === 0 ? 0 : this.x / other.x,
|
|
239
|
+
other.y === 0 ? 0 : this.y / other.y,
|
|
240
|
+
other.z === 0 ? 0 : this.z / other.z
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
negate(): Vector3 {
|
|
245
|
+
return new Vector3(-this.x, -this.y, -this.z);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Mutable operations modify 'this'
|
|
249
|
+
set(x: number, y: number, z: number): this {
|
|
250
|
+
this.x = x; this.y = y; this.z = z;
|
|
251
|
+
return this;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
copy(other: Vector3): this {
|
|
255
|
+
this.x = other.x; this.y = other.y; this.z = other.z;
|
|
256
|
+
return this;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
addInPlace(other: Vector3): this {
|
|
260
|
+
this.x += other.x; this.y += other.y; this.z += other.z;
|
|
261
|
+
return this;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
subtractInPlace(other: Vector3): this {
|
|
265
|
+
this.x -= other.x; this.y -= other.y; this.z -= other.z;
|
|
266
|
+
return this;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
scaleInPlace(factor: number): this {
|
|
270
|
+
this.x *= factor; this.y *= factor; this.z *= factor;
|
|
271
|
+
return this;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
normalizeInPlace(): this {
|
|
275
|
+
const len = this.length();
|
|
276
|
+
if (len > 0) {
|
|
277
|
+
this.scaleInPlace(1 / len);
|
|
278
|
+
} else {
|
|
279
|
+
this.x = 0; this.y = 0; this.z = 0;
|
|
280
|
+
}
|
|
281
|
+
return this;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Products
|
|
83
285
|
dot(other: Vector3): number {
|
|
84
286
|
return this.x * other.x + this.y * other.y + this.z * other.z;
|
|
85
287
|
}
|
|
@@ -92,6 +294,11 @@ export class Vector3 {
|
|
|
92
294
|
);
|
|
93
295
|
}
|
|
94
296
|
|
|
297
|
+
tripleProduct(b: Vector3, c: Vector3): number {
|
|
298
|
+
return this.dot(b.cross(c));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Magnitude & Distance
|
|
95
302
|
length(): number {
|
|
96
303
|
return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
|
|
97
304
|
}
|
|
@@ -100,18 +307,234 @@ export class Vector3 {
|
|
|
100
307
|
return this.length();
|
|
101
308
|
}
|
|
102
309
|
|
|
310
|
+
magnitudeSquared(): number {
|
|
311
|
+
return this.x * this.x + this.y * this.y + this.z * this.z;
|
|
312
|
+
}
|
|
313
|
+
|
|
103
314
|
normalize(): Vector3 {
|
|
104
|
-
|
|
105
|
-
if (len === 0) {
|
|
106
|
-
return new Vector3(0, 0, 0);
|
|
107
|
-
}
|
|
108
|
-
return new Vector3(this.x / len, this.y / len, this.z / len);
|
|
315
|
+
return this.clone().normalizeInPlace();
|
|
109
316
|
}
|
|
110
317
|
|
|
111
318
|
distanceTo(other: Vector3): number {
|
|
319
|
+
return Math.sqrt(this.distanceSquaredTo(other));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
distanceSquaredTo(other: Vector3): number {
|
|
112
323
|
const dx = this.x - other.x;
|
|
113
324
|
const dy = this.y - other.y;
|
|
114
325
|
const dz = this.z - other.z;
|
|
115
|
-
return
|
|
326
|
+
return dx*dx + dy*dy + dz*dz;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Interpolation
|
|
330
|
+
lerp(other: Vector3, t: number): Vector3 {
|
|
331
|
+
return new Vector3(
|
|
332
|
+
this.x + (other.x - this.x) * t,
|
|
333
|
+
this.y + (other.y - this.y) * t,
|
|
334
|
+
this.z + (other.z - this.z) * t
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
slerp(other: Vector3, t: number): Vector3 {
|
|
339
|
+
let dot = this.dot(other);
|
|
340
|
+
// Clamp dot to [-1, 1]
|
|
341
|
+
dot = Math.max(-1, Math.min(1, dot));
|
|
342
|
+
|
|
343
|
+
const theta = Math.acos(dot);
|
|
344
|
+
const sinTheta = Math.sin(theta);
|
|
345
|
+
|
|
346
|
+
if (Math.abs(sinTheta) < 0.001) {
|
|
347
|
+
return this.lerp(other, t);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const w1 = Math.sin((1 - t) * theta) / sinTheta;
|
|
351
|
+
const w2 = Math.sin(t * theta) / sinTheta;
|
|
352
|
+
|
|
353
|
+
return this.scale(w1).add(other.scale(w2));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
nlerp(other: Vector3, t: number): Vector3 {
|
|
357
|
+
return this.lerp(other, t).normalize();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
hermite(t1: Vector3, p2: Vector3, t2: Vector3, t: number): Vector3 {
|
|
361
|
+
const tSq = t * t;
|
|
362
|
+
const tCub = tSq * t;
|
|
363
|
+
|
|
364
|
+
const h1 = 2*tCub - 3*tSq + 1;
|
|
365
|
+
const h2 = -2*tCub + 3*tSq;
|
|
366
|
+
const h3 = tCub - 2*tSq + t;
|
|
367
|
+
const h4 = tCub - tSq;
|
|
368
|
+
|
|
369
|
+
return this.scale(h1).add(p2.scale(h2)).add(t1.scale(h3)).add(t2.scale(h4));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Utility
|
|
373
|
+
toArray(): [number, number, number] {
|
|
374
|
+
return [this.x, this.y, this.z];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
toFloat32Array(): Float32Array {
|
|
378
|
+
return new Float32Array([this.x, this.y, this.z]);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
equals(other: Vector3, epsilon: number = Number.EPSILON): boolean {
|
|
382
|
+
return (
|
|
383
|
+
Math.abs(this.x - other.x) < epsilon &&
|
|
384
|
+
Math.abs(this.y - other.y) < epsilon &&
|
|
385
|
+
Math.abs(this.z - other.z) < epsilon
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
isZero(): boolean {
|
|
390
|
+
return this.x === 0 && this.y === 0 && this.z === 0;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
abs(): Vector3 {
|
|
394
|
+
return new Vector3(Math.abs(this.x), Math.abs(this.y), Math.abs(this.z));
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
floor(): Vector3 {
|
|
398
|
+
return new Vector3(Math.floor(this.x), Math.floor(this.y), Math.floor(this.z));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
ceil(): Vector3 {
|
|
402
|
+
return new Vector3(Math.ceil(this.x), Math.ceil(this.y), Math.ceil(this.z));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
round(): Vector3 {
|
|
406
|
+
return new Vector3(Math.round(this.x), Math.round(this.y), Math.round(this.z));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
min(other: Vector3): Vector3 {
|
|
410
|
+
return new Vector3(Math.min(this.x, other.x), Math.min(this.y, other.y), Math.min(this.z, other.z));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
max(other: Vector3): Vector3 {
|
|
414
|
+
return new Vector3(Math.max(this.x, other.x), Math.max(this.y, other.y), Math.max(this.z, other.z));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
clamp(min: Vector3, max: Vector3): Vector3 {
|
|
418
|
+
return new Vector3(
|
|
419
|
+
Math.max(min.x, Math.min(max.x, this.x)),
|
|
420
|
+
Math.max(min.y, Math.min(max.y, this.y)),
|
|
421
|
+
Math.max(min.z, Math.min(max.z, this.z))
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
clampMagnitude(max: number): Vector3 {
|
|
426
|
+
const len = this.magnitude();
|
|
427
|
+
if (len > max) {
|
|
428
|
+
return this.scale(max / len);
|
|
429
|
+
}
|
|
430
|
+
return this.clone();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
setMagnitude(len: number): Vector3 {
|
|
434
|
+
return this.normalize().scale(len);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
reflect(normal: Vector3): Vector3 {
|
|
438
|
+
// r = d - 2(d.n)n
|
|
439
|
+
const d = this;
|
|
440
|
+
const n = normal.normalize(); // Ensure normal is normalized
|
|
441
|
+
const term = n.scale(2 * d.dot(n));
|
|
442
|
+
return d.subtract(term);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Angles - Spherical
|
|
446
|
+
angleTo(other: Vector3): number {
|
|
447
|
+
const denom = this.magnitude() * other.magnitude();
|
|
448
|
+
if (denom === 0) return 0;
|
|
449
|
+
const dot = this.dot(other);
|
|
450
|
+
return Math.acos(Math.max(-1, Math.min(1, dot / denom)));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
signedAngleTo(other: Vector3, axis: Vector3): number {
|
|
454
|
+
const angle = this.angleTo(other);
|
|
455
|
+
const cross = this.cross(other);
|
|
456
|
+
const sign = cross.dot(axis) >= 0 ? 1 : -1;
|
|
457
|
+
return angle * sign;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
toSpherical(): { radius: number; theta: number; phi: number } {
|
|
461
|
+
const r = this.magnitude();
|
|
462
|
+
if (r === 0) return { radius: 0, theta: 0, phi: 0 };
|
|
463
|
+
const theta = Math.asin(this.z / r);
|
|
464
|
+
const phi = Math.atan2(this.y, this.x);
|
|
465
|
+
return { radius: r, theta, phi: phi >= 0 ? phi : phi + 2 * Math.PI };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
toLatLng(): { lat: number; lng: number } {
|
|
469
|
+
const r = this.magnitude();
|
|
470
|
+
if (r === 0) return { lat: 0, lng: 0 };
|
|
471
|
+
const lat = Math.asin(this.z / r);
|
|
472
|
+
const lng = Math.atan2(this.y, this.x);
|
|
473
|
+
return {
|
|
474
|
+
lat: (lat * 180) / Math.PI,
|
|
475
|
+
lng: (lng * 180) / Math.PI
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
greatCircleDistanceTo(other: Vector3): number {
|
|
480
|
+
return this.angleTo(other);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
greatCircleLerp(other: Vector3, t: number): Vector3 {
|
|
484
|
+
return this.slerp(other, t);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Rotations
|
|
488
|
+
rotateAround(axis: Vector3, angle: number): Vector3 {
|
|
489
|
+
const k = axis.normalize();
|
|
490
|
+
const cos = Math.cos(angle);
|
|
491
|
+
const sin = Math.sin(angle);
|
|
492
|
+
|
|
493
|
+
const term1 = this.scale(cos);
|
|
494
|
+
const term2 = k.cross(this).scale(sin);
|
|
495
|
+
const term3 = k.scale(k.dot(this) * (1 - cos));
|
|
496
|
+
|
|
497
|
+
return term1.add(term2).add(term3);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
rotateX(angle: number): Vector3 {
|
|
501
|
+
return this.rotateAround(Vector3.right(), angle);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
rotateY(angle: number): Vector3 {
|
|
505
|
+
return this.rotateAround(Vector3.up(), angle);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
rotateZ(angle: number): Vector3 {
|
|
509
|
+
return this.rotateAround(Vector3.forward(), angle);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Projection
|
|
513
|
+
projectOnto(other: Vector3): Vector3 {
|
|
514
|
+
const denom = other.magnitudeSquared();
|
|
515
|
+
if (denom === 0) return Vector3.zero();
|
|
516
|
+
return other.scale(this.dot(other) / denom);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
projectOntoPlane(normal: Vector3): Vector3 {
|
|
520
|
+
const d = this.projectOnto(normal);
|
|
521
|
+
return this.subtract(d);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
refract(normal: Vector3, eta: number): Vector3 {
|
|
525
|
+
// Snell's law in vector form
|
|
526
|
+
const n = normal.normalize();
|
|
527
|
+
const i = this.normalize();
|
|
528
|
+
const nDotI = n.dot(i);
|
|
529
|
+
const k = 1 - eta * eta * (1 - nDotI * nDotI);
|
|
530
|
+
if (k < 0) {
|
|
531
|
+
// Total internal reflection - test expects a vector > 0
|
|
532
|
+
return this.reflect(normal);
|
|
533
|
+
}
|
|
534
|
+
return i.scale(eta).subtract(n.scale(eta * nDotI + Math.sqrt(k)));
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
toString(): string {
|
|
538
|
+
return `Vector3(${this.x.toFixed(4)}, ${this.y.toFixed(4)}, ${this.z.toFixed(4)})`;
|
|
116
539
|
}
|
|
117
540
|
}
|