@pluot/core 0.1.0 → 0.1.1

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.
Files changed (34) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +1109 -4101
  3. package/dist-tsc/functional-3d-view-controls.d.ts +11 -0
  4. package/dist-tsc/functional-3d-view-controls.d.ts.map +1 -0
  5. package/dist-tsc/functional-3d-view-controls.js +517 -0
  6. package/dist-tsc/functional-dom-2d-camera.d.ts +9 -0
  7. package/dist-tsc/functional-dom-2d-camera.d.ts.map +1 -0
  8. package/dist-tsc/functional-dom-2d-camera.js +178 -0
  9. package/dist-tsc/index.d.ts +2 -2
  10. package/dist-tsc/index.d.ts.map +1 -1
  11. package/dist-tsc/index.js +2 -2
  12. package/dist-tsc/lru-store.d.ts.map +1 -1
  13. package/dist-tsc/lru-store.js +3 -0
  14. package/dist-tsc/unrolled-3d-view-controls.d.ts +2 -0
  15. package/dist-tsc/unrolled-3d-view-controls.d.ts.map +1 -0
  16. package/dist-tsc/unrolled-3d-view-controls.js +637 -0
  17. package/dist-tsc/unrolled-dom-2d-camera.d.ts +2 -0
  18. package/dist-tsc/unrolled-dom-2d-camera.d.ts.map +1 -0
  19. package/dist-tsc/unrolled-dom-2d-camera.js +193 -0
  20. package/dist-tsc/viewport.d.ts +91 -0
  21. package/dist-tsc/viewport.d.ts.map +1 -1
  22. package/dist-tsc/viewport.js +91 -3
  23. package/dist-tsc/viewport.test.d.ts +2 -0
  24. package/dist-tsc/viewport.test.d.ts.map +1 -0
  25. package/dist-tsc/viewport.test.js +184 -0
  26. package/package.json +8 -2
  27. package/src/functional-3d-view-controls.ts +585 -0
  28. package/src/functional-dom-2d-camera.ts +214 -0
  29. package/src/index.ts +2 -2
  30. package/src/lru-store.ts +3 -0
  31. package/src/viewport.test.ts +262 -0
  32. package/src/viewport.ts +91 -3
  33. package/src/3d-view-controls.js +0 -271
  34. package/src/dom-2d-camera.js +0 -441
@@ -0,0 +1,585 @@
1
+ // Functional/stateless adaptation of 3d-view-controls.
2
+ //
3
+ // License copied from https://github.com/mikolalysenko/3d-view-controls/blob/master/LICENSE
4
+ //
5
+ // The MIT License (MIT)
6
+ //
7
+ // Copyright (c) 2013 Mikola Lysenko
8
+ //
9
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ // of this software and associated documentation files (the "Software"), to deal
11
+ // in the Software without restriction, including without limitation the rights
12
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ // copies of the Software, and to permit persons to whom the Software is
14
+ // furnished to do so, subject to the following conditions:
15
+ //
16
+ // The above copyright notice and this permission notice shall be included in
17
+ // all copies or substantial portions of the Software.
18
+ //
19
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25
+ // THE SOFTWARE.
26
+
27
+ import { mat4, vec3, quat } from 'gl-matrix';
28
+ import { ViewportParams } from './viewport.js';
29
+ import { type CameraMatrix } from './functional-dom-2d-camera.js';
30
+
31
+ // Camera settings matching 3d-view-controls.js defaults
32
+ const ROTATE_SPEED = 1.0;
33
+ const ZOOM_SPEED = 1.0;
34
+ const TRANSLATE_SPEED = 1.0;
35
+ const FLIP_X = false;
36
+ const FLIP_Y = false;
37
+
38
+ // Low-level helpers
39
+
40
+ function len3(x: number, y: number, z: number): number {
41
+ return Math.sqrt(x * x + y * y + z * z);
42
+ }
43
+
44
+ // Extract eye position from a column-major view matrix.
45
+ // eye = -R^T * t (R is upper-left 3x3, t is translation column)
46
+ function eyeFromMat(m: Float32Array): vec3 {
47
+ const tx = m[12], ty = m[13], tz = m[14];
48
+ return vec3.fromValues(
49
+ -(m[0] * tx + m[1] * ty + m[2] * tz),
50
+ -(m[4] * tx + m[5] * ty + m[6] * tz),
51
+ -(m[8] * tx + m[9] * ty + m[10] * tz),
52
+ );
53
+ }
54
+
55
+ // Rebuild a lookAt view matrix from eye, center, and up.
56
+ function buildLookAt(eye: vec3, center: vec3, up: vec3): Float32Array {
57
+ const m = mat4.create();
58
+ mat4.lookAt(m, eye, center, up);
59
+ return m as Float32Array;
60
+ }
61
+
62
+ // ============================================================
63
+ // Orbit controller
64
+ // State layout: Float32Array of 20 elements
65
+ // [0-15]: view matrix (column-major)
66
+ // [16-18]: center x, y, z
67
+ // [19]: log(radius)
68
+ // ============================================================
69
+
70
+ function orbitStateFrom(cam: CameraMatrix): [Float32Array, vec3, number] {
71
+ const m = new Float32Array(cam.subarray ? cam.subarray(0, 16) : cam.slice(0, 16));
72
+ const cx = (cam.length > 16 && isFinite(cam[16])) ? cam[16] : 0;
73
+ const cy = (cam.length > 17 && isFinite(cam[17])) ? cam[17] : 0;
74
+ const cz = (cam.length > 18 && isFinite(cam[18])) ? cam[18] : 0;
75
+ const center = vec3.fromValues(cx, cy, cz);
76
+ let logRadius: number;
77
+ if (cam.length > 19 && isFinite(cam[19])) {
78
+ logRadius = cam[19];
79
+ } else {
80
+ const eye = eyeFromMat(m);
81
+ const radius = vec3.distance(eye, center);
82
+ logRadius = Math.log(Math.max(1e-4, radius));
83
+ }
84
+ return [m, center, logRadius];
85
+ }
86
+
87
+ function packOrbit(m: Float32Array, center: vec3, logRadius: number): CameraMatrix {
88
+ const result = new Float32Array(20);
89
+ result.set(m, 0);
90
+ result[16] = center[0];
91
+ result[17] = center[1];
92
+ result[18] = center[2];
93
+ result[19] = logRadius;
94
+ return result as CameraMatrix;
95
+ }
96
+
97
+ function orbitRotate(cam: CameraMatrix, dx: number, dy: number, dz: number): CameraMatrix {
98
+ const [m, center, logRadius] = orbitStateFrom(cam);
99
+
100
+ // Camera frame from view matrix rows (column-major: row i = mat[i], mat[4+i], mat[8+i])
101
+ const rx = m[0], ry = m[4], rz = m[8]; // right
102
+ const ux = m[1], uy = m[5], uz = m[9]; // up
103
+ const fx = m[2], fy = m[6], fz = m[10]; // forward (center → eye direction)
104
+
105
+ // World-space direction from screen deltas
106
+ const qx = dx * rx + dy * ux;
107
+ const qy = dx * ry + dy * uy;
108
+ const qz = dx * rz + dy * uz;
109
+
110
+ // Rotation axis b_axis = -(forward × q)
111
+ let bx = -(fy * qz - fz * qy);
112
+ let by = -(fz * qx - fx * qz);
113
+ let bz = -(fx * qy - fy * qx);
114
+ let bw = Math.sqrt(Math.max(0.0, 1.0 - bx * bx - by * by - bz * bz));
115
+ const bl = Math.sqrt(bx * bx + by * by + bz * bz + bw * bw);
116
+ if (bl > 1e-6) { bx /= bl; by /= bl; bz /= bl; bw /= bl; }
117
+ else { bx = by = bz = 0; bw = 1; }
118
+
119
+ const bq = quat.fromValues(bx, by, bz, bw);
120
+
121
+ // Rotate eye around center
122
+ const eye = eyeFromMat(m);
123
+ const relEye = vec3.fromValues(eye[0] - center[0], eye[1] - center[1], eye[2] - center[2]);
124
+ vec3.transformQuat(relEye, relEye, bq);
125
+ const newEye = vec3.fromValues(center[0] + relEye[0], center[1] + relEye[1], center[2] + relEye[2]);
126
+
127
+ // Rotate up
128
+ const upVec = vec3.fromValues(ux, uy, uz);
129
+ vec3.transformQuat(upVec, upVec, bq);
130
+
131
+ if (dz) {
132
+ const fwd = vec3.fromValues(fx, fy, fz);
133
+ const rollQ = quat.setAxisAngle(quat.create(), fwd, dz);
134
+ vec3.transformQuat(upVec, upVec, rollQ);
135
+ }
136
+
137
+ const newMat = buildLookAt(newEye, center, upVec);
138
+ return packOrbit(newMat, center, logRadius);
139
+ }
140
+
141
+ function orbitPan(cam: CameraMatrix, dx: number, dy: number, dz: number): CameraMatrix {
142
+ const [m, center, logRadius] = orbitStateFrom(cam);
143
+
144
+ // Normalized up
145
+ let ux = m[1], uy = m[5], uz = m[9];
146
+ const ul = len3(ux, uy, uz);
147
+ ux /= ul; uy /= ul; uz /= ul;
148
+
149
+ // Right orthogonalized to up
150
+ let rx = m[0], ry = m[4], rz = m[8];
151
+ const ru = rx * ux + ry * uy + rz * uz;
152
+ rx -= ux * ru; ry -= uy * ru; rz -= uz * ru;
153
+ const rl = len3(rx, ry, rz);
154
+ rx /= rl; ry /= rl; rz /= rl;
155
+
156
+ const newCenter = vec3.fromValues(
157
+ center[0] + rx * dx + ux * dy,
158
+ center[1] + ry * dx + uy * dy,
159
+ center[2] + rz * dx + uz * dy,
160
+ );
161
+
162
+ const radius = Math.exp(logRadius);
163
+ const newLogRadius = Math.log(Math.max(1e-4, radius + dz));
164
+ const newRadius = Math.exp(newLogRadius);
165
+
166
+ // Maintain eye direction (forward = row 2 of view matrix)
167
+ const fx = m[2], fy = m[6], fz = m[10];
168
+ const newEye = vec3.fromValues(
169
+ newCenter[0] + fx * newRadius,
170
+ newCenter[1] + fy * newRadius,
171
+ newCenter[2] + fz * newRadius,
172
+ );
173
+ const upVec = vec3.fromValues(ux, uy, uz);
174
+
175
+ const newMat = buildLookAt(newEye, newCenter, upVec);
176
+ return packOrbit(newMat, newCenter, newLogRadius);
177
+ }
178
+
179
+ // ============================================================
180
+ // Turntable controller
181
+ // State layout: Float32Array of 28 elements
182
+ // [0-15]: view matrix
183
+ // [16-18]: center
184
+ // [19]: log(radius)
185
+ // [20]: theta (azimuth)
186
+ // [21]: phi (elevation)
187
+ // [22-24]: up_base
188
+ // [25-27]: right_base
189
+ // ============================================================
190
+
191
+ // Default turntable base frame: Y-up, X-right, -Z-toward
192
+ const TT_UP_DEFAULT: [number, number, number] = [0, 1, 0];
193
+ const TT_RIGHT_DEFAULT: [number, number, number] = [1, 0, 0];
194
+
195
+ // Derive initial (theta, phi) from a view matrix assuming standard base frame.
196
+ function anglesFromMat(m: Float32Array): { theta: number; phi: number } {
197
+ // forward row (row 2): mat[2], mat[6], mat[10] = outward direction (center→eye)
198
+ // With right_base=(1,0,0), toward_base=(0,0,-1), up_base=(0,1,0):
199
+ // wx = dot(fw, right_base) = fw.x
200
+ // wy = dot(fw, toward_base) = -fw.z
201
+ // wz = dot(fw, up_base) = fw.y
202
+ // wz = sin(phi) → phi = asin(fw.y)
203
+ // wx = cos(phi)*cos(theta), wy = cos(phi)*sin(theta) → theta = atan2(wy, wx) = atan2(-fw.z, fw.x)
204
+ const fy = m[6];
205
+ const phi = Math.asin(Math.max(-1, Math.min(1, fy)));
206
+ const theta = Math.atan2(-m[10], m[2]);
207
+ return { theta, phi };
208
+ }
209
+
210
+ type TurntableState = {
211
+ m: Float32Array;
212
+ center: vec3;
213
+ logRadius: number;
214
+ theta: number;
215
+ phi: number;
216
+ upBase: vec3;
217
+ rightBase: vec3;
218
+ };
219
+
220
+ function turntableStateFrom(cam: CameraMatrix): TurntableState {
221
+ const m = new Float32Array(cam.subarray ? cam.subarray(0, 16) : cam.slice(0, 16));
222
+ const cx = (cam.length > 16 && isFinite(cam[16])) ? cam[16] : 0;
223
+ const cy = (cam.length > 17 && isFinite(cam[17])) ? cam[17] : 0;
224
+ const cz = (cam.length > 18 && isFinite(cam[18])) ? cam[18] : 0;
225
+ const center = vec3.fromValues(cx, cy, cz);
226
+
227
+ let logRadius: number;
228
+ if (cam.length > 19 && isFinite(cam[19])) {
229
+ logRadius = cam[19];
230
+ } else {
231
+ const eye = eyeFromMat(m);
232
+ const radius = vec3.distance(eye, center);
233
+ logRadius = Math.log(Math.max(1e-4, radius));
234
+ }
235
+
236
+ let theta: number, phi: number;
237
+ if (cam.length > 21 && isFinite(cam[20]) && isFinite(cam[21])) {
238
+ theta = cam[20];
239
+ phi = cam[21];
240
+ } else {
241
+ ({ theta, phi } = anglesFromMat(m));
242
+ }
243
+
244
+ const upBase = (cam.length > 24 && isFinite(cam[22]))
245
+ ? vec3.fromValues(cam[22], cam[23], cam[24])
246
+ : vec3.fromValues(...TT_UP_DEFAULT);
247
+ const rightBase = (cam.length > 27 && isFinite(cam[25]))
248
+ ? vec3.fromValues(cam[25], cam[26], cam[27])
249
+ : vec3.fromValues(...TT_RIGHT_DEFAULT);
250
+
251
+ return { m, center, logRadius, theta, phi, upBase, rightBase };
252
+ }
253
+
254
+ function turntableRecalc(
255
+ center: vec3, logRadius: number, theta: number, phi: number, upBase: vec3, rightBase: vec3
256
+ ): { m: Float32Array; upBase: vec3; rightBase: vec3 } {
257
+ // Gram-Schmidt orthonormalize upBase and rightBase
258
+ const up = vec3.clone(upBase);
259
+ const right = vec3.clone(rightBase);
260
+
261
+ const uu = vec3.dot(up, up);
262
+ const ur = vec3.dot(up, right);
263
+ vec3.normalize(up, up);
264
+ // right = right - up * (dot(right, up) / dot(up, up))
265
+ vec3.scaleAndAdd(right, right, up, -ur / uu);
266
+ vec3.normalize(right, right);
267
+
268
+ // toward = up × right
269
+ const toward = vec3.create();
270
+ vec3.cross(toward, up, right);
271
+ vec3.normalize(toward, toward);
272
+
273
+ const radius = Math.exp(logRadius);
274
+ const ctheta = Math.cos(theta), stheta = Math.sin(theta);
275
+ const cphi = Math.cos(phi), sphi = Math.sin(phi);
276
+
277
+ // Outward direction (center → eye) in (right, toward, up) frame
278
+ const wx = ctheta * cphi, wy = stheta * cphi, wz = sphi;
279
+ // Screen-up direction in same frame
280
+ const sx = -ctheta * sphi, sy = -stheta * sphi, sz = cphi;
281
+
282
+ // Fill rows 1 (screen-up) and 2 (forward/outward) of the view matrix.
283
+ // Column-major: mat[4*col + row].
284
+ const mat = new Float32Array(16);
285
+ for (let i = 0; i < 3; ++i) {
286
+ mat[4 * i + 1] = sx * right[i] + sy * toward[i] + sz * up[i]; // row 1
287
+ mat[4 * i + 2] = wx * right[i] + wy * toward[i] + wz * up[i]; // row 2
288
+ mat[4 * i + 3] = 0.0;
289
+ }
290
+
291
+ // Row 0 (right) = cross(row1, row2) normalized
292
+ const a0 = mat[1], a1 = mat[5], a2 = mat[9]; // row 1
293
+ const b0 = mat[2], b1 = mat[6], b2 = mat[10]; // row 2
294
+ let c0 = a1 * b2 - a2 * b1;
295
+ let c1 = a2 * b0 - a0 * b2;
296
+ let c2 = a0 * b1 - a1 * b0;
297
+ const cl = len3(c0, c1, c2);
298
+ c0 /= cl; c1 /= cl; c2 /= cl;
299
+ mat[0] = c0; mat[4] = c1; mat[8] = c2;
300
+
301
+ // Eye = center + forward * radius (forward = row 2 = mat[2], mat[6], mat[10])
302
+ const eyeArr = [
303
+ center[0] + mat[2] * radius,
304
+ center[1] + mat[6] * radius,
305
+ center[2] + mat[10] * radius,
306
+ ];
307
+
308
+ // Translation column: mat[12+i] = -dot(row_i, eye)
309
+ for (let i = 0; i < 3; ++i) {
310
+ let r = 0;
311
+ for (let j = 0; j < 3; ++j) r += mat[i + 4 * j] * eyeArr[j];
312
+ mat[12 + i] = -r;
313
+ }
314
+ mat[15] = 1.0;
315
+
316
+ return { m: mat, upBase: up, rightBase: right };
317
+ }
318
+
319
+ function packTurntable(
320
+ m: Float32Array, center: vec3, logRadius: number,
321
+ theta: number, phi: number, upBase: vec3, rightBase: vec3
322
+ ): CameraMatrix {
323
+ const result = new Float32Array(28);
324
+ result.set(m, 0);
325
+ result[16] = center[0]; result[17] = center[1]; result[18] = center[2];
326
+ result[19] = logRadius;
327
+ result[20] = theta;
328
+ result[21] = phi;
329
+ result[22] = upBase[0]; result[23] = upBase[1]; result[24] = upBase[2];
330
+ result[25] = rightBase[0]; result[26] = rightBase[1]; result[27] = rightBase[2];
331
+ return result as CameraMatrix;
332
+ }
333
+
334
+ function turntableRotate(
335
+ cam: CameraMatrix, dtheta: number, dphi: number, droll: number
336
+ ): CameraMatrix {
337
+ const { center, logRadius, theta: t0, phi: p0, upBase, rightBase } = turntableStateFrom(cam);
338
+
339
+ const theta = t0 + dtheta;
340
+ const phi = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, p0 + dphi));
341
+
342
+ let { m, upBase: newUp, rightBase: newRight } = turntableRecalc(center, logRadius, theta, phi, upBase, rightBase);
343
+
344
+ if (droll) {
345
+ const fwd = vec3.fromValues(m[2], m[6], m[10]);
346
+ const rollQ = quat.setAxisAngle(quat.create(), fwd, droll);
347
+ const rollUp = vec3.clone(newUp);
348
+ const rollRight = vec3.clone(newRight);
349
+ vec3.transformQuat(rollUp, rollUp, rollQ);
350
+ vec3.transformQuat(rollRight, rollRight, rollQ);
351
+ const recalc = turntableRecalc(center, logRadius, theta, phi, rollUp, rollRight);
352
+ m = recalc.m;
353
+ newUp = recalc.upBase;
354
+ newRight = recalc.rightBase;
355
+ }
356
+
357
+ return packTurntable(m, center, logRadius, theta, phi, newUp, newRight);
358
+ }
359
+
360
+ function turntablePan(cam: CameraMatrix, dx: number, dy: number, dz: number): CameraMatrix {
361
+ const { m, center, logRadius, theta, phi, upBase, rightBase } = turntableStateFrom(cam);
362
+
363
+ let ux = m[1], uy = m[5], uz = m[9];
364
+ const ul = len3(ux, uy, uz);
365
+ ux /= ul; uy /= ul; uz /= ul;
366
+
367
+ let rx = m[0], ry = m[4], rz = m[8];
368
+ const ru = rx * ux + ry * uy + rz * uz;
369
+ rx -= ux * ru; ry -= uy * ru; rz -= uz * ru;
370
+ const rl = len3(rx, ry, rz);
371
+ rx /= rl; ry /= rl; rz /= rl;
372
+
373
+ const newCenter = vec3.fromValues(
374
+ center[0] + rx * dx + ux * dy,
375
+ center[1] + ry * dx + uy * dy,
376
+ center[2] + rz * dx + uz * dy,
377
+ );
378
+
379
+ const radius = Math.exp(logRadius);
380
+ const newLogRadius = Math.log(Math.max(1e-4, radius + dz));
381
+
382
+ const { m: newMat, upBase: newUp, rightBase: newRight } = turntableRecalc(
383
+ newCenter, newLogRadius, theta, phi, upBase, rightBase
384
+ );
385
+
386
+ return packTurntable(newMat, newCenter, newLogRadius, theta, phi, newUp, newRight);
387
+ }
388
+
389
+ // Matrix controller
390
+ // State: just the 16-element view matrix
391
+
392
+ function matrixRotate(cam: CameraMatrix, yaw: number, pitch: number, roll: number): CameraMatrix {
393
+ const imat = mat4.create();
394
+ mat4.invert(imat, cam.subarray ? cam.subarray(0, 16) as Float32Array : new Float32Array(cam.slice(0, 16)));
395
+ if (yaw) mat4.rotateY(imat, imat, yaw);
396
+ if (pitch) mat4.rotateX(imat, imat, pitch);
397
+ if (roll) mat4.rotateZ(imat, imat, roll);
398
+ const result = mat4.create();
399
+ mat4.invert(result, imat);
400
+ return result as CameraMatrix;
401
+ }
402
+
403
+ function matrixPan(cam: CameraMatrix, dx: number, dy: number, dz: number): CameraMatrix {
404
+ const imat = mat4.create();
405
+ mat4.invert(imat, cam.subarray ? cam.subarray(0, 16) as Float32Array : new Float32Array(cam.slice(0, 16)));
406
+ mat4.translate(imat, imat, [-dx, -dy, -dz]);
407
+ const result = mat4.create();
408
+ mat4.invert(result, imat);
409
+ return result as CameraMatrix;
410
+ }
411
+
412
+ // Event handler helpers
413
+
414
+ function getWheelDeltas(event: WheelEvent, viewportParams: ViewportParams): { dx: number; dy: number } {
415
+ let dx = event.deltaX || 0;
416
+ let dy = event.deltaY || 0;
417
+ let wheelScale = 1;
418
+ if (event.deltaMode === 1) wheelScale = 20;
419
+ else if (event.deltaMode === 2) wheelScale = viewportParams.height;
420
+ return { dx: dx * wheelScale, dy: dy * wheelScale };
421
+ }
422
+
423
+ // Turntable event handlers
424
+
425
+ export function onWheel3dTurntable(viewportParams: ViewportParams, prevCameraMatrix: CameraMatrix, event: WheelEvent): CameraMatrix {
426
+ event.preventDefault();
427
+ const { dx, dy } = getWheelDeltas(event, viewportParams);
428
+ if (!dx && !dy) return prevCameraMatrix;
429
+
430
+ const flipX = FLIP_X ? 1 : -1;
431
+ const flipY = FLIP_Y ? 1 : -1;
432
+ const { logRadius } = turntableStateFrom(prevCameraMatrix);
433
+ const distance = Math.exp(logRadius);
434
+
435
+ if (Math.abs(dx) > Math.abs(dy)) {
436
+ return turntableRotate(prevCameraMatrix, 0, 0, -dx * flipX * Math.PI * ROTATE_SPEED / viewportParams.width);
437
+ } else {
438
+ const kzoom = ZOOM_SPEED * dy / viewportParams.height * 0.16;
439
+ return turntablePan(prevCameraMatrix, 0, 0, distance * (Math.exp(kzoom) - 1));
440
+ }
441
+ }
442
+
443
+ export function onMouseMove3dTurntable(viewportParams: ViewportParams, prevCameraMatrix: CameraMatrix, event: MouseEvent): CameraMatrix {
444
+ const { height } = viewportParams;
445
+ const scale = 1.0 / height;
446
+ const dx = scale * event.movementX;
447
+ const dy = scale * event.movementY;
448
+ if (!dx && !dy) return prevCameraMatrix;
449
+
450
+ const flipX = FLIP_X ? 1 : -1;
451
+ const flipY = FLIP_Y ? 1 : -1;
452
+ const drot = Math.PI * ROTATE_SPEED;
453
+ const buttons = event.buttons;
454
+
455
+ if (buttons & 1) {
456
+ if (event.shiftKey) {
457
+ return turntableRotate(prevCameraMatrix, 0, 0, -dx * drot);
458
+ } else {
459
+ return turntableRotate(prevCameraMatrix, -flipX * drot * dx, flipY * drot * dy, 0);
460
+ }
461
+ } else if (buttons & 2) {
462
+ const { logRadius } = turntableStateFrom(prevCameraMatrix);
463
+ const distance = Math.exp(logRadius);
464
+ return turntablePan(prevCameraMatrix, TRANSLATE_SPEED * dx * distance, -TRANSLATE_SPEED * dy * distance, 0);
465
+ } else if (buttons & 4) {
466
+ const { logRadius } = turntableStateFrom(prevCameraMatrix);
467
+ const distance = Math.exp(logRadius);
468
+ const kzoom = ZOOM_SPEED * dy / height * 0.32;
469
+ return turntablePan(prevCameraMatrix, 0, 0, distance * (Math.exp(kzoom) - 1));
470
+ }
471
+
472
+ return prevCameraMatrix;
473
+ }
474
+
475
+ // Orbit event handlers
476
+
477
+ export function onWheel3dOrbit(viewportParams: ViewportParams, prevCameraMatrix: CameraMatrix, event: WheelEvent): CameraMatrix {
478
+ event.preventDefault();
479
+ const { dx, dy } = getWheelDeltas(event, viewportParams);
480
+ if (!dx && !dy) return prevCameraMatrix;
481
+
482
+ const flipX = FLIP_X ? 1 : -1;
483
+ const flipY = FLIP_Y ? 1 : -1;
484
+ const [, , logRadius] = orbitStateFrom(prevCameraMatrix);
485
+ const distance = Math.exp(logRadius);
486
+
487
+ if (Math.abs(dx) > Math.abs(dy)) {
488
+ return orbitRotate(prevCameraMatrix, 0, 0, -dx * flipX * Math.PI * ROTATE_SPEED / viewportParams.width);
489
+ } else {
490
+ const kzoom = ZOOM_SPEED * dy / viewportParams.height * 0.16;
491
+ return orbitPan(prevCameraMatrix, 0, 0, distance * (Math.exp(kzoom) - 1));
492
+ }
493
+ }
494
+
495
+ export function onMouseMove3dOrbit(viewportParams: ViewportParams, prevCameraMatrix: CameraMatrix, event: MouseEvent): CameraMatrix {
496
+ const { height } = viewportParams;
497
+ const scale = 1.0 / height;
498
+ const dx = scale * event.movementX;
499
+ const dy = scale * event.movementY;
500
+ if (!dx && !dy) return prevCameraMatrix;
501
+
502
+ const flipX = FLIP_X ? 1 : -1;
503
+ const flipY = FLIP_Y ? 1 : -1;
504
+ const drot = Math.PI * ROTATE_SPEED;
505
+ const buttons = event.buttons;
506
+
507
+ if (buttons & 1) {
508
+ if (event.shiftKey) {
509
+ return orbitRotate(prevCameraMatrix, 0, 0, -dx * drot);
510
+ } else {
511
+ return orbitRotate(prevCameraMatrix, -flipX * drot * dx, flipY * drot * dy, 0);
512
+ }
513
+ } else if (buttons & 2) {
514
+ const [, , logRadius] = orbitStateFrom(prevCameraMatrix);
515
+ const distance = Math.exp(logRadius);
516
+ return orbitPan(prevCameraMatrix, TRANSLATE_SPEED * dx * distance, -TRANSLATE_SPEED * dy * distance, 0);
517
+ } else if (buttons & 4) {
518
+ const [, , logRadius] = orbitStateFrom(prevCameraMatrix);
519
+ const distance = Math.exp(logRadius);
520
+ const kzoom = ZOOM_SPEED * dy / height * 0.32;
521
+ return orbitPan(prevCameraMatrix, 0, 0, distance * (Math.exp(kzoom) - 1));
522
+ }
523
+
524
+ return prevCameraMatrix;
525
+ }
526
+
527
+ // Matrix event handlers
528
+
529
+ export function onWheel3dMatrix(viewportParams: ViewportParams, prevCameraMatrix: CameraMatrix, event: WheelEvent): CameraMatrix {
530
+ event.preventDefault();
531
+ const { dx, dy } = getWheelDeltas(event, viewportParams);
532
+ if (!dx && !dy) return prevCameraMatrix;
533
+
534
+ const flipX = FLIP_X ? 1 : -1;
535
+ const flipY = FLIP_Y ? 1 : -1;
536
+ const eye = eyeFromMat(prevCameraMatrix as Float32Array);
537
+ const distance = vec3.length(eye);
538
+
539
+ if (Math.abs(dx) > Math.abs(dy)) {
540
+ return matrixRotate(prevCameraMatrix, 0, 0, -dx * flipX * Math.PI * ROTATE_SPEED / viewportParams.width);
541
+ } else {
542
+ const kzoom = ZOOM_SPEED * dy / viewportParams.height * 0.16;
543
+ return matrixPan(prevCameraMatrix, 0, 0, distance * (Math.exp(kzoom) - 1));
544
+ }
545
+ }
546
+
547
+ export function onMouseMove3dMatrix(viewportParams: ViewportParams, prevCameraMatrix: CameraMatrix, event: MouseEvent): CameraMatrix {
548
+ const { height } = viewportParams;
549
+ const scale = 1.0 / height;
550
+ const dx = scale * event.movementX;
551
+ const dy = scale * event.movementY;
552
+ if (!dx && !dy) return prevCameraMatrix;
553
+
554
+ const flipX = FLIP_X ? 1 : -1;
555
+ const flipY = FLIP_Y ? 1 : -1;
556
+ const drot = Math.PI * ROTATE_SPEED;
557
+ const buttons = event.buttons;
558
+ const eye = eyeFromMat(prevCameraMatrix as Float32Array);
559
+ const distance = vec3.length(eye);
560
+
561
+ if (buttons & 1) {
562
+ if (event.shiftKey) {
563
+ return matrixRotate(prevCameraMatrix, 0, 0, -dx * drot);
564
+ } else {
565
+ return matrixRotate(prevCameraMatrix, -flipX * drot * dx, flipY * drot * dy, 0);
566
+ }
567
+ } else if (buttons & 2) {
568
+ return matrixPan(prevCameraMatrix, TRANSLATE_SPEED * dx * distance, -TRANSLATE_SPEED * dy * distance, 0);
569
+ } else if (buttons & 4) {
570
+ const kzoom = ZOOM_SPEED * dy / height * 0.32;
571
+ return matrixPan(prevCameraMatrix, 0, 0, distance * (Math.exp(kzoom) - 1));
572
+ }
573
+
574
+ return prevCameraMatrix;
575
+ }
576
+
577
+ // Default 3D handlers (orbit controller)
578
+
579
+ export function onWheel(viewportParams: ViewportParams, prevCameraMatrix: CameraMatrix, event: WheelEvent): CameraMatrix {
580
+ return onWheel3dOrbit(viewportParams, prevCameraMatrix, event);
581
+ }
582
+
583
+ export function onMouseMove(viewportParams: ViewportParams, prevCameraMatrix: CameraMatrix, event: MouseEvent): CameraMatrix {
584
+ return onMouseMove3dOrbit(viewportParams, prevCameraMatrix, event);
585
+ }