@expofp/renderer 1.2.1 → 1.3.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/dist/index.d.ts +385 -138
- package/dist/index.js +747 -237
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
2
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
3
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
-
import { DataTexture, FloatType, UnsignedIntType, IntType, RGBAFormat, RGBAIntegerFormat, RGFormat, RGIntegerFormat, RedFormat, RedIntegerFormat, BatchedMesh as BatchedMesh$1, BufferAttribute, StreamDrawUsage, Color, Matrix4, Vector3, Vector4, DoubleSide, MeshBasicMaterial, Texture, Quaternion, Group, PlaneGeometry, SRGBColorSpace, Vector2, BufferGeometry, LinearSRGBColorSpace, Mesh, Raycaster, Sphere, Box3, Spherical, PerspectiveCamera, Scene,
|
|
4
|
+
import { DataTexture, FloatType, UnsignedIntType, IntType, RGBAFormat, RGBAIntegerFormat, RGFormat, RGIntegerFormat, RedFormat, RedIntegerFormat, BatchedMesh as BatchedMesh$1, BufferAttribute, StreamDrawUsage, Color, Matrix4, Vector3, Vector4, DoubleSide, MeshBasicMaterial, Texture, Quaternion, Group, PlaneGeometry, SRGBColorSpace, Vector2, BufferGeometry, LinearSRGBColorSpace, Mesh, Plane, Raycaster, Sphere, Box3, Spherical, PerspectiveCamera, Scene, Camera, MathUtils, Clock, WebGLRenderer } from "three";
|
|
5
5
|
import { traverseAncestorsGenerator } from "three/examples/jsm/utils/SceneUtils.js";
|
|
6
6
|
import { BatchedText as BatchedText$1, Text as Text$1 } from "troika-three-text";
|
|
7
7
|
import { LineMaterial, LineSegmentsGeometry } from "three/examples/jsm/Addons.js";
|
|
8
8
|
import { MaxRectsPacker, Rectangle } from "maxrects-packer";
|
|
9
9
|
import { converter, parse } from "culori";
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
10
|
+
import { RAD2DEG, DEG2RAD, MathUtils as MathUtils$1 } from "three/src/math/MathUtils.js";
|
|
11
|
+
import CameraControls from "camera-controls";
|
|
12
|
+
import { EventManager, Rotate, Pan } from "mjolnir.js";
|
|
13
13
|
function isObject(item) {
|
|
14
14
|
return !!item && typeof item === "object" && !Array.isArray(item);
|
|
15
15
|
}
|
|
@@ -2190,18 +2190,43 @@ const subsetOfTHREE = {
|
|
|
2190
2190
|
Spherical,
|
|
2191
2191
|
Box3,
|
|
2192
2192
|
Sphere,
|
|
2193
|
-
Raycaster
|
|
2193
|
+
Raycaster,
|
|
2194
|
+
Plane
|
|
2194
2195
|
};
|
|
2195
|
-
|
|
2196
|
+
CameraControls.install({ THREE: subsetOfTHREE });
|
|
2197
|
+
class CameraController extends CameraControls {
|
|
2198
|
+
/**
|
|
2199
|
+
* @param camera {@link PerspectiveCamera} instance
|
|
2200
|
+
* @param renderer {@link Renderer} instance
|
|
2201
|
+
*/
|
|
2202
|
+
constructor(camera, renderer) {
|
|
2203
|
+
super(camera);
|
|
2204
|
+
this.renderer = renderer;
|
|
2205
|
+
}
|
|
2206
|
+
update(delta) {
|
|
2207
|
+
var _a;
|
|
2208
|
+
const needsUpdate = super.update(delta);
|
|
2209
|
+
if (needsUpdate && ((_a = this.renderer) == null ? void 0 : _a.debugLog)) {
|
|
2210
|
+
const position = this.camera.position.toArray().map((value) => value.toFixed(2)).join(", ");
|
|
2211
|
+
const target = this._target.toArray().map((value) => value.toFixed(2)).join(", ");
|
|
2212
|
+
const spherical = [this._spherical.theta * RAD2DEG, this._spherical.phi * RAD2DEG, this._spherical.radius].map((value) => value.toFixed(2)).join(", ");
|
|
2213
|
+
console.log(`position: [${position}]
|
|
2214
|
+
target: [${target}]
|
|
2215
|
+
spherical: [${spherical}]`);
|
|
2216
|
+
}
|
|
2217
|
+
return needsUpdate;
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2196
2220
|
class CameraSystem {
|
|
2197
2221
|
/**
|
|
2198
2222
|
* @param renderer {@link Renderer} instance
|
|
2199
2223
|
*/
|
|
2200
2224
|
constructor(renderer) {
|
|
2225
|
+
/** External camera instance. Used to render the scene in external mode (e.g. Mapbox GL JS). */
|
|
2226
|
+
__publicField(this, "externalCamera");
|
|
2201
2227
|
/** {@link CameraController} instance. Used to smoothly animate the camera. */
|
|
2202
2228
|
__publicField(this, "controller");
|
|
2203
2229
|
__publicField(this, "camera");
|
|
2204
|
-
__publicField(this, "externalCamera");
|
|
2205
2230
|
__publicField(this, "zoomIdentityDistance");
|
|
2206
2231
|
__publicField(this, "zoomBounds");
|
|
2207
2232
|
this.renderer = renderer;
|
|
@@ -2210,7 +2235,7 @@ class CameraSystem {
|
|
|
2210
2235
|
this.camera.up.set(0, 0, -1);
|
|
2211
2236
|
this.zoomIdentityDistance = h / 2;
|
|
2212
2237
|
this.camera.position.y = this.zoomIdentityDistance;
|
|
2213
|
-
this.controller = new CameraController(this.camera);
|
|
2238
|
+
this.controller = new CameraController(this.camera, renderer);
|
|
2214
2239
|
void this.controller.rotatePolarTo(0);
|
|
2215
2240
|
}
|
|
2216
2241
|
/** Current camera instance. */
|
|
@@ -2219,7 +2244,16 @@ class CameraSystem {
|
|
|
2219
2244
|
}
|
|
2220
2245
|
/** Current camera zoom factor. */
|
|
2221
2246
|
get zoomFactor() {
|
|
2222
|
-
|
|
2247
|
+
const distance = this.controller.distance;
|
|
2248
|
+
return distance ? this.zoomIdentityDistance / distance : 1;
|
|
2249
|
+
}
|
|
2250
|
+
/**
|
|
2251
|
+
* Calculates the camera distance from the scene's plane for a given zoom factor.
|
|
2252
|
+
* @param zoomFactor Zoom factor
|
|
2253
|
+
* @returns Corresponding camera distance on the Z axis
|
|
2254
|
+
*/
|
|
2255
|
+
zoomFactorToDistance(zoomFactor) {
|
|
2256
|
+
return zoomFactor > 0 ? this.zoomIdentityDistance / zoomFactor : this.zoomIdentityDistance;
|
|
2223
2257
|
}
|
|
2224
2258
|
/**
|
|
2225
2259
|
* Initializes the camera with the given zoom bounds.
|
|
@@ -2233,35 +2267,41 @@ class CameraSystem {
|
|
|
2233
2267
|
updateCamera() {
|
|
2234
2268
|
if (!this.zoomBounds) return;
|
|
2235
2269
|
const [w, h] = this.renderer.size;
|
|
2236
|
-
|
|
2270
|
+
const zoomFactor = this.zoomFactor;
|
|
2271
|
+
this.zoomIdentityDistance = h / 2;
|
|
2237
2272
|
const maxDistance = Math.abs(this.zoomIdentityDistance / this.zoomBounds[0]);
|
|
2238
2273
|
const minDistance = Math.abs(this.zoomIdentityDistance / this.zoomBounds[1]);
|
|
2239
2274
|
this.camera.aspect = w / (h || 1);
|
|
2240
|
-
this.
|
|
2241
|
-
this.camera.near = minDistance - 1;
|
|
2275
|
+
this.computeCameraClipPlanes(minDistance, maxDistance);
|
|
2242
2276
|
this.camera.updateProjectionMatrix();
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2277
|
+
this.syncController(minDistance, maxDistance, zoomFactor);
|
|
2278
|
+
}
|
|
2279
|
+
computeCameraClipPlanes(minDistance, maxDistance, nearSafetyFactor = 0.5, farSafetyFactor = 1.5) {
|
|
2280
|
+
const fov = this.camera.fov * DEG2RAD;
|
|
2281
|
+
const aspect = this.camera.aspect;
|
|
2282
|
+
const maxPolarAngle = 85 * DEG2RAD;
|
|
2283
|
+
const halfFovY = fov / 2;
|
|
2284
|
+
const halfFovX = Math.atan(Math.tan(halfFovY) * aspect);
|
|
2285
|
+
const diagonalFov = 2 * Math.atan(Math.sqrt(Math.tan(halfFovX) ** 2 + Math.tan(halfFovY) ** 2));
|
|
2286
|
+
const minHeight = minDistance * Math.cos(maxPolarAngle);
|
|
2287
|
+
const near = Math.max(0.1, minHeight * nearSafetyFactor);
|
|
2288
|
+
const criticalHeight = minHeight;
|
|
2289
|
+
const horizontalDistToOrbit = minDistance * Math.sin(maxPolarAngle);
|
|
2290
|
+
const distToOrbit = minDistance;
|
|
2291
|
+
const visibleRadiusAtOrbit = distToOrbit * Math.tan(diagonalFov / 2);
|
|
2292
|
+
const planeExtent = Math.max(maxDistance, visibleRadiusAtOrbit);
|
|
2293
|
+
const horizontalDistToFarEdge = horizontalDistToOrbit + planeExtent;
|
|
2294
|
+
const maxViewDistance = Math.sqrt(criticalHeight ** 2 + horizontalDistToFarEdge ** 2);
|
|
2295
|
+
const far = maxViewDistance * farSafetyFactor;
|
|
2296
|
+
this.camera.near = near;
|
|
2297
|
+
this.camera.far = far;
|
|
2298
|
+
if (this.renderer.debugLog) console.log("camera clip planes", near, far);
|
|
2251
2299
|
}
|
|
2252
2300
|
syncController(minDistance, maxDistance, zoomFactor) {
|
|
2253
2301
|
if (this.renderer.debugLog) console.log("syncController", minDistance, maxDistance, zoomFactor);
|
|
2254
2302
|
this.controller.minDistance = minDistance;
|
|
2255
2303
|
this.controller.maxDistance = maxDistance;
|
|
2256
|
-
void this.controller.
|
|
2257
|
-
this.camera.position.x,
|
|
2258
|
-
this.camera.position.y,
|
|
2259
|
-
-this.zoomFactorToDistance(zoomFactor),
|
|
2260
|
-
this.camera.position.x,
|
|
2261
|
-
this.camera.position.y,
|
|
2262
|
-
0,
|
|
2263
|
-
false
|
|
2264
|
-
);
|
|
2304
|
+
void this.controller.dollyTo(this.zoomFactorToDistance(zoomFactor), false);
|
|
2265
2305
|
}
|
|
2266
2306
|
}
|
|
2267
2307
|
class SceneSystem {
|
|
@@ -2277,12 +2317,24 @@ class SceneSystem {
|
|
|
2277
2317
|
__publicField(this, "inverseWorldMatrix", new Matrix4());
|
|
2278
2318
|
__publicField(this, "translationMatrix", new Matrix4());
|
|
2279
2319
|
__publicField(this, "scaleMatrix", new Matrix4());
|
|
2320
|
+
__publicField(this, "scaleVector", new Vector3());
|
|
2280
2321
|
__publicField(this, "visibleRectOffsetMatrix", new Matrix4());
|
|
2281
2322
|
__publicField(this, "viewbox");
|
|
2282
2323
|
this.renderer = renderer;
|
|
2283
2324
|
this.scene = new Scene();
|
|
2284
2325
|
this.scene.matrixAutoUpdate = false;
|
|
2285
2326
|
}
|
|
2327
|
+
/** Scene scale factor (SVG to pixel) */
|
|
2328
|
+
get scaleFactor() {
|
|
2329
|
+
this.scaleVector.setFromMatrixScale(this.scene.matrix);
|
|
2330
|
+
if (this.scaleVector.z === 1) {
|
|
2331
|
+
return this.scaleVector.x;
|
|
2332
|
+
} else {
|
|
2333
|
+
const perspectiveW = this.scene.matrix.elements[15];
|
|
2334
|
+
const halfViewportWidth = this.renderer.size[0] / 2;
|
|
2335
|
+
return halfViewportWidth * this.scaleVector.x / perspectiveW;
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2286
2338
|
/**
|
|
2287
2339
|
* Initializes the scene with the given SVG viewbox.
|
|
2288
2340
|
* @param viewbox {@link Rect} viewbox
|
|
@@ -2333,6 +2385,7 @@ class ViewportSystem {
|
|
|
2333
2385
|
__publicField(this, "viewboxPlane", new Plane(new Vector3(0, 0, 1), 0));
|
|
2334
2386
|
__publicField(this, "pxToSvgScaleThreshold", 1e-3);
|
|
2335
2387
|
__publicField(this, "prevPxToSvgScale");
|
|
2388
|
+
__publicField(this, "externalStaticTransformMatrix", new Matrix4());
|
|
2336
2389
|
this.renderer = renderer;
|
|
2337
2390
|
this.eventSystem = eventSystem;
|
|
2338
2391
|
this.sceneSystem = new SceneSystem(renderer);
|
|
@@ -2351,6 +2404,14 @@ class ViewportSystem {
|
|
|
2351
2404
|
get cameraController() {
|
|
2352
2405
|
return this.cameraSystem.controller;
|
|
2353
2406
|
}
|
|
2407
|
+
/** Current camera zoom factor. */
|
|
2408
|
+
get zoomFactor() {
|
|
2409
|
+
return this.cameraSystem.zoomFactor;
|
|
2410
|
+
}
|
|
2411
|
+
/** Scene scale factor (SVG to pixel) */
|
|
2412
|
+
get scaleFactor() {
|
|
2413
|
+
return this.sceneSystem.scaleFactor;
|
|
2414
|
+
}
|
|
2354
2415
|
/**
|
|
2355
2416
|
* Initializes the viewport and zoom bounds with the given scene definition.
|
|
2356
2417
|
* @param sceneDef {@link SceneDef} scene definition
|
|
@@ -2358,6 +2419,13 @@ class ViewportSystem {
|
|
|
2358
2419
|
initViewport(sceneDef) {
|
|
2359
2420
|
this.sceneSystem.initScene(sceneDef.viewbox);
|
|
2360
2421
|
this.cameraSystem.initCamera([0.1, sceneDef.viewbox.size.width > 1e5 ? 100 : 35]);
|
|
2422
|
+
if (sceneDef.bounds) {
|
|
2423
|
+
const boundary = new Box3(
|
|
2424
|
+
new Vector3(sceneDef.bounds.min.x, sceneDef.bounds.min.y, 0),
|
|
2425
|
+
new Vector3(sceneDef.bounds.max.x, sceneDef.bounds.max.y, 0)
|
|
2426
|
+
).applyMatrix4(this.sceneSystem.worldMatrix);
|
|
2427
|
+
this.cameraController.setBoundary(boundary);
|
|
2428
|
+
}
|
|
2361
2429
|
}
|
|
2362
2430
|
/** Updates the viewport when the renderer size changes. */
|
|
2363
2431
|
updateViewport() {
|
|
@@ -2368,17 +2436,7 @@ class ViewportSystem {
|
|
|
2368
2436
|
* Recalculates the svg to pixel scale factor and emits the event if necessary.
|
|
2369
2437
|
*/
|
|
2370
2438
|
updatePtScale() {
|
|
2371
|
-
const
|
|
2372
|
-
scaleVector.setFromMatrixScale(this.scene.matrix);
|
|
2373
|
-
let scaleFactor;
|
|
2374
|
-
if (scaleVector.z === 1) {
|
|
2375
|
-
scaleFactor = scaleVector.x;
|
|
2376
|
-
} else {
|
|
2377
|
-
const perspectiveW = this.scene.matrix.elements[15];
|
|
2378
|
-
const halfViewportWidth = this.renderer.size[0] / 2;
|
|
2379
|
-
scaleFactor = halfViewportWidth * scaleVector.x / perspectiveW;
|
|
2380
|
-
}
|
|
2381
|
-
const denominator = scaleFactor * this.cameraSystem.zoomFactor;
|
|
2439
|
+
const denominator = this.sceneSystem.scaleFactor * this.cameraSystem.zoomFactor;
|
|
2382
2440
|
const pxToSvgScale = 1 / denominator;
|
|
2383
2441
|
if (Math.abs(pxToSvgScale - (this.prevPxToSvgScale ?? 0)) < this.pxToSvgScaleThreshold) return;
|
|
2384
2442
|
if (this.renderer.debugLog) console.log("pxToSvgScale", +pxToSvgScale.toFixed(3));
|
|
@@ -2396,6 +2454,16 @@ class ViewportSystem {
|
|
|
2396
2454
|
const intersections = this.raycaster.intersectObject(scene, true).filter((i) => isVisible(i.object));
|
|
2397
2455
|
return intersections;
|
|
2398
2456
|
}
|
|
2457
|
+
/**
|
|
2458
|
+
* Converts a point from SVG coordinates to world coordinates.
|
|
2459
|
+
* @param svgCoords Point in SVG coordinates
|
|
2460
|
+
* @returns Point in world coordinates
|
|
2461
|
+
*/
|
|
2462
|
+
svgToWorld(svgCoords) {
|
|
2463
|
+
const svg3D = new Vector3(...svgCoords, 0);
|
|
2464
|
+
svg3D.applyMatrix4(this.sceneSystem.worldMatrix);
|
|
2465
|
+
return new Vector2(svg3D.x, svg3D.y);
|
|
2466
|
+
}
|
|
2399
2467
|
/**
|
|
2400
2468
|
* Converts a point from screen coordinates to the given coordinate space.
|
|
2401
2469
|
* @param space Space to convert to (either "svg" or "world")
|
|
@@ -2408,6 +2476,219 @@ class ViewportSystem {
|
|
|
2408
2476
|
if (space === "svg") this.intersectionPoint.applyMatrix4(this.sceneSystem.inverseWorldMatrix);
|
|
2409
2477
|
return { x: this.intersectionPoint.x, y: this.intersectionPoint.y };
|
|
2410
2478
|
}
|
|
2479
|
+
/**
|
|
2480
|
+
* Calculates the camera distance from the scene's plane for a given zoom factor.
|
|
2481
|
+
* @param zoomFactor Zoom factor
|
|
2482
|
+
* @returns Corresponding camera distance on the Z axis
|
|
2483
|
+
*/
|
|
2484
|
+
zoomFactorToDistance(zoomFactor) {
|
|
2485
|
+
return this.cameraSystem.zoomFactorToDistance(zoomFactor);
|
|
2486
|
+
}
|
|
2487
|
+
/**
|
|
2488
|
+
* Sets the external transform matrix.
|
|
2489
|
+
* @param staticTransformMatrix static transform matrix to apply to the scene
|
|
2490
|
+
*/
|
|
2491
|
+
setExternalTransform(staticTransformMatrix) {
|
|
2492
|
+
this.cameraSystem.externalCamera = new Camera();
|
|
2493
|
+
this.externalStaticTransformMatrix.fromArray(staticTransformMatrix);
|
|
2494
|
+
}
|
|
2495
|
+
/**
|
|
2496
|
+
* Updates the external camera.
|
|
2497
|
+
* @param dynamicTransformMatrix dynamic transform matrix to apply to the scene
|
|
2498
|
+
*/
|
|
2499
|
+
updateExternalCamera(dynamicTransformMatrix) {
|
|
2500
|
+
this.scene.matrix.fromArray(dynamicTransformMatrix).multiply(this.externalStaticTransformMatrix);
|
|
2501
|
+
this.scene.matrixWorldNeedsUpdate = true;
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
class ControlsSystem {
|
|
2505
|
+
/**
|
|
2506
|
+
* @param renderer {@link Renderer} instance
|
|
2507
|
+
* @param viewportSystem {@link ViewportSystem} instance
|
|
2508
|
+
* @param interactionsSystem {@link InteractionsSystem} instance
|
|
2509
|
+
*/
|
|
2510
|
+
constructor(renderer, viewportSystem, interactionsSystem) {
|
|
2511
|
+
__publicField(this, "controller");
|
|
2512
|
+
this.renderer = renderer;
|
|
2513
|
+
this.viewportSystem = viewportSystem;
|
|
2514
|
+
this.interactionsSystem = interactionsSystem;
|
|
2515
|
+
this.controller = viewportSystem.cameraController;
|
|
2516
|
+
}
|
|
2517
|
+
/** Gesture handlers for camera controls. */
|
|
2518
|
+
get handlers() {
|
|
2519
|
+
return this.interactionsSystem.handlers;
|
|
2520
|
+
}
|
|
2521
|
+
/**
|
|
2522
|
+
* Zooms the camera by the given factor.
|
|
2523
|
+
* @param zoom Zoom factor to apply (e.g., 1.5 for 50% zoom in, 0.5 for 50% zoom out)
|
|
2524
|
+
* @param immediate If true, applies the change immediately without animation
|
|
2525
|
+
* @returns Promise that resolves when the zoom animation completes
|
|
2526
|
+
*/
|
|
2527
|
+
zoomBy(zoom, immediate) {
|
|
2528
|
+
const newZoomFactor = this.viewportSystem.zoomFactor * zoom;
|
|
2529
|
+
const newDistance = this.viewportSystem.zoomFactorToDistance(newZoomFactor);
|
|
2530
|
+
return this.controller.dollyTo(newDistance, !immediate);
|
|
2531
|
+
}
|
|
2532
|
+
/**
|
|
2533
|
+
* Zooms the camera to fit the given rectangle.
|
|
2534
|
+
* @param rect Rectangle to zoom to in SVG coordinates
|
|
2535
|
+
* @param opts Optional zoom configuration
|
|
2536
|
+
* @param immediate If true, applies the change immediately without animation
|
|
2537
|
+
* @returns Promise that resolves when the zoom animation completes
|
|
2538
|
+
*/
|
|
2539
|
+
async zoomTo(rect, opts = {}, immediate) {
|
|
2540
|
+
const { maxZoom, paddingPercent } = opts;
|
|
2541
|
+
const dpr = this.renderer.context.getPixelRatio();
|
|
2542
|
+
const visibleRect = this.renderer.visibleRect;
|
|
2543
|
+
const sourceRect = new Rect(this.viewportSystem.svgToWorld(rect.min), this.viewportSystem.svgToWorld(rect.max));
|
|
2544
|
+
const targetRect = visibleRect ? new Rect(visibleRect.min.clone().multiplyScalar(dpr), visibleRect.max.clone().multiplyScalar(dpr)) : new Rect([0, 0], this.renderer.size);
|
|
2545
|
+
if (paddingPercent) targetRect.addPadding(targetRect.size.x * paddingPercent, targetRect.size.y * paddingPercent);
|
|
2546
|
+
const zoomByWidth = targetRect.size.x / sourceRect.size.x;
|
|
2547
|
+
const zoomByHeight = targetRect.size.y / sourceRect.size.y;
|
|
2548
|
+
const minZoom = Math.min(zoomByWidth, zoomByHeight);
|
|
2549
|
+
const zoom = maxZoom ? Math.min(minZoom, maxZoom) : minZoom;
|
|
2550
|
+
const translate = sourceRect.center;
|
|
2551
|
+
if (visibleRect) {
|
|
2552
|
+
const offset = new Vector2(...this.renderer.size).multiplyScalar(0.5).sub(targetRect.center).multiplyScalar(1 / zoom);
|
|
2553
|
+
translate.add(offset);
|
|
2554
|
+
}
|
|
2555
|
+
const enableTransition = !immediate;
|
|
2556
|
+
await Promise.all([
|
|
2557
|
+
this.controller.moveTo(translate.x, translate.y, 0, enableTransition),
|
|
2558
|
+
this.controller.dollyTo(this.viewportSystem.zoomFactorToDistance(zoom), enableTransition)
|
|
2559
|
+
]);
|
|
2560
|
+
}
|
|
2561
|
+
/**
|
|
2562
|
+
* Pans the camera by the given offset in SVG space.
|
|
2563
|
+
* @param x X offset in SVG space
|
|
2564
|
+
* @param y Y offset in SVG space
|
|
2565
|
+
* @param immediate If true, applies the change immediately without animation
|
|
2566
|
+
* @returns Promise that resolves when the pan animation completes
|
|
2567
|
+
*/
|
|
2568
|
+
panBy(x, y, immediate) {
|
|
2569
|
+
const svgOrigin = this.viewportSystem.svgToWorld(new Vector2(0, 0));
|
|
2570
|
+
const svgOffset = this.viewportSystem.svgToWorld(new Vector2(x, y));
|
|
2571
|
+
const worldOffset = new Vector3(svgOffset.x - svgOrigin.x, svgOffset.y - svgOrigin.y, 0);
|
|
2572
|
+
const currentTarget = this.controller.getTarget(new Vector3());
|
|
2573
|
+
const newTarget = currentTarget.add(worldOffset);
|
|
2574
|
+
return this.controller.moveTo(newTarget.x, newTarget.y, newTarget.z, !immediate);
|
|
2575
|
+
}
|
|
2576
|
+
/**
|
|
2577
|
+
* Pans the camera to the given coordinates in SVG space.
|
|
2578
|
+
* @param x X coordinate in SVG space
|
|
2579
|
+
* @param y Y coordinate in SVG space
|
|
2580
|
+
* @param immediate If true, applies the change immediately without animation
|
|
2581
|
+
* @returns Promise that resolves when the pan animation completes
|
|
2582
|
+
*/
|
|
2583
|
+
panTo(x, y, immediate) {
|
|
2584
|
+
const worldCoords = this.viewportSystem.svgToWorld(new Vector2(x, y));
|
|
2585
|
+
return this.controller.moveTo(worldCoords.x, worldCoords.y, 0, !immediate);
|
|
2586
|
+
}
|
|
2587
|
+
/**
|
|
2588
|
+
* Rolls the camera by the given angle in degrees.
|
|
2589
|
+
* Positive angle rolls clockwise, negative angle rolls counterclockwise.
|
|
2590
|
+
* @param angle Angle in degrees to roll by
|
|
2591
|
+
* @param immediate If true, applies the change immediately without animation
|
|
2592
|
+
* @returns Promise that resolves when the roll animation completes
|
|
2593
|
+
*/
|
|
2594
|
+
rollBy(angle, immediate) {
|
|
2595
|
+
const angleRad = -angle * DEG2RAD;
|
|
2596
|
+
const spherical = this.controller.getSpherical(new Spherical());
|
|
2597
|
+
const azimuthAngle = spherical.theta;
|
|
2598
|
+
const newAzimuthAngle = azimuthAngle + angleRad;
|
|
2599
|
+
return this.controller.rotateAzimuthTo(newAzimuthAngle, !immediate);
|
|
2600
|
+
}
|
|
2601
|
+
/**
|
|
2602
|
+
* Rolls the camera to the given angle in degrees.
|
|
2603
|
+
* Positive angles go clockwise.
|
|
2604
|
+
* @param angle Target angle in degrees
|
|
2605
|
+
* @param immediate If true, applies the change immediately without animation
|
|
2606
|
+
* @returns Promise that resolves when the roll animation completes
|
|
2607
|
+
*/
|
|
2608
|
+
rollTo(angle, immediate) {
|
|
2609
|
+
const targetAngleRad = -angle * DEG2RAD;
|
|
2610
|
+
const spherical = this.controller.getSpherical(new Spherical());
|
|
2611
|
+
const azimuthAngle = spherical.theta;
|
|
2612
|
+
const deltaAngleRad = shortestRotationAngle(targetAngleRad, azimuthAngle);
|
|
2613
|
+
console.log("rollTo", deltaAngleRad * RAD2DEG, targetAngleRad * RAD2DEG, azimuthAngle * RAD2DEG);
|
|
2614
|
+
return this.rollBy(-deltaAngleRad * RAD2DEG, immediate);
|
|
2615
|
+
}
|
|
2616
|
+
/**
|
|
2617
|
+
* Pitches the camera by the given angle in degrees.
|
|
2618
|
+
* Positive angles increase pitch, negative angles decrease pitch.
|
|
2619
|
+
* Note: This method will not pitch the camera outside the pitch range set by the `handlers.pitch.configure()` method.
|
|
2620
|
+
* @param angle Angle in degrees to pitch by
|
|
2621
|
+
* @param immediate If true, applies the change immediately without animation
|
|
2622
|
+
* @returns Promise that resolves when the pitch animation completes
|
|
2623
|
+
*/
|
|
2624
|
+
pitchBy(angle, immediate) {
|
|
2625
|
+
const angleRad = angle * DEG2RAD;
|
|
2626
|
+
const spherical = this.controller.getSpherical(new Spherical());
|
|
2627
|
+
const polarAngle = spherical.phi;
|
|
2628
|
+
const newPolarAngle = polarAngle + angleRad;
|
|
2629
|
+
return this.controller.rotatePolarTo(newPolarAngle, !immediate);
|
|
2630
|
+
}
|
|
2631
|
+
/**
|
|
2632
|
+
* Pitches the camera to the given angle in degrees.
|
|
2633
|
+
* The angle is clamped to the 0-90 degree range.
|
|
2634
|
+
* Note: This method will not pitch the camera outside the pitch range set by the `handlers.pitch.configure()` method.
|
|
2635
|
+
* @param angle Target angle in degrees
|
|
2636
|
+
* @param immediate If true, applies the change immediately without animation
|
|
2637
|
+
* @returns Promise that resolves when the pitch animation completes
|
|
2638
|
+
*/
|
|
2639
|
+
pitchTo(angle, immediate) {
|
|
2640
|
+
const angleRad = angle * DEG2RAD;
|
|
2641
|
+
const clampedAngleRad = MathUtils.euclideanModulo(angleRad, Math.PI / 2);
|
|
2642
|
+
return this.controller.rotatePolarTo(clampedAngleRad, !immediate);
|
|
2643
|
+
}
|
|
2644
|
+
/**
|
|
2645
|
+
* Resets the camera to the starting state by resetting enabled handlers.
|
|
2646
|
+
* @param options Configuration for which handlers to reset (defaults to all handlers)
|
|
2647
|
+
* @param immediate If true, applies the change immediately without animation
|
|
2648
|
+
* @returns Promise that resolves when all reset animations complete
|
|
2649
|
+
*/
|
|
2650
|
+
async resetCamera(options = { roll: true, zoom: true, pan: true, pitch: true }, immediate) {
|
|
2651
|
+
const resetPromises = [];
|
|
2652
|
+
const enableTransition = !immediate;
|
|
2653
|
+
for (const [key, handler] of Object.entries(this.handlers)) {
|
|
2654
|
+
if (options[key] && handler.isEnabled()) {
|
|
2655
|
+
resetPromises.push(handler.reset(enableTransition));
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
await Promise.all(resetPromises);
|
|
2659
|
+
}
|
|
2660
|
+
/**
|
|
2661
|
+
* Configures smooth time values for camera operations.
|
|
2662
|
+
* All time values are in seconds.
|
|
2663
|
+
* @param options Partial configuration object with time values in seconds
|
|
2664
|
+
*/
|
|
2665
|
+
configure(options) {
|
|
2666
|
+
const controller = this.controller;
|
|
2667
|
+
if (options.zoomTime !== void 0) controller.dollyTime = options.zoomTime;
|
|
2668
|
+
if (options.panTime !== void 0) controller.truckTime = options.panTime;
|
|
2669
|
+
if (options.rollTime !== void 0) controller.azimuthTime = options.rollTime;
|
|
2670
|
+
if (options.pitchTime !== void 0) controller.polarTime = options.pitchTime;
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
function asControlsAPI(controlsSystem) {
|
|
2674
|
+
return {
|
|
2675
|
+
handlers: controlsSystem.handlers,
|
|
2676
|
+
zoomBy: controlsSystem.zoomBy.bind(controlsSystem),
|
|
2677
|
+
zoomTo: controlsSystem.zoomTo.bind(controlsSystem),
|
|
2678
|
+
panBy: controlsSystem.panBy.bind(controlsSystem),
|
|
2679
|
+
panTo: controlsSystem.panTo.bind(controlsSystem),
|
|
2680
|
+
rollBy: controlsSystem.rollBy.bind(controlsSystem),
|
|
2681
|
+
rollTo: controlsSystem.rollTo.bind(controlsSystem),
|
|
2682
|
+
pitchBy: controlsSystem.pitchBy.bind(controlsSystem),
|
|
2683
|
+
pitchTo: controlsSystem.pitchTo.bind(controlsSystem),
|
|
2684
|
+
resetCamera: controlsSystem.resetCamera.bind(controlsSystem),
|
|
2685
|
+
configure: controlsSystem.configure.bind(controlsSystem)
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
function shortestRotationAngle(targetAngle, sourceAngle) {
|
|
2689
|
+
const angle = targetAngle - sourceAngle;
|
|
2690
|
+
const TAU = Math.PI * 2;
|
|
2691
|
+
return MathUtils.euclideanModulo(angle + Math.PI, TAU) - Math.PI;
|
|
2411
2692
|
}
|
|
2412
2693
|
class EventSystem {
|
|
2413
2694
|
constructor() {
|
|
@@ -2478,29 +2759,36 @@ function asEventAPI(system) {
|
|
|
2478
2759
|
clear: system.clear.bind(system)
|
|
2479
2760
|
};
|
|
2480
2761
|
}
|
|
2481
|
-
function
|
|
2762
|
+
function clientToCanvas(event, domElement, target) {
|
|
2763
|
+
target = target ?? new Vector2();
|
|
2482
2764
|
const { left, top } = domElement.getBoundingClientRect();
|
|
2483
|
-
|
|
2765
|
+
const clientX = "clientX" in event ? event.clientX : event.x;
|
|
2766
|
+
const clientY = "clientY" in event ? event.clientY : event.y;
|
|
2767
|
+
target.set(clientX - left, clientY - top);
|
|
2768
|
+
return target;
|
|
2484
2769
|
}
|
|
2485
|
-
function canvasToNDC(coordinates,
|
|
2770
|
+
function canvasToNDC(coordinates, canvas, target) {
|
|
2486
2771
|
target = target ?? new Vector2();
|
|
2487
|
-
const { width, height } =
|
|
2772
|
+
const { width, height } = canvas.getBoundingClientRect();
|
|
2488
2773
|
const uv = [coordinates.x / width, coordinates.y / height];
|
|
2489
2774
|
target.set(uv[0] * 2 - 1, -uv[1] * 2 + 1);
|
|
2490
2775
|
return target;
|
|
2491
2776
|
}
|
|
2492
2777
|
class Handler {
|
|
2493
2778
|
/**
|
|
2494
|
-
* @param
|
|
2779
|
+
* @param viewportSystem The viewport system instance
|
|
2495
2780
|
* @param domElement The DOM element to attach event listeners to
|
|
2496
2781
|
* @param eventManager Shared Mjolnir EventManager for gesture recognition
|
|
2497
2782
|
*/
|
|
2498
|
-
constructor(
|
|
2783
|
+
constructor(viewportSystem, domElement, eventManager) {
|
|
2499
2784
|
/** Whether this handler is enabled */
|
|
2500
2785
|
__publicField(this, "enabled", false);
|
|
2501
|
-
|
|
2786
|
+
/** The camera-controls instance for camera manipulation */
|
|
2787
|
+
__publicField(this, "controller");
|
|
2788
|
+
this.viewportSystem = viewportSystem;
|
|
2502
2789
|
this.domElement = domElement;
|
|
2503
2790
|
this.eventManager = eventManager;
|
|
2791
|
+
this.controller = viewportSystem.cameraController;
|
|
2504
2792
|
}
|
|
2505
2793
|
/**
|
|
2506
2794
|
* Per-frame update for this handler.
|
|
@@ -2532,7 +2820,7 @@ class Handler {
|
|
|
2532
2820
|
}
|
|
2533
2821
|
/**
|
|
2534
2822
|
* Disable this handler.
|
|
2535
|
-
*
|
|
2823
|
+
* Resets to initial state without transition, then calls onDisable() hook for subclass-specific disabling logic
|
|
2536
2824
|
*/
|
|
2537
2825
|
disable() {
|
|
2538
2826
|
if (this.enabled) {
|
|
@@ -2548,17 +2836,144 @@ class Handler {
|
|
|
2548
2836
|
return this.enabled;
|
|
2549
2837
|
}
|
|
2550
2838
|
}
|
|
2839
|
+
class InertiaController {
|
|
2840
|
+
/**
|
|
2841
|
+
* @param controller {@link CameraController}
|
|
2842
|
+
* @param opts {@link InertiaOptions}
|
|
2843
|
+
*/
|
|
2844
|
+
constructor(controller, opts = {}) {
|
|
2845
|
+
__publicField(this, "enabled", true);
|
|
2846
|
+
__publicField(this, "minSampleMs", 50);
|
|
2847
|
+
__publicField(this, "durationMs", 1e3);
|
|
2848
|
+
__publicField(this, "speedThreshold", 0.08);
|
|
2849
|
+
__publicField(this, "ease");
|
|
2850
|
+
__publicField(this, "samples", []);
|
|
2851
|
+
__publicField(this, "position", new Vector3());
|
|
2852
|
+
__publicField(this, "target", new Vector3());
|
|
2853
|
+
__publicField(this, "delta", new Vector3());
|
|
2854
|
+
__publicField(this, "velocity", new Vector3());
|
|
2855
|
+
__publicField(this, "active", false);
|
|
2856
|
+
__publicField(this, "elapsedMs", 0);
|
|
2857
|
+
__publicField(this, "prevFactor", 1);
|
|
2858
|
+
__publicField(this, "enableZ", false);
|
|
2859
|
+
__publicField(this, "isConnected", false);
|
|
2860
|
+
__publicField(this, "onControlStart", () => {
|
|
2861
|
+
this.active = false;
|
|
2862
|
+
this.samples.length = 0;
|
|
2863
|
+
});
|
|
2864
|
+
__publicField(this, "onControl", () => {
|
|
2865
|
+
this.controller.getTarget(this.target, false);
|
|
2866
|
+
this.samples.push({ t: performance.now(), target: this.target.clone() });
|
|
2867
|
+
const cutoff = performance.now() - 300;
|
|
2868
|
+
while (this.samples.length && this.samples[0].t < cutoff) this.samples.shift();
|
|
2869
|
+
});
|
|
2870
|
+
__publicField(this, "onControlEnd", () => {
|
|
2871
|
+
const now = performance.now();
|
|
2872
|
+
const minSampleTimestamp = now - this.minSampleMs;
|
|
2873
|
+
for (let i = this.samples.length - 1; i >= 0; i--) {
|
|
2874
|
+
const s = this.samples[i];
|
|
2875
|
+
if (s.t < minSampleTimestamp || i === 0) {
|
|
2876
|
+
const dt = now - s.t;
|
|
2877
|
+
if (dt <= 0) return;
|
|
2878
|
+
this.controller.getTarget(this.target, false);
|
|
2879
|
+
this.velocity.subVectors(this.target, s.target).divideScalar(dt);
|
|
2880
|
+
if (!this.enableZ) this.velocity.z = 0;
|
|
2881
|
+
const speed = this.velocity.length();
|
|
2882
|
+
if (speed <= this.speedThreshold) return;
|
|
2883
|
+
this.elapsedMs = 0;
|
|
2884
|
+
this.prevFactor = 1;
|
|
2885
|
+
this.active = true;
|
|
2886
|
+
return;
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
});
|
|
2890
|
+
this.controller = controller;
|
|
2891
|
+
this.configure(opts);
|
|
2892
|
+
this.ease = opts.ease ?? ((t) => Math.pow(2, 10 * (t - 1)));
|
|
2893
|
+
}
|
|
2894
|
+
/**
|
|
2895
|
+
* Configure the inertia controller.
|
|
2896
|
+
* @param opts {@link InertiaOptions}
|
|
2897
|
+
*/
|
|
2898
|
+
configure(opts) {
|
|
2899
|
+
if (opts.enabled !== void 0) {
|
|
2900
|
+
const prev = this.enabled;
|
|
2901
|
+
this.enabled = opts.enabled;
|
|
2902
|
+
if (prev !== this.enabled) {
|
|
2903
|
+
if (this.enabled) this.connect();
|
|
2904
|
+
else this.disconnect();
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
if (opts.minSampleMs !== void 0) this.minSampleMs = opts.minSampleMs;
|
|
2908
|
+
if (opts.durationMs !== void 0) this.durationMs = opts.durationMs;
|
|
2909
|
+
if (opts.speedThreshold !== void 0) this.speedThreshold = opts.speedThreshold;
|
|
2910
|
+
if (opts.ease !== void 0) this.ease = opts.ease;
|
|
2911
|
+
}
|
|
2912
|
+
/**
|
|
2913
|
+
* Connect the inertia controller event listeners to the camera controls.
|
|
2914
|
+
*/
|
|
2915
|
+
connect() {
|
|
2916
|
+
if (!this.enabled || this.isConnected) return;
|
|
2917
|
+
this.controller.addEventListener("controlstart", this.onControlStart);
|
|
2918
|
+
this.controller.addEventListener("control", this.onControl);
|
|
2919
|
+
this.controller.addEventListener("controlend", this.onControlEnd);
|
|
2920
|
+
this.isConnected = true;
|
|
2921
|
+
}
|
|
2922
|
+
/**
|
|
2923
|
+
* Disconnect the inertia controller event listeners from the camera controls.
|
|
2924
|
+
*/
|
|
2925
|
+
disconnect() {
|
|
2926
|
+
if (!this.isConnected) return;
|
|
2927
|
+
this.active = false;
|
|
2928
|
+
this.controller.removeEventListener("controlstart", this.onControlStart);
|
|
2929
|
+
this.controller.removeEventListener("control", this.onControl);
|
|
2930
|
+
this.controller.removeEventListener("controlend", this.onControlEnd);
|
|
2931
|
+
this.isConnected = false;
|
|
2932
|
+
}
|
|
2933
|
+
/**
|
|
2934
|
+
* Per-frame update of camera properties during inertia.
|
|
2935
|
+
* @param delta delta time in seconds
|
|
2936
|
+
* @returns true if the camera has moved due to inertia
|
|
2937
|
+
*/
|
|
2938
|
+
update(delta) {
|
|
2939
|
+
if (!this.active) return false;
|
|
2940
|
+
const deltaMs = delta * 1e3;
|
|
2941
|
+
this.elapsedMs += deltaMs;
|
|
2942
|
+
const t = Math.min(1, this.elapsedMs / this.durationMs);
|
|
2943
|
+
const factor = this.ease(1 - t);
|
|
2944
|
+
const avgFactor = (factor + this.prevFactor) * 0.5;
|
|
2945
|
+
this.prevFactor = factor;
|
|
2946
|
+
this.controller.getPosition(this.position, false);
|
|
2947
|
+
this.controller.getTarget(this.target, false);
|
|
2948
|
+
this.delta.copy(this.velocity).multiplyScalar(avgFactor * deltaMs);
|
|
2949
|
+
this.target.add(this.delta);
|
|
2950
|
+
this.position.add(this.delta);
|
|
2951
|
+
void this.controller.moveTo(this.target.x, this.target.y, this.target.z, false);
|
|
2952
|
+
if (factor <= 0.02 || t >= 1) {
|
|
2953
|
+
this.active = false;
|
|
2954
|
+
}
|
|
2955
|
+
return true;
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2551
2958
|
class PanHandler extends Handler {
|
|
2552
|
-
|
|
2553
|
-
|
|
2959
|
+
constructor() {
|
|
2960
|
+
super(...arguments);
|
|
2961
|
+
__publicField(this, "inertia", new InertiaController(this.controller));
|
|
2962
|
+
}
|
|
2963
|
+
reset(enableTransition = true) {
|
|
2964
|
+
return this.controller.moveTo(0, 0, 0, enableTransition);
|
|
2965
|
+
}
|
|
2966
|
+
update(delta) {
|
|
2967
|
+
return this.inertia.update(delta);
|
|
2554
2968
|
}
|
|
2555
2969
|
/**
|
|
2556
2970
|
* Enable pan gestures.
|
|
2557
2971
|
* Configures camera-controls to handle left-click drag and single-touch drag
|
|
2558
2972
|
*/
|
|
2559
2973
|
onEnable() {
|
|
2560
|
-
this.controller.mouseButtons.left = CameraController.ACTION.
|
|
2561
|
-
this.controller.touches.one = CameraController.ACTION.
|
|
2974
|
+
this.controller.mouseButtons.left = CameraController.ACTION.SCREEN_PAN;
|
|
2975
|
+
this.controller.touches.one = CameraController.ACTION.TOUCH_SCREEN_PAN;
|
|
2976
|
+
this.inertia.connect();
|
|
2562
2977
|
}
|
|
2563
2978
|
/**
|
|
2564
2979
|
* Disable pan gestures.
|
|
@@ -2567,195 +2982,297 @@ class PanHandler extends Handler {
|
|
|
2567
2982
|
onDisable() {
|
|
2568
2983
|
this.controller.mouseButtons.left = CameraController.ACTION.NONE;
|
|
2569
2984
|
this.controller.touches.one = CameraController.ACTION.NONE;
|
|
2985
|
+
this.inertia.disconnect();
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
class PitchHandler extends Handler {
|
|
2989
|
+
/**
|
|
2990
|
+
* @param viewportSystem {@link ViewportSystem} instance
|
|
2991
|
+
* @param domElement {@link HTMLElement} instance
|
|
2992
|
+
* @param eventManager {@link EventManager} instance
|
|
2993
|
+
*/
|
|
2994
|
+
constructor(viewportSystem, domElement, eventManager) {
|
|
2995
|
+
super(viewportSystem, domElement, eventManager);
|
|
2996
|
+
// Configuration
|
|
2997
|
+
__publicField(this, "minPitch", 0);
|
|
2998
|
+
__publicField(this, "maxPitch", 85);
|
|
2999
|
+
__publicField(this, "isValid");
|
|
3000
|
+
__publicField(this, "firstMove");
|
|
3001
|
+
__publicField(this, "lastPoints");
|
|
3002
|
+
__publicField(this, "prevTwoFingerAction");
|
|
3003
|
+
__publicField(this, "onPitchStart", (e) => {
|
|
3004
|
+
const pointers = e.pointers.sort((a, b) => a.pointerId - b.pointerId);
|
|
3005
|
+
const p0 = clientToCanvas(pointers[0], this.domElement);
|
|
3006
|
+
const p1 = clientToCanvas(pointers[1], this.domElement);
|
|
3007
|
+
this.lastPoints = [p0, p1];
|
|
3008
|
+
if (this.isVertical(p0.clone().sub(p1))) {
|
|
3009
|
+
this.isValid = false;
|
|
3010
|
+
}
|
|
3011
|
+
});
|
|
3012
|
+
__publicField(this, "onPitchEnd", () => {
|
|
3013
|
+
if (this.prevTwoFingerAction) this.controller.touches.two = this.prevTwoFingerAction;
|
|
3014
|
+
this.prevTwoFingerAction = void 0;
|
|
3015
|
+
this.isValid = void 0;
|
|
3016
|
+
this.firstMove = void 0;
|
|
3017
|
+
this.lastPoints = void 0;
|
|
3018
|
+
});
|
|
3019
|
+
__publicField(this, "onPitch", (e) => {
|
|
3020
|
+
const lastPoints = this.lastPoints;
|
|
3021
|
+
if (!lastPoints) return;
|
|
3022
|
+
const pointers = e.pointers.sort((a, b) => a.pointerId - b.pointerId);
|
|
3023
|
+
const p0 = clientToCanvas(pointers[0], this.domElement);
|
|
3024
|
+
const p1 = clientToCanvas(pointers[1], this.domElement);
|
|
3025
|
+
const vectorA = p0.clone().sub(lastPoints[0]);
|
|
3026
|
+
const vectorB = p1.clone().sub(lastPoints[1]);
|
|
3027
|
+
this.isValid = this.gestureBeginsVertically(vectorA, vectorB, e.timeStamp);
|
|
3028
|
+
if (!this.isValid) return;
|
|
3029
|
+
if (this.prevTwoFingerAction === void 0) {
|
|
3030
|
+
this.prevTwoFingerAction = this.controller.touches.two;
|
|
3031
|
+
this.controller.touches.two = CameraController.ACTION.NONE;
|
|
3032
|
+
}
|
|
3033
|
+
this.lastPoints = [p0, p1];
|
|
3034
|
+
const yDeltaAverage = (vectorA.y + vectorB.y) / 2;
|
|
3035
|
+
const degreesPerPixelMoved = -0.5;
|
|
3036
|
+
const deltaAngle = yDeltaAverage * degreesPerPixelMoved * DEG2RAD;
|
|
3037
|
+
void this.controller.rotatePolarTo(this.controller.polarAngle + deltaAngle, false);
|
|
3038
|
+
});
|
|
3039
|
+
this.updatePolarAngles();
|
|
3040
|
+
}
|
|
3041
|
+
reset(enableTransition = true) {
|
|
3042
|
+
const polarAngle = this.enabled ? this.minPitch * DEG2RAD : 0;
|
|
3043
|
+
return this.controller.rotatePolarTo(polarAngle, enableTransition);
|
|
3044
|
+
}
|
|
3045
|
+
/**
|
|
3046
|
+
* Configure pitch handler options
|
|
3047
|
+
* @param options Partial options to update
|
|
3048
|
+
*/
|
|
3049
|
+
configure(options) {
|
|
3050
|
+
if (options.minPitch !== void 0) this.minPitch = options.minPitch;
|
|
3051
|
+
if (options.maxPitch !== void 0) this.maxPitch = options.maxPitch;
|
|
3052
|
+
this.updatePolarAngles();
|
|
3053
|
+
}
|
|
3054
|
+
/**
|
|
3055
|
+
* Enable pitch.
|
|
3056
|
+
* Configures camera-controls to allow polar angle changes within configured range
|
|
3057
|
+
*/
|
|
3058
|
+
onEnable() {
|
|
3059
|
+
this.controller.mouseButtons.right |= CameraController.ACTION.ROTATE_POLAR;
|
|
3060
|
+
this.eventManager.on("pitchstart", this.onPitchStart);
|
|
3061
|
+
this.eventManager.on("pitch", this.onPitch);
|
|
3062
|
+
this.eventManager.on("pitchend", this.onPitchEnd);
|
|
3063
|
+
}
|
|
3064
|
+
/**
|
|
3065
|
+
* Disable pitch.
|
|
3066
|
+
*/
|
|
3067
|
+
onDisable() {
|
|
3068
|
+
this.controller.mouseButtons.right ^= CameraController.ACTION.ROTATE_POLAR;
|
|
3069
|
+
this.eventManager.off("pitchstart", this.onPitchStart);
|
|
3070
|
+
this.eventManager.off("pitch", this.onPitch);
|
|
3071
|
+
this.eventManager.off("pitchend", this.onPitchEnd);
|
|
3072
|
+
}
|
|
3073
|
+
/**
|
|
3074
|
+
* Update controller polar angles based on current configuration
|
|
3075
|
+
*/
|
|
3076
|
+
updatePolarAngles() {
|
|
3077
|
+
this.controller.minPolarAngle = this.minPitch * DEG2RAD;
|
|
3078
|
+
this.controller.maxPolarAngle = this.maxPitch * DEG2RAD;
|
|
3079
|
+
if (this.controller.polarAngle < this.controller.minPolarAngle) {
|
|
3080
|
+
void this.controller.rotatePolarTo(this.controller.minPolarAngle, false);
|
|
3081
|
+
}
|
|
3082
|
+
if (this.controller.polarAngle > this.controller.maxPolarAngle) {
|
|
3083
|
+
void this.controller.rotatePolarTo(this.controller.maxPolarAngle, false);
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
gestureBeginsVertically(vectorA, vectorB, timeStamp) {
|
|
3087
|
+
if (this.isValid !== void 0) return this.isValid;
|
|
3088
|
+
const threshold = 2;
|
|
3089
|
+
const movedA = vectorA.length() >= threshold;
|
|
3090
|
+
const movedB = vectorB.length() >= threshold;
|
|
3091
|
+
if (!movedA && !movedB) return;
|
|
3092
|
+
if (!movedA || !movedB) {
|
|
3093
|
+
this.firstMove ?? (this.firstMove = timeStamp);
|
|
3094
|
+
if (timeStamp - this.firstMove < ALLOWED_SINGLE_TOUCH_TIME) {
|
|
3095
|
+
return void 0;
|
|
3096
|
+
} else {
|
|
3097
|
+
return false;
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
const isSameDirection = vectorA.y > 0 === vectorB.y > 0;
|
|
3101
|
+
return this.isVertical(vectorA) && this.isVertical(vectorB) && isSameDirection;
|
|
3102
|
+
}
|
|
3103
|
+
isVertical(vector) {
|
|
3104
|
+
return Math.abs(vector.y) > Math.abs(vector.x);
|
|
2570
3105
|
}
|
|
2571
|
-
// FIXME: Add inertia
|
|
2572
3106
|
}
|
|
2573
|
-
const
|
|
3107
|
+
const ALLOWED_SINGLE_TOUCH_TIME = 100;
|
|
2574
3108
|
class RollHandler extends Handler {
|
|
2575
3109
|
constructor() {
|
|
2576
3110
|
super(...arguments);
|
|
2577
|
-
__publicField(this, "
|
|
3111
|
+
__publicField(this, "isRolling", false);
|
|
3112
|
+
// Configuration
|
|
3113
|
+
__publicField(this, "rotationThreshold", 25);
|
|
3114
|
+
// Threshold tracking (Mapbox-style)
|
|
2578
3115
|
__publicField(this, "startVector");
|
|
2579
3116
|
__publicField(this, "vector");
|
|
2580
3117
|
__publicField(this, "minDiameter", 0);
|
|
2581
|
-
__publicField(this, "
|
|
2582
|
-
|
|
3118
|
+
__publicField(this, "prevAngle", 0);
|
|
3119
|
+
// Camera and pivot vectors
|
|
2583
3120
|
__publicField(this, "pivotWorld", new Vector3());
|
|
2584
|
-
__publicField(this, "
|
|
2585
|
-
__publicField(this, "
|
|
2586
|
-
__publicField(this, "
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
const
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
const p0 = new Vector2(pointers[0].offsetX, pointers[0].offsetY);
|
|
2596
|
-
const p1 = new Vector2(pointers[1].offsetX, pointers[1].offsetY);
|
|
2597
|
-
this.startVector = this.vector = p0.clone().sub(p1);
|
|
2598
|
-
this.minDiameter = p0.distanceTo(p1);
|
|
2599
|
-
this.startBearing = 0;
|
|
2600
|
-
this.rotating = false;
|
|
2601
|
-
console.log("RollHandler: initialized rotation, diameter:", this.minDiameter);
|
|
2602
|
-
});
|
|
2603
|
-
__publicField(this, "onRotate", (event) => {
|
|
2604
|
-
if (!this.enabled) return;
|
|
2605
|
-
const { pointers, rotation } = event;
|
|
2606
|
-
if (pointers.length !== 2 || !this.startVector) {
|
|
2607
|
-
console.log("RollHandler: rotate ignored", pointers == null ? void 0 : pointers.length, !!this.startVector);
|
|
2608
|
-
return;
|
|
2609
|
-
}
|
|
2610
|
-
const p0 = new Vector2(pointers[0].offsetX, pointers[0].offsetY);
|
|
2611
|
-
const p1 = new Vector2(pointers[1].offsetX, pointers[1].offsetY);
|
|
2612
|
-
this.vector = p0.clone().sub(p1);
|
|
2613
|
-
if (!this.rotating && this.isBelowThreshold(this.vector)) {
|
|
2614
|
-
console.log("RollHandler: below threshold");
|
|
2615
|
-
return;
|
|
2616
|
-
}
|
|
2617
|
-
if (!this.rotating) {
|
|
2618
|
-
this.rotating = true;
|
|
2619
|
-
this.startBearing = rotation;
|
|
2620
|
-
console.log("RollHandler: rotation started, bearing:", rotation);
|
|
2621
|
-
}
|
|
2622
|
-
const bearingDelta = -(rotation - this.startBearing);
|
|
2623
|
-
this.startBearing = rotation;
|
|
2624
|
-
if (Math.abs(bearingDelta) < 0.01) {
|
|
2625
|
-
console.log("RollHandler: delta too small:", bearingDelta);
|
|
2626
|
-
return;
|
|
2627
|
-
}
|
|
2628
|
-
console.log("RollHandler: rotating by", bearingDelta, "degrees");
|
|
2629
|
-
const midpointPx = p0.clone().add(p1).multiplyScalar(0.5);
|
|
2630
|
-
this.normalizeScreenCoords(midpointPx, this.tempVec2);
|
|
2631
|
-
this.unprojectToWorldPlane(this.tempVec2, this.pivotWorld);
|
|
2632
|
-
void this.controller.rotate(bearingDelta * DEG2RAD, 0, false);
|
|
2633
|
-
this.projectWorldToScreen(this.pivotWorld, this.tempVec2);
|
|
2634
|
-
const pivotNewPx = this.denormalizeScreenCoords(this.tempVec2, this.tempVec3);
|
|
2635
|
-
const screenDeltaX = midpointPx.x - pivotNewPx.x;
|
|
2636
|
-
const screenDeltaY = midpointPx.y - pivotNewPx.y;
|
|
2637
|
-
const worldPan = this.screenDeltaToWorldDelta(screenDeltaX, screenDeltaY);
|
|
2638
|
-
void this.controller.truck(worldPan.x, worldPan.y, false);
|
|
3121
|
+
__publicField(this, "targetWorld", new Vector3());
|
|
3122
|
+
__publicField(this, "cameraPosition", new Vector3());
|
|
3123
|
+
__publicField(this, "cameraForward", new Vector3());
|
|
3124
|
+
__publicField(this, "rotationMatrix", new Matrix4());
|
|
3125
|
+
__publicField(this, "onRotateStart", (e) => {
|
|
3126
|
+
console.log("onRotateStart");
|
|
3127
|
+
const pointers = e.pointers;
|
|
3128
|
+
const p0 = clientToCanvas(pointers[0], this.domElement);
|
|
3129
|
+
const p1 = clientToCanvas(pointers[1], this.domElement);
|
|
3130
|
+
this.startVector = p0.sub(p1);
|
|
3131
|
+
this.minDiameter = this.startVector.length();
|
|
2639
3132
|
});
|
|
2640
3133
|
__publicField(this, "onRotateEnd", () => {
|
|
2641
|
-
this.
|
|
3134
|
+
this.isRolling = false;
|
|
2642
3135
|
this.startVector = void 0;
|
|
2643
3136
|
this.vector = void 0;
|
|
2644
3137
|
this.minDiameter = 0;
|
|
2645
3138
|
});
|
|
3139
|
+
__publicField(this, "onRotate", (e) => {
|
|
3140
|
+
const pointers = e.pointers;
|
|
3141
|
+
if (!this.isRolling) {
|
|
3142
|
+
const p0 = clientToCanvas(pointers[0], this.domElement);
|
|
3143
|
+
const p1 = clientToCanvas(pointers[1], this.domElement);
|
|
3144
|
+
this.vector = p0.sub(p1);
|
|
3145
|
+
if (this.isBelowThreshold(this.vector)) return;
|
|
3146
|
+
this.isRolling = true;
|
|
3147
|
+
this.prevAngle = e.rotation;
|
|
3148
|
+
}
|
|
3149
|
+
const deltaAngle = (e.rotation - this.prevAngle) * -DEG2RAD;
|
|
3150
|
+
this.prevAngle = e.rotation;
|
|
3151
|
+
if (Math.abs(deltaAngle) < 1e-3) return;
|
|
3152
|
+
this.setPivot(e);
|
|
3153
|
+
this.rotationMatrix.makeRotationZ(deltaAngle);
|
|
3154
|
+
this.cameraPosition.sub(this.pivotWorld).applyMatrix4(this.rotationMatrix).add(this.pivotWorld);
|
|
3155
|
+
this.cameraForward.applyMatrix4(this.rotationMatrix);
|
|
3156
|
+
this.targetWorld.copy(this.cameraPosition).add(this.cameraForward);
|
|
3157
|
+
void this.controller.setLookAt(
|
|
3158
|
+
this.cameraPosition.x,
|
|
3159
|
+
this.cameraPosition.y,
|
|
3160
|
+
this.cameraPosition.z,
|
|
3161
|
+
this.targetWorld.x,
|
|
3162
|
+
this.targetWorld.y,
|
|
3163
|
+
this.targetWorld.z,
|
|
3164
|
+
false
|
|
3165
|
+
);
|
|
3166
|
+
});
|
|
3167
|
+
}
|
|
3168
|
+
/**
|
|
3169
|
+
* Get bearing angle between current camera orientation and true north (in radians).
|
|
3170
|
+
* Angle is in range [0, 2π), going clockwise from north.
|
|
3171
|
+
*/
|
|
3172
|
+
get bearing() {
|
|
3173
|
+
const tau = Math.PI * 2;
|
|
3174
|
+
return MathUtils$1.euclideanModulo(-this.controller.azimuthAngle, tau);
|
|
3175
|
+
}
|
|
3176
|
+
reset(enableTransition = true) {
|
|
3177
|
+
return this.controller.normalizeRotations().rotateAzimuthTo(0, enableTransition);
|
|
2646
3178
|
}
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
3179
|
+
/**
|
|
3180
|
+
* Configure roll handler options
|
|
3181
|
+
* @param options Partial options to update
|
|
3182
|
+
*/
|
|
3183
|
+
configure(options) {
|
|
3184
|
+
if (options.rotationThreshold !== void 0) {
|
|
3185
|
+
this.rotationThreshold = options.rotationThreshold;
|
|
3186
|
+
}
|
|
2653
3187
|
}
|
|
2654
3188
|
/**
|
|
2655
3189
|
* Enable roll gestures.
|
|
2656
|
-
* -
|
|
3190
|
+
* Configures camera-controls to allow any azimuth angle and two-finger touch rotate
|
|
2657
3191
|
*/
|
|
2658
3192
|
onEnable() {
|
|
2659
|
-
this.controller.
|
|
2660
|
-
this.controller.minAzimuthAngle = -Infinity;
|
|
3193
|
+
this.controller.mouseButtons.right |= CameraController.ACTION.ROTATE_AZIMUTH;
|
|
2661
3194
|
this.eventManager.on("rotatestart", this.onRotateStart);
|
|
2662
3195
|
this.eventManager.on("rotate", this.onRotate);
|
|
2663
3196
|
this.eventManager.on("rotateend", this.onRotateEnd);
|
|
2664
3197
|
}
|
|
2665
3198
|
/**
|
|
2666
3199
|
* Disable roll gestures.
|
|
2667
|
-
* Restricts azimuth angle to zero and removes event listeners
|
|
2668
3200
|
*/
|
|
2669
3201
|
onDisable() {
|
|
2670
|
-
this.controller.
|
|
2671
|
-
this.controller.minAzimuthAngle = 0;
|
|
3202
|
+
this.controller.mouseButtons.right ^= CameraController.ACTION.ROTATE_AZIMUTH;
|
|
2672
3203
|
this.eventManager.off("rotatestart", this.onRotateStart);
|
|
2673
3204
|
this.eventManager.off("rotate", this.onRotate);
|
|
2674
3205
|
this.eventManager.off("rotateend", this.onRotateEnd);
|
|
2675
|
-
|
|
3206
|
+
}
|
|
3207
|
+
setPivot(e) {
|
|
3208
|
+
const pivotScreen = e.center;
|
|
3209
|
+
const pivotNDC = canvasToNDC(pivotScreen, this.domElement);
|
|
3210
|
+
const pivotWorld2D = this.viewportSystem.screenTo("world", pivotNDC);
|
|
3211
|
+
this.pivotWorld.set(pivotWorld2D.x, pivotWorld2D.y, 0);
|
|
3212
|
+
this.controller.getPosition(this.cameraPosition);
|
|
3213
|
+
this.controller.getTarget(this.targetWorld);
|
|
3214
|
+
this.cameraForward.copy(this.targetWorld).sub(this.cameraPosition);
|
|
2676
3215
|
}
|
|
2677
3216
|
/**
|
|
2678
3217
|
* Check if rotation is below threshold (Mapbox-style).
|
|
2679
|
-
*
|
|
3218
|
+
* The threshold before a rotation actually happens is configured in
|
|
3219
|
+
* pixels along the circumference of the circle formed by the two fingers.
|
|
3220
|
+
* This makes the threshold in degrees larger when the fingers are close
|
|
3221
|
+
* together and smaller when the fingers are far apart.
|
|
3222
|
+
* Uses the smallest diameter from the whole gesture to reduce sensitivity
|
|
3223
|
+
* when pinching in and out.
|
|
2680
3224
|
* @param vector Current vector between fingers
|
|
2681
3225
|
* @returns true if below threshold, false otherwise
|
|
2682
3226
|
*/
|
|
2683
3227
|
isBelowThreshold(vector) {
|
|
2684
3228
|
this.minDiameter = Math.min(this.minDiameter, vector.length());
|
|
2685
3229
|
const circumference = Math.PI * this.minDiameter;
|
|
2686
|
-
const
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
3230
|
+
const threshold = this.rotationThreshold / circumference * 360;
|
|
3231
|
+
const startVector = this.startVector;
|
|
3232
|
+
if (!startVector) return false;
|
|
3233
|
+
const bearingDeltaSinceStart = this.getBearingDelta(vector, startVector);
|
|
3234
|
+
console.log("bearingDeltaSinceStart", vector, startVector);
|
|
3235
|
+
return Math.abs(bearingDeltaSinceStart) < threshold;
|
|
2690
3236
|
}
|
|
2691
3237
|
/**
|
|
2692
|
-
* Get signed angle between two vectors in degrees
|
|
3238
|
+
* Get signed angle between two vectors in degrees (Mapbox pattern)
|
|
2693
3239
|
* @param a First vector
|
|
2694
3240
|
* @param b Second vector
|
|
2695
3241
|
* @returns Angle difference in degrees
|
|
2696
3242
|
*/
|
|
2697
3243
|
getBearingDelta(a, b) {
|
|
2698
|
-
return a.
|
|
3244
|
+
return a.angleTo(b) * RAD2DEG;
|
|
2699
3245
|
}
|
|
2700
3246
|
/**
|
|
2701
|
-
* Normalize
|
|
2702
|
-
* @param
|
|
2703
|
-
* @
|
|
2704
|
-
* @returns NDC coordinates
|
|
3247
|
+
* Normalize angle to be between -π and π
|
|
3248
|
+
* @param angle Angle in radians
|
|
3249
|
+
* @returns Normalized angle in radians
|
|
2705
3250
|
*/
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
3251
|
+
normalizeAngle(angle) {
|
|
3252
|
+
while (angle > Math.PI) angle -= 2 * Math.PI;
|
|
3253
|
+
while (angle < -Math.PI) angle += 2 * Math.PI;
|
|
3254
|
+
return angle;
|
|
2709
3255
|
}
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
* @returns Screen pixel coordinates
|
|
2715
|
-
*/
|
|
2716
|
-
denormalizeScreenCoords(ndc, out) {
|
|
2717
|
-
const canvas = this.domElement;
|
|
2718
|
-
return out.set((ndc.x + 1) * 0.5 * canvas.width, (1 - ndc.y) * 0.5 * canvas.height, 0);
|
|
2719
|
-
}
|
|
2720
|
-
/**
|
|
2721
|
-
* Unproject screen NDC coordinates to world plane (Z=0)
|
|
2722
|
-
* @param ndc NDC coordinates [-1, 1]
|
|
2723
|
-
* @param out Output vector for world coordinates
|
|
2724
|
-
* @returns World coordinates on Z=0 plane
|
|
2725
|
-
*/
|
|
2726
|
-
unprojectToWorldPlane(ndc, out) {
|
|
2727
|
-
const camera = this.controller.camera;
|
|
2728
|
-
this.raycaster.setFromCamera(ndc, camera);
|
|
2729
|
-
const ray = this.raycaster.ray;
|
|
2730
|
-
const t = -ray.origin.z / ray.direction.z;
|
|
2731
|
-
return out.copy(ray.origin).addScaledVector(ray.direction, t);
|
|
3256
|
+
}
|
|
3257
|
+
class ZoomHandler extends Handler {
|
|
3258
|
+
reset(enableTransition = true) {
|
|
3259
|
+
return this.controller.dollyTo(this.viewportSystem.zoomFactorToDistance(1), enableTransition);
|
|
2732
3260
|
}
|
|
2733
3261
|
/**
|
|
2734
|
-
*
|
|
2735
|
-
*
|
|
2736
|
-
* @param out Output vector for NDC coordinates
|
|
2737
|
-
* @returns NDC coordinates
|
|
3262
|
+
* Enable zoom gestures.
|
|
3263
|
+
* Configures camera-controls to handle mouse wheel and two-finger pinch
|
|
2738
3264
|
*/
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
return out.set(projected.x, projected.y);
|
|
3265
|
+
onEnable() {
|
|
3266
|
+
this.controller.mouseButtons.wheel = CameraController.ACTION.DOLLY;
|
|
3267
|
+
this.controller.touches.two = CameraController.ACTION.TOUCH_DOLLY_SCREEN_PAN;
|
|
2743
3268
|
}
|
|
2744
3269
|
/**
|
|
2745
|
-
*
|
|
2746
|
-
*
|
|
2747
|
-
* @param deltaX Screen delta X in pixels
|
|
2748
|
-
* @param deltaY Screen delta Y in pixels
|
|
2749
|
-
* @returns World-space translation vector
|
|
3270
|
+
* Disable zoom gestures.
|
|
3271
|
+
* Removes zoom actions from camera-controls
|
|
2750
3272
|
*/
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
const distance = Math.abs(camera.position.z);
|
|
2755
|
-
const fov = camera.getEffectiveFOV() * DEG2RAD;
|
|
2756
|
-
const worldHeight = 2 * distance * Math.tan(fov / 2);
|
|
2757
|
-
const worldWidth = worldHeight * camera.aspect;
|
|
2758
|
-
return this.tempVec2.set(deltaX / canvas.width * worldWidth, -(deltaY / canvas.height) * worldHeight);
|
|
3273
|
+
onDisable() {
|
|
3274
|
+
this.controller.mouseButtons.wheel = CameraController.ACTION.NONE;
|
|
3275
|
+
this.controller.touches.two = CameraController.ACTION.NONE;
|
|
2759
3276
|
}
|
|
2760
3277
|
}
|
|
2761
3278
|
class InteractionsSystem {
|
|
@@ -2775,25 +3292,28 @@ class InteractionsSystem {
|
|
|
2775
3292
|
__publicField(this, "dragStart");
|
|
2776
3293
|
__publicField(this, "dragThreshold", 15);
|
|
2777
3294
|
__publicField(this, "isDragging", false);
|
|
2778
|
-
this
|
|
3295
|
+
__publicField(this, "prevBearing", 0);
|
|
2779
3296
|
this.events = events;
|
|
2780
3297
|
this.viewportSystem = viewportSystem;
|
|
2781
3298
|
this.layerSystem = layerSystem;
|
|
2782
3299
|
this.canvas = renderer.canvas;
|
|
2783
3300
|
this.eventManager = new EventManager(this.canvas, {
|
|
2784
|
-
recognizers: [
|
|
3301
|
+
recognizers: [Rotate, [Pan, { event: "pitch", pointers: 2 }, "rotate"]]
|
|
2785
3302
|
});
|
|
2786
|
-
console.log("EventManager created with Rotate recognizer");
|
|
2787
3303
|
this.configureCameraControls();
|
|
2788
3304
|
this.attachCanvasListeners();
|
|
2789
|
-
const
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
roll: new RollHandler(
|
|
3305
|
+
const handlers = {
|
|
3306
|
+
pan: new PanHandler(viewportSystem, this.canvas, this.eventManager),
|
|
3307
|
+
zoom: new ZoomHandler(viewportSystem, this.canvas, this.eventManager),
|
|
3308
|
+
roll: new RollHandler(viewportSystem, this.canvas, this.eventManager),
|
|
3309
|
+
pitch: new PitchHandler(viewportSystem, this.canvas, this.eventManager)
|
|
2793
3310
|
};
|
|
2794
|
-
this.
|
|
3311
|
+
this.handlers = handlers;
|
|
3312
|
+
this.handlerArray = Object.values(handlers);
|
|
2795
3313
|
this.handlers.pan.enable();
|
|
3314
|
+
this.handlers.zoom.enable();
|
|
2796
3315
|
this.handlers.roll.enable();
|
|
3316
|
+
this.handlers.pitch.enable();
|
|
2797
3317
|
}
|
|
2798
3318
|
/**
|
|
2799
3319
|
* Update camera position and directions.
|
|
@@ -2803,44 +3323,43 @@ class InteractionsSystem {
|
|
|
2803
3323
|
*/
|
|
2804
3324
|
updateControls(delta) {
|
|
2805
3325
|
let needsUpdate = this.viewportSystem.cameraController.update(delta);
|
|
2806
|
-
if (needsUpdate) {
|
|
2807
|
-
console.log(
|
|
2808
|
-
// "azimuth angle",
|
|
2809
|
-
// Math.round(this.viewportSystem.cameraController.azimuthAngle * RAD2DEG),
|
|
2810
|
-
"polar angle",
|
|
2811
|
-
Math.round(this.viewportSystem.cameraController.polarAngle * RAD2DEG),
|
|
2812
|
-
"camera position",
|
|
2813
|
-
this.viewportSystem.cameraController.camera.position.toArray().map(Math.round)
|
|
2814
|
-
);
|
|
2815
|
-
}
|
|
2816
3326
|
for (const handler of this.handlerArray) {
|
|
2817
3327
|
if (handler.isEnabled()) {
|
|
2818
3328
|
needsUpdate = handler.update(delta) || needsUpdate;
|
|
2819
3329
|
}
|
|
2820
3330
|
}
|
|
3331
|
+
if (this.handlers.roll.bearing !== this.prevBearing) {
|
|
3332
|
+
this.prevBearing = this.handlers.roll.bearing;
|
|
3333
|
+
this.events.emit("navigation:roll", this.handlers.roll.bearing);
|
|
3334
|
+
}
|
|
2821
3335
|
return needsUpdate;
|
|
2822
3336
|
}
|
|
3337
|
+
/** Disconnect the interactions system. */
|
|
3338
|
+
disconnect() {
|
|
3339
|
+
this.viewportSystem.cameraController.disconnect();
|
|
3340
|
+
for (const handler of this.handlerArray) {
|
|
3341
|
+
handler.disable();
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
2823
3344
|
configureCameraControls() {
|
|
2824
3345
|
const controller = this.viewportSystem.cameraController;
|
|
2825
3346
|
controller.draggingSmoothTime = 0;
|
|
2826
3347
|
controller.dollyToCursor = true;
|
|
2827
|
-
controller.restThreshold = 1;
|
|
2828
|
-
controller.maxAzimuthAngle = 0;
|
|
2829
|
-
controller.minAzimuthAngle = 0;
|
|
2830
|
-
controller.maxPolarAngle = 0;
|
|
2831
3348
|
controller.mouseButtons = {
|
|
2832
3349
|
left: CameraController.ACTION.NONE,
|
|
2833
3350
|
middle: CameraController.ACTION.NONE,
|
|
2834
|
-
right: CameraController.ACTION.
|
|
3351
|
+
right: CameraController.ACTION.NONE,
|
|
2835
3352
|
wheel: CameraController.ACTION.NONE
|
|
2836
3353
|
};
|
|
2837
3354
|
controller.touches = {
|
|
2838
3355
|
one: CameraController.ACTION.NONE,
|
|
2839
3356
|
two: CameraController.ACTION.NONE,
|
|
2840
|
-
// Disabled - handlers implement custom gestures
|
|
2841
3357
|
three: CameraController.ACTION.NONE
|
|
2842
3358
|
};
|
|
2843
3359
|
controller.connect(this.canvas);
|
|
3360
|
+
controller.addEventListener("transitionstart", () => {
|
|
3361
|
+
this.events.emit("navigation:change");
|
|
3362
|
+
});
|
|
2844
3363
|
}
|
|
2845
3364
|
attachCanvasListeners() {
|
|
2846
3365
|
this.canvas.addEventListener("pointerdown", (event) => {
|
|
@@ -2866,7 +3385,8 @@ class InteractionsSystem {
|
|
|
2866
3385
|
const isDragging = type === "click" && this.isDragging;
|
|
2867
3386
|
const hasListeners = this.events.hasListeners(eventType);
|
|
2868
3387
|
if (isDragging || !hasListeners) return;
|
|
2869
|
-
|
|
3388
|
+
clientToCanvas(event, this.canvas, this.mousePointer);
|
|
3389
|
+
canvasToNDC(this.mousePointer, this.canvas, this.mousePointer);
|
|
2870
3390
|
const intersections = this.viewportSystem.getIntersectedObjects(this.mousePointer);
|
|
2871
3391
|
const point = this.viewportSystem.screenTo("svg", this.mousePointer);
|
|
2872
3392
|
const defs = this.layerSystem.getIntersectedDefs(intersections);
|
|
@@ -2885,23 +3405,24 @@ class Renderer {
|
|
|
2885
3405
|
/** {@link HTMLCanvasElement} that this renderer is rendering to */
|
|
2886
3406
|
__publicField(this, "canvas");
|
|
2887
3407
|
__publicField(this, "ui");
|
|
2888
|
-
__publicField(this, "
|
|
2889
|
-
__publicField(this, "renderer");
|
|
3408
|
+
__publicField(this, "gl");
|
|
2890
3409
|
__publicField(this, "eventSystem");
|
|
2891
3410
|
__publicField(this, "layerSystem");
|
|
2892
3411
|
__publicField(this, "viewportSystem");
|
|
2893
3412
|
__publicField(this, "interactionsSystem");
|
|
2894
|
-
|
|
2895
|
-
__publicField(this, "
|
|
2896
|
-
__publicField(this, "
|
|
3413
|
+
__publicField(this, "controlsSystem");
|
|
3414
|
+
__publicField(this, "clock");
|
|
3415
|
+
__publicField(this, "renderer");
|
|
2897
3416
|
__publicField(this, "viewport");
|
|
2898
3417
|
__publicField(this, "needsRedraw", true);
|
|
2899
|
-
__publicField(this, "
|
|
3418
|
+
__publicField(this, "memoryInfoExtension");
|
|
3419
|
+
__publicField(this, "memoryInfo", "");
|
|
2900
3420
|
var _a, _b;
|
|
2901
3421
|
const { canvas, gl, debugLog = false, ui } = opts;
|
|
2902
3422
|
this.canvas = canvas;
|
|
2903
3423
|
this.debugLog = debugLog;
|
|
2904
3424
|
this.ui = ui;
|
|
3425
|
+
this.gl = gl;
|
|
2905
3426
|
const rendererOptions = {
|
|
2906
3427
|
antialias: true,
|
|
2907
3428
|
context: gl,
|
|
@@ -2915,31 +3436,17 @@ class Renderer {
|
|
|
2915
3436
|
this.viewportSystem = new ViewportSystem(this, this.eventSystem);
|
|
2916
3437
|
this.layerSystem = new LayerSystem(this);
|
|
2917
3438
|
this.interactionsSystem = new InteractionsSystem(this, this.eventSystem, this.viewportSystem, this.layerSystem);
|
|
3439
|
+
this.controlsSystem = new ControlsSystem(this, this.viewportSystem, this.interactionsSystem);
|
|
2918
3440
|
this.memoryInfoExtension = this.renderer.getContext().getExtension("GMAN_webgl_memory");
|
|
2919
3441
|
this.canvas.addEventListener("webglcontextlost", (e) => this.onContextLost(e), false);
|
|
2920
3442
|
this.canvas.addEventListener("webglcontextrestored", (e) => this.onContextRestored(e), false);
|
|
2921
3443
|
void ((_b = (_a = this.ui) == null ? void 0 : _a.stats) == null ? void 0 : _b.init(this.renderer.getContext()));
|
|
2922
3444
|
}
|
|
2923
3445
|
/**
|
|
2924
|
-
* {@link
|
|
3446
|
+
* {@link ControlsAPI} instance for controlling the viewport
|
|
2925
3447
|
*/
|
|
2926
3448
|
get controls() {
|
|
2927
|
-
return
|
|
2928
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
2929
|
-
zoomBy: () => {
|
|
2930
|
-
},
|
|
2931
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
2932
|
-
zoomTo: () => {
|
|
2933
|
-
},
|
|
2934
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
2935
|
-
resetCamera: () => {
|
|
2936
|
-
},
|
|
2937
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
2938
|
-
configure: () => {
|
|
2939
|
-
},
|
|
2940
|
-
handlers: this.interactionsSystem.handlers,
|
|
2941
|
-
controller: this.viewportSystem.cameraController
|
|
2942
|
-
};
|
|
3449
|
+
return asControlsAPI(this.controlsSystem);
|
|
2943
3450
|
}
|
|
2944
3451
|
/**
|
|
2945
3452
|
* {@link EventsAPI} instance for subscribing to internal events
|
|
@@ -2978,14 +3485,18 @@ class Renderer {
|
|
|
2978
3485
|
* Sets the renderer to external mode, where parts of rendering process are not managed by the renderer (e.g. Mapbox GL JS).
|
|
2979
3486
|
* @param staticTransformMatrix static transform matrix to apply to the scene
|
|
2980
3487
|
*/
|
|
2981
|
-
//
|
|
2982
|
-
|
|
3488
|
+
// TODO: Move somewhere
|
|
3489
|
+
setExternalTransform(staticTransformMatrix) {
|
|
3490
|
+
this.renderer.autoClear = false;
|
|
3491
|
+
this.interactionsSystem.disconnect();
|
|
3492
|
+
this.viewportSystem.setExternalTransform(staticTransformMatrix);
|
|
2983
3493
|
}
|
|
2984
3494
|
/**
|
|
2985
3495
|
* Update scene matrix from dynamic transform matrix.
|
|
2986
3496
|
* @param dynamicTransformMatrix dynamic transform matrix (changes every frame)
|
|
2987
3497
|
*/
|
|
2988
|
-
|
|
3498
|
+
updateExternalCamera(dynamicTransformMatrix) {
|
|
3499
|
+
this.viewportSystem.updateExternalCamera(dynamicTransformMatrix);
|
|
2989
3500
|
}
|
|
2990
3501
|
/**
|
|
2991
3502
|
* Initialize the scene and start the rendering loop
|
|
@@ -3006,10 +3517,9 @@ class Renderer {
|
|
|
3006
3517
|
this.layerSystem.updateDefs(defs);
|
|
3007
3518
|
this.needsRedraw = true;
|
|
3008
3519
|
}
|
|
3009
|
-
// FIXME: Move to viewport system?
|
|
3010
3520
|
/**
|
|
3011
3521
|
* Converts coordinates from canvas space to SVG space.
|
|
3012
|
-
* @param point point in canvas space (relative to the canvas's top left corner)
|
|
3522
|
+
* @param point point in canvas space (relative to the canvas's top left corner), in css pixels
|
|
3013
3523
|
* @returns point in SVG space
|
|
3014
3524
|
*/
|
|
3015
3525
|
screenToSvg(point) {
|
|
@@ -3023,7 +3533,7 @@ class Renderer {
|
|
|
3023
3533
|
render() {
|
|
3024
3534
|
var _a, _b, _c, _d, _e, _f;
|
|
3025
3535
|
(_b = (_a = this.ui) == null ? void 0 : _a.stats) == null ? void 0 : _b.begin();
|
|
3026
|
-
if (this.
|
|
3536
|
+
if (this.gl !== void 0) this.renderer.resetState();
|
|
3027
3537
|
else this.resizeCanvasToDisplaySize();
|
|
3028
3538
|
this.viewportSystem.updatePtScale();
|
|
3029
3539
|
const delta = this.clock.getDelta();
|