@expofp/renderer 1.5.0 → 2.0.0

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 (3) hide show
  1. package/dist/index.d.ts +90 -32
  2. package/dist/index.js +645 -319
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ 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
4
  var _a;
5
- import { Color, Matrix4, Vector3, DataTexture, RGBAFormat, FloatType, RedFormat, UnsignedIntType, IntType, RGBAIntegerFormat, RGFormat, RGIntegerFormat, RedIntegerFormat, BatchedMesh as BatchedMesh$1, BufferAttribute, StreamDrawUsage, Vector4, AlwaysDepth, DoubleSide, MeshBasicMaterial, Texture, Group, PlaneGeometry, SRGBColorSpace, Vector2, Mesh, LessEqualDepth, Quaternion, BufferGeometry, LinearSRGBColorSpace, Plane, Raycaster, Sphere, Box3, Spherical, PerspectiveCamera, Scene, Camera, MathUtils, Clock, WebGLRenderer } from "three";
5
+ import { Color, Matrix4, Vector3, DataTexture, RGBAFormat, FloatType, RedFormat, UnsignedIntType, IntType, RGBAIntegerFormat, RGFormat, RGIntegerFormat, RedIntegerFormat, BatchedMesh as BatchedMesh$1, BufferAttribute, StreamDrawUsage, Vector4, AlwaysDepth, DoubleSide, MeshBasicMaterial, Texture, Group, PlaneGeometry, SRGBColorSpace, Vector2, Mesh, LessEqualDepth, Quaternion, BufferGeometry, LinearSRGBColorSpace, Plane, Raycaster, Sphere, Box3, Spherical, PerspectiveCamera, Camera, Scene, MathUtils, Clock, WebGLRenderer } from "three";
6
6
  import { traverseAncestorsGenerator } from "three/examples/jsm/utils/SceneUtils.js";
7
7
  import { BatchedText as BatchedText$1, Text as Text$1 } from "troika-three-text";
8
8
  import { LineMaterial, LineSegmentsGeometry } from "three/examples/jsm/Addons.js";
@@ -336,7 +336,7 @@ const dimColorVertexImpl = (
336
336
  `
337
337
  void setDimAmount() {
338
338
  float instanceDim = 0.;
339
- #ifdef USE_BATCH_UNIFORMS
339
+ #ifdef USE_BATCH_UNIFORMS
340
340
  instanceDim = batch_skipDimInstance;
341
341
  #endif
342
342
  #ifdef TROIKA_DERIVED_MATERIAL_1
@@ -747,7 +747,7 @@ const _BatchedMesh = class _BatchedMesh extends BatchedMesh$1 {
747
747
  }
748
748
  dispose() {
749
749
  var _a2;
750
- this.geometry.setIndex(this.indexBuffer ?? null);
750
+ if (this.indexBuffer) this.geometry.setIndex(this.indexBuffer);
751
751
  super.dispose();
752
752
  this.uniformSchema = {};
753
753
  (_a2 = this.uniformsTexture) == null ? void 0 : _a2.dispose();
@@ -1082,19 +1082,6 @@ function isVisible(object) {
1082
1082
  if (!object.visible) return false;
1083
1083
  return [...traverseAncestorsGenerator(object)].every((obj) => obj.visible);
1084
1084
  }
1085
- function printTree(object, fullName = false) {
1086
- object.traverse((obj) => {
1087
- let s = "";
1088
- let obj2 = obj;
1089
- while (obj2 !== object) {
1090
- s = "|___ " + s;
1091
- obj2 = (obj2 == null ? void 0 : obj2.parent) ?? null;
1092
- }
1093
- const renderOrder = obj.isGroup ? "" : `, RO: ${obj.renderOrder}`;
1094
- const name = fullName ? obj.name : obj.name.split(":").at(-1);
1095
- console.log(`${s}${name}<${obj.type}>${renderOrder}`);
1096
- });
1097
- }
1098
1085
  class RenderableSystem {
1099
1086
  /**
1100
1087
  * @param type readable name of the system's type for debugging
@@ -1825,6 +1812,7 @@ class TextSystem extends RenderableSystem {
1825
1812
  __publicField(this, "initialTextScale", new Vector2(1, -1));
1826
1813
  __publicField(this, "textColor", new Color());
1827
1814
  __publicField(this, "pendingUpdates", /* @__PURE__ */ new Map());
1815
+ __publicField(this, "sdfAtlases", /* @__PURE__ */ new Set());
1828
1816
  __publicField(this, "alignmentOffset", new Vector2());
1829
1817
  __publicField(this, "alignmentDirection", new Vector2());
1830
1818
  __publicField(this, "localPosition", new Vector2());
@@ -1834,6 +1822,10 @@ class TextSystem extends RenderableSystem {
1834
1822
  __publicField(this, "localToMax", new Vector2());
1835
1823
  this.materialSystem = materialSystem;
1836
1824
  }
1825
+ dispose() {
1826
+ super.dispose();
1827
+ this.sdfAtlases.forEach((texture) => texture.dispose());
1828
+ }
1837
1829
  buildLayer(layer) {
1838
1830
  const group = new Group();
1839
1831
  const batchedText = this.buildBatchedText(layer);
@@ -1911,7 +1903,12 @@ class TextSystem extends RenderableSystem {
1911
1903
  for (const { textDef, instanceIds } of mappingData) {
1912
1904
  this.registerDefObject(textDef, batchedText, instanceIds);
1913
1905
  }
1914
- batchedText.addEventListener("synccomplete", () => this.renderer.update());
1906
+ batchedText.addEventListener("synccomplete", () => {
1907
+ var _a2;
1908
+ const sdfTexture = (_a2 = batchedText.textRenderInfo) == null ? void 0 : _a2.sdfTexture;
1909
+ if (sdfTexture) this.sdfAtlases.add(sdfTexture);
1910
+ this.renderer.update();
1911
+ });
1915
1912
  return batchedText;
1916
1913
  }
1917
1914
  // TODO: Simplify
@@ -2044,6 +2041,7 @@ class LayerSystem {
2044
2041
  }
2045
2042
  /**
2046
2043
  * Update the given defs immediately, or queue them for update if update buffering is enabled.
2044
+ * NOTE: Currently update buffering is disabled, as observed performance gains are negligible. Need to revisit this.
2047
2045
  * @param defs {@link RenderableDef} array
2048
2046
  */
2049
2047
  updateDefs(defs) {
@@ -2211,6 +2209,19 @@ class LayerSystem {
2211
2209
  return fullName;
2212
2210
  }
2213
2211
  }
2212
+ function printTree(object, fullName = false) {
2213
+ object.traverse((obj) => {
2214
+ let s = "";
2215
+ let obj2 = obj;
2216
+ while (obj2 !== object) {
2217
+ s = "|___ " + s;
2218
+ obj2 = (obj2 == null ? void 0 : obj2.parent) ?? null;
2219
+ }
2220
+ const renderOrder = obj.isGroup ? "" : `, RO: ${obj.renderOrder}`;
2221
+ const name = fullName ? obj.name : obj.name.split(":").at(-1);
2222
+ console.log(`${s}${name}<${obj.type}>${renderOrder}`);
2223
+ });
2224
+ }
2214
2225
  /*!
2215
2226
  * camera-controls
2216
2227
  * https://github.com/yomotsu/camera-controls
@@ -4747,6 +4758,20 @@ class CameraController extends CameraControls {
4747
4758
  constructor(camera, renderer) {
4748
4759
  super(camera);
4749
4760
  this.renderer = renderer;
4761
+ this.dollyToCursor = true;
4762
+ this.draggingSmoothTime = 0;
4763
+ void this.rotatePolarTo(0, false);
4764
+ this.mouseButtons = {
4765
+ left: CameraController.ACTION.NONE,
4766
+ middle: CameraController.ACTION.NONE,
4767
+ right: CameraController.ACTION.NONE,
4768
+ wheel: CameraController.ACTION.NONE
4769
+ };
4770
+ this.touches = {
4771
+ one: CameraController.ACTION.NONE,
4772
+ two: CameraController.ACTION.NONE,
4773
+ three: CameraController.ACTION.NONE
4774
+ };
4750
4775
  }
4751
4776
  update(delta) {
4752
4777
  var _a2;
@@ -4767,31 +4792,30 @@ class CameraSystem {
4767
4792
  * @param renderer {@link Renderer} instance
4768
4793
  */
4769
4794
  constructor(renderer) {
4770
- /** External camera instance. Used to render the scene in external mode (e.g. Mapbox GL JS). */
4771
- __publicField(this, "externalCamera");
4795
+ /** {@link PerspectiveCamera} instance. Used to render the scene in internal mode. */
4796
+ __publicField(this, "camera");
4772
4797
  /** {@link CameraController} instance. Used to smoothly animate the camera. */
4773
4798
  __publicField(this, "controller");
4774
- __publicField(this, "camera");
4775
- __publicField(this, "zoomIdentityDistance");
4799
+ /**
4800
+ * Cached previous viewport height used to preserve zoom across resizes.
4801
+ * Note: we intentionally keep this separate from the derived identity distance.
4802
+ */
4803
+ __publicField(this, "prevViewportHeightPx");
4804
+ /** [min, max] zoom factors */
4776
4805
  __publicField(this, "zoomBounds");
4806
+ /** Default FOV for the camera. Taken from Mapbox GL JS. */
4807
+ __publicField(this, "defaultFov", 36.87);
4777
4808
  this.renderer = renderer;
4778
- const [w, h] = renderer.size;
4779
- this.camera = new PerspectiveCamera(90, w / (h || 1));
4809
+ const h = renderer.size[1];
4810
+ this.prevViewportHeightPx = h;
4811
+ this.camera = new PerspectiveCamera(this.defaultFov);
4780
4812
  this.camera.up.set(0, 0, -1);
4781
- this.zoomIdentityDistance = h / 2;
4782
- this.camera.position.z = this.zoomIdentityDistance;
4783
- this.controller = new CameraController(this.camera, renderer);
4784
- this.controller.polarAngle = 0;
4813
+ this.controller = new CameraController(this.camera, this.renderer);
4785
4814
  this.controller.distance = this.zoomIdentityDistance;
4786
4815
  }
4787
- /** Current camera instance. */
4788
- get currentCamera() {
4789
- return this.externalCamera ?? this.camera;
4790
- }
4791
4816
  /** Current camera zoom factor. */
4792
4817
  get zoomFactor() {
4793
- const distance = this.controller.distance;
4794
- return distance ? this.zoomIdentityDistance / distance : 1;
4818
+ return this.zoomFactorForHeight(this.renderer.size[1]);
4795
4819
  }
4796
4820
  /**
4797
4821
  * Calculates the camera distance from the scene's plane for a given zoom factor.
@@ -4799,7 +4823,7 @@ class CameraSystem {
4799
4823
  * @returns Corresponding camera distance on the Z axis
4800
4824
  */
4801
4825
  zoomFactorToDistance(zoomFactor) {
4802
- return zoomFactor > 0 ? this.zoomIdentityDistance / zoomFactor : this.zoomIdentityDistance;
4826
+ return this.zoomIdentityDistance / zoomFactor;
4803
4827
  }
4804
4828
  /**
4805
4829
  * Initializes the camera with the given zoom bounds.
@@ -4813,41 +4837,188 @@ class CameraSystem {
4813
4837
  updateCamera() {
4814
4838
  if (!this.zoomBounds) return;
4815
4839
  const [w, h] = this.renderer.size;
4816
- const zoomFactor = this.zoomFactor;
4817
- this.zoomIdentityDistance = h / 2;
4818
- const maxDistance = Math.abs(this.zoomIdentityDistance / this.zoomBounds[0]);
4819
- const minDistance = Math.abs(this.zoomIdentityDistance / this.zoomBounds[1]);
4840
+ if (w <= 0 || h <= 0) return;
4841
+ const zoomFactor = this.zoomFactorForHeight(this.prevViewportHeightPx);
4842
+ const newZoomIdentity = this.zoomIdentityDistanceForHeight(h);
4843
+ const maxDistance = Math.abs(newZoomIdentity / this.zoomBounds[0]);
4844
+ const minDistance = Math.abs(newZoomIdentity / this.zoomBounds[1]);
4820
4845
  this.camera.aspect = w / (h || 1);
4821
- this.computeCameraClipPlanes(minDistance, maxDistance);
4846
+ this.camera.near = 0.01;
4847
+ this.camera.far = Math.max(maxDistance, this.camera.near) * 2;
4822
4848
  this.camera.updateProjectionMatrix();
4823
- this.syncController(minDistance, maxDistance, zoomFactor);
4824
- }
4825
- computeCameraClipPlanes(minDistance, maxDistance, nearSafetyFactor = 0.5, farSafetyFactor = 1.5) {
4826
- const fov = this.camera.fov * DEG2RAD$1;
4827
- const aspect = this.camera.aspect;
4828
- const maxPolarAngle = 85 * DEG2RAD$1;
4829
- const halfFovY = fov / 2;
4830
- const halfFovX = Math.atan(Math.tan(halfFovY) * aspect);
4831
- const diagonalFov = 2 * Math.atan(Math.sqrt(Math.tan(halfFovX) ** 2 + Math.tan(halfFovY) ** 2));
4832
- const minHeight = minDistance * Math.cos(maxPolarAngle);
4833
- const near = Math.max(0.1, minHeight * nearSafetyFactor);
4834
- const criticalHeight = minHeight;
4835
- const horizontalDistToOrbit = minDistance * Math.sin(maxPolarAngle);
4836
- const distToOrbit = minDistance;
4837
- const visibleRadiusAtOrbit = distToOrbit * Math.tan(diagonalFov / 2);
4838
- const planeExtent = Math.max(maxDistance, visibleRadiusAtOrbit);
4839
- const horizontalDistToFarEdge = horizontalDistToOrbit + planeExtent;
4840
- const maxViewDistance = Math.sqrt(criticalHeight ** 2 + horizontalDistToFarEdge ** 2);
4841
- const far = maxViewDistance * farSafetyFactor;
4842
- this.camera.near = near;
4843
- this.camera.far = far;
4844
- if (this.renderer.debugLog) console.log("camera clip planes", near, far);
4845
- }
4846
- syncController(minDistance, maxDistance, zoomFactor) {
4847
- if (this.renderer.debugLog) console.log("syncController", minDistance, maxDistance, zoomFactor);
4848
4849
  this.controller.minDistance = minDistance;
4849
4850
  this.controller.maxDistance = maxDistance;
4850
4851
  void this.controller.dollyTo(this.zoomFactorToDistance(zoomFactor), false);
4852
+ this.prevViewportHeightPx = h;
4853
+ }
4854
+ /**
4855
+ * Distance from the scene plane corresponding to zoomFactor = 1 for the current viewport height.
4856
+ * Derived from camera FOV and renderer height (in pixels).
4857
+ */
4858
+ get zoomIdentityDistance() {
4859
+ return this.zoomIdentityDistanceForHeight(this.renderer.size[1]);
4860
+ }
4861
+ /**
4862
+ * Calculates the zoom identity distance for a given viewport height.
4863
+ * @param viewportHeightPx Renderer height in pixels
4864
+ * @returns Zoom identity distance
4865
+ */
4866
+ zoomIdentityDistanceForHeight(viewportHeightPx) {
4867
+ if (viewportHeightPx <= 0) return 0;
4868
+ return viewportHeightPx * 0.5 / Math.tan(this.camera.fov * DEG2RAD$1 / 2);
4869
+ }
4870
+ /**
4871
+ * Calculates the zoom factor for a given viewport height.
4872
+ * @param viewportHeightPx Renderer height in pixels
4873
+ * @returns Zoom factor
4874
+ */
4875
+ zoomFactorForHeight(viewportHeightPx) {
4876
+ const zid = this.zoomIdentityDistanceForHeight(viewportHeightPx);
4877
+ if (zid === 0) return 1;
4878
+ return zid / (this.controller.distance || zid);
4879
+ }
4880
+ }
4881
+ class ExternalSystem {
4882
+ /**
4883
+ * @param pickingSystem {@link PickingSystem} instance
4884
+ */
4885
+ constructor(pickingSystem) {
4886
+ /** External camera instance */
4887
+ __publicField(this, "camera", new Camera());
4888
+ __publicField(this, "staticTransformMatrix", new Matrix4());
4889
+ __publicField(this, "intersectionPoint", new Vector3());
4890
+ /**
4891
+ * Scratch NDC coordinate used by external ptScale. Kept as a field to avoid allocating a Vector2 each frame.
4892
+ * (This is always screen center: NDC (0,0))
4893
+ */
4894
+ __publicField(this, "ndcCenter", new Vector2(0, 0));
4895
+ /**
4896
+ * Scratch clip-space vector used when projecting SVG points through the external camera matrix.
4897
+ * Kept as a field to avoid allocating a Vector4 each frame.
4898
+ */
4899
+ __publicField(this, "clipPoint", new Vector4());
4900
+ /**
4901
+ * Scratch pixel points used by external ptScale estimation.
4902
+ * p0 is the screen center in drawing-buffer pixels; p1/p2 are projected offsets from the anchor point.
4903
+ */
4904
+ __publicField(this, "px0", new Vector2());
4905
+ __publicField(this, "px1", new Vector2());
4906
+ __publicField(this, "px2", new Vector2());
4907
+ this.pickingSystem = pickingSystem;
4908
+ }
4909
+ /**
4910
+ * Set static part of an svg -> px transform matrix
4911
+ * @param staticTransformMatrix static transform matrix to apply to the scene
4912
+ */
4913
+ setStaticTransform(staticTransformMatrix) {
4914
+ if (!this.validateMatrix(staticTransformMatrix, "setStaticTransform")) return;
4915
+ this.staticTransformMatrix.fromArray(staticTransformMatrix);
4916
+ }
4917
+ /**
4918
+ * Set dynamic part of an svg -> px transform matrix. Should be called every frame.
4919
+ * @param dynamicTransformMatrix dynamic transform matrix (changes every frame)
4920
+ */
4921
+ setDynamicTransform(dynamicTransformMatrix) {
4922
+ if (!this.validateMatrix(dynamicTransformMatrix, "setDynamicTransform")) return;
4923
+ this.camera.projectionMatrix.fromArray(dynamicTransformMatrix).multiply(this.staticTransformMatrix);
4924
+ this.camera.projectionMatrixInverse.copy(this.camera.projectionMatrix).invert();
4925
+ }
4926
+ /**
4927
+ * Estimates pixel→SVG scale for external context rendering (e.g. Mapbox).
4928
+ *
4929
+ * In external mode we don't own a camera rig/controller, so we can't derive scale from "camera distance".
4930
+ * Instead, we:
4931
+ * 1) Find the point on the SVG plane that is currently under the screen center.
4932
+ * 2) Measure how much the screen position changes when moving 1 SVG unit in X/Y around that point.
4933
+ * 3) Convert that local plane→screen mapping into a single "zoom-like" scalar that is stable under tilt+rotate.
4934
+ *
4935
+ * This matches internal mode semantics better because the internal orbit target is also kept under screen center.
4936
+ * @param viewportSize Size of the viewport in drawing-buffer pixels
4937
+ * @returns Pixel-to-SVG scale factor (px → svg units)
4938
+ */
4939
+ pxToSvgScale(viewportSize) {
4940
+ const M = this.camera.projectionMatrix;
4941
+ const [viewportW, viewportH] = viewportSize;
4942
+ if (viewportW <= 0 || viewportH <= 0) return;
4943
+ const intersectionPoint = this.pickingSystem.intersectPlane(this.ndcCenter, this.camera, this.intersectionPoint);
4944
+ if (!intersectionPoint) return;
4945
+ const anchorX = intersectionPoint.x;
4946
+ const anchorY = intersectionPoint.y;
4947
+ const clip = this.clipPoint;
4948
+ const p0 = this.px0.set(viewportW * 0.5, viewportH * 0.5);
4949
+ const p1 = this.px1;
4950
+ const p2 = this.px2;
4951
+ const svgToPixels = (x, y, out) => {
4952
+ clip.set(x, y, 0, 1).applyMatrix4(M);
4953
+ if (clip.w === 0) return false;
4954
+ const ndcX = clip.x / clip.w;
4955
+ const ndcY = clip.y / clip.w;
4956
+ out.set((ndcX + 1) * 0.5 * viewportW, (1 - ndcY) * 0.5 * viewportH);
4957
+ return true;
4958
+ };
4959
+ const svgStep = 1;
4960
+ if (!svgToPixels(anchorX + svgStep, anchorY, p1)) return;
4961
+ if (!svgToPixels(anchorX, anchorY + svgStep, p2)) return;
4962
+ const pxDeltaPerSvgX = p1.sub(p0).divideScalar(svgStep);
4963
+ const pxDeltaPerSvgY = p2.sub(p0).divideScalar(svgStep);
4964
+ const pixelsSqPerSvgX = pxDeltaPerSvgX.dot(pxDeltaPerSvgX);
4965
+ const pixelsSqPerSvgY = pxDeltaPerSvgY.dot(pxDeltaPerSvgY);
4966
+ const pixelsSqCross = pxDeltaPerSvgX.dot(pxDeltaPerSvgY);
4967
+ const sumPixelsSq = pixelsSqPerSvgX + pixelsSqPerSvgY;
4968
+ const areaPixelsSq = pixelsSqPerSvgX * pixelsSqPerSvgY - pixelsSqCross * pixelsSqCross;
4969
+ const maxStretchDiscriminant = Math.max(0, sumPixelsSq * sumPixelsSq - 4 * areaPixelsSq);
4970
+ const maxPixelsSqPerSvg = 0.5 * (sumPixelsSq + Math.sqrt(maxStretchDiscriminant));
4971
+ const pxPerSvg = Math.sqrt(maxPixelsSqPerSvg);
4972
+ if (!Number.isFinite(pxPerSvg) || pxPerSvg <= 0) return;
4973
+ return 1 / pxPerSvg;
4974
+ }
4975
+ validateMatrix(matrix, name) {
4976
+ if (matrix.length !== 16) {
4977
+ console.warn(`[ViewportSystem.${name}]: Matrix must be 16 elements long`);
4978
+ return false;
4979
+ }
4980
+ return true;
4981
+ }
4982
+ }
4983
+ class PickingSystem {
4984
+ /** */
4985
+ constructor() {
4986
+ __publicField(this, "raycaster", new Raycaster());
4987
+ __publicField(this, "ndcPoint", new Vector2());
4988
+ __publicField(this, "viewboxPlane", new Plane(new Vector3(0, 0, 1), 0));
4989
+ this.raycaster.layers.set(INTERACTIVE_LAYER);
4990
+ }
4991
+ /**
4992
+ * Gets the objects intersected by the raycaster.
4993
+ * @param ndcCoords raycast point in NDC (normalized device coordinates)
4994
+ * @param scene {@link Scene} instance
4995
+ * @param camera {@link Camera} instance
4996
+ * @returns Array of {@link Intersection} instances
4997
+ */
4998
+ getIntersectedObjects(ndcCoords, scene, camera) {
4999
+ this.setRaycasterFromCamera(ndcCoords, camera);
5000
+ const intersections = this.raycaster.intersectObject(scene, true);
5001
+ return intersections.filter((i) => isVisible(i.object));
5002
+ }
5003
+ /**
5004
+ * Intersects the xy-plane with the raycaster.
5005
+ * @param ndcCoords raycast point in NDC (normalized device coordinates
5006
+ * @param camera {@link Camera} instance
5007
+ * @param out Output vector
5008
+ * @returns Intersection point in world space or null if no intersection.
5009
+ */
5010
+ intersectPlane(ndcCoords, camera, out) {
5011
+ this.setRaycasterFromCamera(ndcCoords, camera);
5012
+ return this.raycaster.ray.intersectPlane(this.viewboxPlane, out) ?? void 0;
5013
+ }
5014
+ setRaycasterFromCamera(ndcCoords, camera) {
5015
+ if (camera.isPerspectiveCamera || camera.isOrthographicCamera) {
5016
+ this.ndcPoint.set(ndcCoords.x, ndcCoords.y);
5017
+ this.raycaster.setFromCamera(this.ndcPoint, camera);
5018
+ } else {
5019
+ this.raycaster.ray.origin.set(0, 0, 0).unproject(camera);
5020
+ this.raycaster.ray.direction.set(ndcCoords.x, ndcCoords.y, 1).unproject(camera).sub(this.raycaster.ray.origin).normalize();
5021
+ }
4851
5022
  }
4852
5023
  }
4853
5024
  class SceneSystem {
@@ -4857,29 +5028,22 @@ class SceneSystem {
4857
5028
  constructor(renderer) {
4858
5029
  /** {@link Scene} instance */
4859
5030
  __publicField(this, "scene");
4860
- /** World matrix - SVGWorld transform */
5031
+ /** World matrix - modelworld transform */
4861
5032
  __publicField(this, "worldMatrix", new Matrix4());
4862
- /** Inverse world matrix - WorldSVG transform */
5033
+ /** Inverse world matrix - worldmodel transform */
4863
5034
  __publicField(this, "inverseWorldMatrix", new Matrix4());
5035
+ __publicField(this, "tempVector3", new Vector3());
4864
5036
  __publicField(this, "translationMatrix", new Matrix4());
4865
5037
  __publicField(this, "scaleMatrix", new Matrix4());
4866
- __publicField(this, "scaleVector", new Vector3());
4867
5038
  __publicField(this, "visibleRectOffsetMatrix", new Matrix4());
4868
5039
  __publicField(this, "viewbox");
4869
5040
  this.renderer = renderer;
4870
5041
  this.scene = new Scene();
4871
5042
  this.scene.matrixAutoUpdate = false;
4872
5043
  }
4873
- /** Scene scale factor (SVG to pixel) */
5044
+ /** Scene scale factor (model space to world space) */
4874
5045
  get scaleFactor() {
4875
- this.scaleVector.setFromMatrixScale(this.scene.matrix);
4876
- if (this.scaleVector.z === 1) {
4877
- return this.scaleVector.x;
4878
- } else {
4879
- const perspectiveW = this.scene.matrix.elements[15];
4880
- const halfViewportWidth = this.renderer.size[0] / 2;
4881
- return halfViewportWidth * this.scaleVector.x / perspectiveW;
4882
- }
5046
+ return this.scene.matrix.elements[0];
4883
5047
  }
4884
5048
  /**
4885
5049
  * Initializes the scene with the given SVG viewbox.
@@ -4889,28 +5053,48 @@ class SceneSystem {
4889
5053
  this.viewbox = viewbox;
4890
5054
  this.updateScene();
4891
5055
  }
4892
- /**
4893
- * Updates the scene transform when the renderer size changes.
4894
- */
5056
+ /** Updates the scene transform from the current viewbox and renderer size. */
4895
5057
  updateScene() {
4896
5058
  if (!this.viewbox) return;
5059
+ this.composeMatrices(this.viewbox);
5060
+ }
5061
+ /**
5062
+ * Converts a point from model coordinates to world coordinates.
5063
+ * @param modelCoords Point in model coordinates
5064
+ * @param out Output vector
5065
+ * @returns Point in world coordinates
5066
+ */
5067
+ modelToWorld(modelCoords, out) {
5068
+ const worldPoint = this.tempVector3.set(modelCoords.x, modelCoords.y, 0).applyMatrix4(this.worldMatrix);
5069
+ out.set(worldPoint.x, worldPoint.y, 0);
5070
+ return out;
5071
+ }
5072
+ /**
5073
+ * Converts a point from world coordinates to model coordinates. Z axis is ignored.
5074
+ * @param worldCoords Point in world coordinates
5075
+ * @param out Output vector
5076
+ * @returns Point in SVG coordinates
5077
+ */
5078
+ worldToModel(worldCoords, out) {
5079
+ const modelPoint = this.tempVector3.copy(worldCoords).applyMatrix4(this.inverseWorldMatrix);
5080
+ out.set(modelPoint.x, modelPoint.y);
5081
+ return out;
5082
+ }
5083
+ composeMatrices(viewbox) {
4897
5084
  const dpr = this.renderer.context.getPixelRatio();
4898
5085
  const visibleRect = this.renderer.visibleRect;
4899
- const [viewBoxWidth, viewBoxHeight] = this.viewbox.size;
5086
+ const [viewBoxWidth, viewBoxHeight] = viewbox.size;
4900
5087
  const [visibleRectWidth, visibleRectHeight] = (visibleRect == null ? void 0 : visibleRect.size.clone().multiplyScalar(dpr)) ?? this.renderer.size;
4901
5088
  const scaleFactor = Math.min(visibleRectWidth / viewBoxWidth, visibleRectHeight / viewBoxHeight);
4902
- const [centerX, centerY] = this.viewbox.center;
5089
+ const [centerX, centerY] = viewbox.center;
4903
5090
  this.translationMatrix.makeTranslation(-centerX, -centerY, 0);
4904
5091
  this.scaleMatrix.makeScale(scaleFactor, scaleFactor, 1);
4905
5092
  if (visibleRect) {
4906
5093
  const visibleRectCenter = visibleRect.center.clone().multiplyScalar(dpr);
4907
- const canvasCenter = new Vector2(...this.renderer.size).multiplyScalar(0.5);
5094
+ const canvasCenter = { x: this.renderer.size[0] / 2, y: this.renderer.size[1] / 2 };
4908
5095
  const offset = visibleRectCenter.sub(canvasCenter);
4909
5096
  this.visibleRectOffsetMatrix.makeTranslation(offset.x, offset.y, 0);
4910
5097
  }
4911
- this.composeMatrices();
4912
- }
4913
- composeMatrices() {
4914
5098
  this.worldMatrix.copy(this.translationMatrix).premultiply(this.scaleMatrix);
4915
5099
  if (this.renderer.visibleRect) this.worldMatrix.premultiply(this.visibleRectOffsetMatrix);
4916
5100
  this.scene.matrix.copy(this.worldMatrix);
@@ -4924,19 +5108,18 @@ class ViewportSystem {
4924
5108
  * @param eventSystem {@link EventSystem} instance
4925
5109
  */
4926
5110
  constructor(renderer, eventSystem) {
5111
+ __publicField(this, "pickingSystem");
5112
+ __publicField(this, "externalSystem");
4927
5113
  __publicField(this, "sceneSystem");
4928
5114
  __publicField(this, "cameraSystem");
4929
- __publicField(this, "raycaster", new Raycaster());
4930
- __publicField(this, "intersectionPoint", new Vector3());
4931
- __publicField(this, "viewboxPlane", new Plane(new Vector3(0, 0, 1), 0));
4932
5115
  __publicField(this, "pxToSvgScaleThreshold", 1e-4);
4933
5116
  __publicField(this, "prevPxToSvgScale");
4934
- __publicField(this, "externalStaticTransformMatrix", new Matrix4());
4935
5117
  this.renderer = renderer;
4936
5118
  this.eventSystem = eventSystem;
5119
+ this.pickingSystem = new PickingSystem();
4937
5120
  this.sceneSystem = new SceneSystem(renderer);
4938
5121
  this.cameraSystem = new CameraSystem(renderer);
4939
- this.raycaster.layers.set(INTERACTIVE_LAYER);
5122
+ this.externalSystem = new ExternalSystem(this.pickingSystem);
4940
5123
  }
4941
5124
  /** {@link Scene} instance */
4942
5125
  get scene() {
@@ -4944,10 +5127,10 @@ class ViewportSystem {
4944
5127
  }
4945
5128
  /** Current {@link Camera} instance */
4946
5129
  get camera() {
4947
- return this.cameraSystem.currentCamera;
5130
+ return this.renderer.isExternalMode ? this.externalSystem.camera : this.cameraSystem.camera;
4948
5131
  }
4949
5132
  /** {@link CameraController} instance */
4950
- get cameraController() {
5133
+ get controller() {
4951
5134
  return this.cameraSystem.controller;
4952
5135
  }
4953
5136
  /** Current camera zoom factor. */
@@ -4960,14 +5143,22 @@ class ViewportSystem {
4960
5143
  }
4961
5144
  /** Pixel to SVG scale factor */
4962
5145
  get pxToSvgScale() {
4963
- return 1 / (this.scaleFactor * this.zoomFactor);
5146
+ return this.renderer.isExternalMode ? this.externalSystem.pxToSvgScale(this.renderer.size) ?? this.prevPxToSvgScale ?? 1 : 1 / (this.scaleFactor * this.zoomFactor);
5147
+ }
5148
+ /**
5149
+ * Get bearing angle between current camera orientation and true north (in radians).
5150
+ * Angle is in range [0, 2π), going clockwise from north.
5151
+ */
5152
+ get bearing() {
5153
+ const tau = Math.PI * 2;
5154
+ return MathUtils.euclideanModulo(-this.controller.azimuthAngle, tau);
4964
5155
  }
4965
5156
  /**
4966
5157
  * Initializes the viewport and zoom bounds with the given scene definition.
4967
5158
  * @param sceneDef {@link SceneDef} scene definition
4968
5159
  */
4969
5160
  initViewport(sceneDef) {
4970
- this.sceneSystem.initScene(sceneDef.viewbox);
5161
+ if (!this.renderer.isExternalMode) this.sceneSystem.initScene(sceneDef.viewbox);
4971
5162
  this.cameraSystem.initCamera([0.1, sceneDef.viewbox.size.width > 1e5 ? 100 : 35]);
4972
5163
  }
4973
5164
  /** Updates the viewport when the renderer size changes. */
@@ -4987,71 +5178,99 @@ class ViewportSystem {
4987
5178
  }
4988
5179
  /**
4989
5180
  * Gets the objects intersected by the raycaster.
4990
- * @param normalizedCoords raycast point in NDC (normalized device coordinates
5181
+ * @param ndcCoords raycast point in NDC (normalized device coordinates)
4991
5182
  * @returns Array of {@link Intersection} instances
4992
5183
  */
4993
- getIntersectedObjects(normalizedCoords) {
4994
- const { scene, camera } = this;
4995
- this.raycaster.setFromCamera(normalizedCoords, camera);
4996
- const intersections = this.raycaster.intersectObject(scene, true).filter((i) => isVisible(i.object));
4997
- return intersections;
5184
+ getIntersectedObjects(ndcCoords) {
5185
+ return this.pickingSystem.getIntersectedObjects(ndcCoords, this.scene, this.camera);
4998
5186
  }
4999
5187
  /**
5000
- * Converts a point from SVG coordinates to world coordinates.
5001
- * @param svgCoords Point in SVG coordinates
5188
+ * Converts a point from model coordinates to world coordinates.
5189
+ * @param modelCoords Point in model coordinates
5190
+ * @param out Optional output vector
5002
5191
  * @returns Point in world coordinates
5003
5192
  */
5004
- svgToWorld(svgCoords) {
5005
- const svg3D = new Vector3(...svgCoords, 0);
5006
- svg3D.applyMatrix4(this.sceneSystem.worldMatrix);
5007
- return new Vector2(svg3D.x, svg3D.y);
5193
+ modelToWorld(modelCoords, out = new Vector3()) {
5194
+ return this.sceneSystem.modelToWorld(modelCoords, out);
5008
5195
  }
5009
5196
  /**
5010
- * Converts a point from world coordinates to SVG coordinates. Z axis is ignored.
5197
+ * Converts a point from world coordinates to model coordinates. Z axis is ignored.
5011
5198
  * @param worldCoords Point in world coordinates
5012
- * @returns Point in SVG coordinates
5199
+ * @param out Optional output vector
5200
+ * @returns Point in model coordinates
5013
5201
  */
5014
- worldToSvg(worldCoords) {
5015
- const svgCoords = worldCoords.clone().applyMatrix4(this.sceneSystem.inverseWorldMatrix);
5016
- return new Vector2(svgCoords.x, svgCoords.y);
5202
+ worldToModel(worldCoords, out = new Vector2()) {
5203
+ return this.sceneSystem.worldToModel(worldCoords, out);
5017
5204
  }
5018
5205
  /**
5019
- * Converts a point from screen coordinates to the given coordinate space.
5020
- * @param space Space to convert to (either "svg" or "world")
5021
- * @param normalizedCoords Point in NDC (normalized device coordinates)
5022
- * @returns Point in the given space
5206
+ * Converts a point from screen coordinates to world space.
5207
+ * @param ndcCoords Point in NDC (normalized device coordinates)
5208
+ * @param out Optional output vector
5209
+ * @returns Point in world space
5023
5210
  */
5024
- screenTo(space, normalizedCoords) {
5025
- this.raycaster.setFromCamera(normalizedCoords, this.camera);
5026
- this.raycaster.ray.intersectPlane(this.viewboxPlane, this.intersectionPoint);
5027
- if (space === "svg") this.intersectionPoint.applyMatrix4(this.sceneSystem.inverseWorldMatrix);
5028
- return { x: this.intersectionPoint.x, y: this.intersectionPoint.y };
5211
+ ndcToWorld(ndcCoords, out = new Vector3()) {
5212
+ return this.pickingSystem.intersectPlane(ndcCoords, this.camera, out);
5029
5213
  }
5030
5214
  /**
5031
- * Calculates the camera distance from the scene's plane for a given zoom factor.
5032
- * @param zoomFactor Zoom factor
5033
- * @returns Corresponding camera distance on the Z axis
5215
+ * Convert canvas coordinates (relative to the canvas's top left corner)
5216
+ * to NDC (normalized device coordinates).
5217
+ * @param point object defining the coordinates relative to the canvas's top left corner
5218
+ * @param out Optional output vector
5219
+ * @returns Point in NDC space
5034
5220
  */
5035
- zoomFactorToDistance(zoomFactor) {
5036
- return this.cameraSystem.zoomFactorToDistance(zoomFactor);
5221
+ canvasToNDC(point, out = new Vector2()) {
5222
+ const dpr = this.renderer.context.getPixelRatio();
5223
+ const [width, height] = [this.renderer.size[0] / dpr, this.renderer.size[1] / dpr];
5224
+ const uv = [point.x / width, point.y / height];
5225
+ const ndc = [uv[0] * 2 - 1, -uv[1] * 2 + 1];
5226
+ return out.set(MathUtils.clamp(ndc[0], -1, 1), MathUtils.clamp(ndc[1], -1, 1));
5227
+ }
5228
+ /**
5229
+ * Convert canvas coordinates (CSS pixels, relative to canvas top-left) to SVG coordinates.
5230
+ * @param point point in canvas space (CSS pixels)
5231
+ * @returns point in SVG coordinates or undefined if point is outside the SVG plane
5232
+ */
5233
+ canvasToSvg(point) {
5234
+ const vec2 = new Vector2();
5235
+ const vec3 = new Vector3();
5236
+ const ndcPoint = this.canvasToNDC(point, vec2);
5237
+ const worldPoint = this.ndcToWorld(ndcPoint, vec3);
5238
+ if (!worldPoint) return;
5239
+ return this.worldToModel(worldPoint, vec2);
5037
5240
  }
5038
5241
  /**
5039
- * Sets the external transform matrix.
5242
+ * Set static part of an svg -> px transform matrix
5040
5243
  * @param staticTransformMatrix static transform matrix to apply to the scene
5041
5244
  */
5042
- setExternalTransform(staticTransformMatrix) {
5043
- this.cameraSystem.externalCamera = new Camera();
5044
- this.externalStaticTransformMatrix.fromArray(staticTransformMatrix);
5245
+ setStaticTransform(staticTransformMatrix) {
5246
+ this.externalSystem.setStaticTransform(staticTransformMatrix);
5045
5247
  }
5046
5248
  /**
5047
- * Updates the external camera.
5048
- * @param dynamicTransformMatrix dynamic transform matrix to apply to the scene
5249
+ * Set dynamic part of an svg -> px transform matrix. Should be called every frame.
5250
+ * @param dynamicTransformMatrix dynamic transform matrix (changes every frame)
5049
5251
  */
5050
- updateExternalCamera(dynamicTransformMatrix) {
5051
- this.scene.matrix.fromArray(dynamicTransformMatrix).multiply(this.externalStaticTransformMatrix);
5052
- this.scene.matrixWorldNeedsUpdate = true;
5252
+ setDynamicTransform(dynamicTransformMatrix) {
5253
+ this.externalSystem.setDynamicTransform(dynamicTransformMatrix);
5254
+ }
5255
+ /**
5256
+ * Calculates the camera distance from the scene's plane for a given zoom factor.
5257
+ * @param zoomFactor Zoom factor
5258
+ * @returns Corresponding camera distance on the Z axis
5259
+ */
5260
+ zoomFactorToDistance(zoomFactor) {
5261
+ return this.cameraSystem.zoomFactorToDistance(zoomFactor);
5053
5262
  }
5054
5263
  }
5264
+ function asViewportAPI(viewportSystem) {
5265
+ return {
5266
+ canvasToSvg: viewportSystem.canvasToSvg.bind(viewportSystem),
5267
+ setStaticTransform: viewportSystem.setStaticTransform.bind(viewportSystem),
5268
+ setDynamicTransform: viewportSystem.setDynamicTransform.bind(viewportSystem)
5269
+ };
5270
+ }
5271
+ function eventToCanvas(event) {
5272
+ return { x: event.offsetX, y: event.offsetY };
5273
+ }
5055
5274
  class ControlsSystem {
5056
5275
  /**
5057
5276
  * @param renderer {@link Renderer} instance
@@ -5063,7 +5282,7 @@ class ControlsSystem {
5063
5282
  this.renderer = renderer;
5064
5283
  this.viewportSystem = viewportSystem;
5065
5284
  this.interactionsSystem = interactionsSystem;
5066
- this.controller = viewportSystem.cameraController;
5285
+ this.controller = viewportSystem.controller;
5067
5286
  }
5068
5287
  /** Gesture handlers for camera controls. */
5069
5288
  get handlers() {
@@ -5092,7 +5311,9 @@ class ControlsSystem {
5092
5311
  const dpr = this.renderer.context.getPixelRatio();
5093
5312
  const visibleRect = this.renderer.visibleRect;
5094
5313
  const bearingAngle = -this.controller.azimuthAngle;
5095
- const worldRect = new Rect(this.viewportSystem.svgToWorld(rect.min), this.viewportSystem.svgToWorld(rect.max));
5314
+ const worldMin = this.viewportSystem.modelToWorld(rect.min);
5315
+ const worldMax = this.viewportSystem.modelToWorld(rect.max);
5316
+ const worldRect = new Rect(worldMin, worldMax);
5096
5317
  const worldPolygon = Polygon.fromRect(worldRect).rotate(bearingAngle, worldRect.center);
5097
5318
  const xValues = worldPolygon.vertices.map((p) => p.x);
5098
5319
  const yValues = worldPolygon.vertices.map((p) => p.y);
@@ -5100,7 +5321,7 @@ class ControlsSystem {
5100
5321
  [Math.min(...xValues), Math.min(...yValues)],
5101
5322
  [Math.max(...xValues), Math.max(...yValues)]
5102
5323
  );
5103
- const targetRect = visibleRect ? new Rect(visibleRect.min.clone().multiplyScalar(dpr), visibleRect.max.clone().multiplyScalar(dpr)) : new Rect([0, 0], this.renderer.size);
5324
+ const targetRect = visibleRect ? new Rect(visibleRect.min.clone().multiplyScalar(dpr), visibleRect.max.clone().multiplyScalar(dpr)) : new Rect([0, 0], [...this.renderer.size]);
5104
5325
  if (paddingPercent) targetRect.addPadding(targetRect.size.x * paddingPercent, targetRect.size.y * paddingPercent);
5105
5326
  const zoomByWidth = targetRect.size.x / sourceRect.size.x;
5106
5327
  const zoomByHeight = targetRect.size.y / sourceRect.size.y;
@@ -5125,8 +5346,8 @@ class ControlsSystem {
5125
5346
  * @returns Promise that resolves when the pan animation completes
5126
5347
  */
5127
5348
  panBy(x, y, immediate) {
5128
- const svgOrigin = this.viewportSystem.svgToWorld(new Vector2(0, 0));
5129
- const svgOffset = this.viewportSystem.svgToWorld(new Vector2(x, y));
5349
+ const svgOrigin = this.viewportSystem.modelToWorld({ x: 0, y: 0 });
5350
+ const svgOffset = this.viewportSystem.modelToWorld({ x, y });
5130
5351
  const worldOffset = new Vector3(svgOffset.x - svgOrigin.x, svgOffset.y - svgOrigin.y, 0);
5131
5352
  const currentTarget = this.controller.getTarget(new Vector3());
5132
5353
  const newTarget = currentTarget.add(worldOffset);
@@ -5140,7 +5361,7 @@ class ControlsSystem {
5140
5361
  * @returns Promise that resolves when the pan animation completes
5141
5362
  */
5142
5363
  panTo(x, y, immediate) {
5143
- const worldCoords = this.viewportSystem.svgToWorld(new Vector2(x, y));
5364
+ const worldCoords = this.viewportSystem.modelToWorld({ x, y });
5144
5365
  return this.controller.moveTo(worldCoords.x, worldCoords.y, 0, !immediate);
5145
5366
  }
5146
5367
  /**
@@ -5238,8 +5459,8 @@ class ControlsSystem {
5238
5459
  this.controller.setBoundary(void 0);
5239
5460
  return;
5240
5461
  }
5241
- const worldMin = this.viewportSystem.svgToWorld(rect.min);
5242
- const worldMax = this.viewportSystem.svgToWorld(rect.max);
5462
+ const worldMin = this.viewportSystem.modelToWorld(rect.min);
5463
+ const worldMax = this.viewportSystem.modelToWorld(rect.max);
5243
5464
  const boundary = new Box3(new Vector3(worldMin.x, worldMin.y, 0), new Vector3(worldMax.x, worldMax.y, 0));
5244
5465
  this.controller.setBoundary(boundary);
5245
5466
  }
@@ -5249,9 +5470,9 @@ class ControlsSystem {
5249
5470
  */
5250
5471
  getCameraState() {
5251
5472
  const target = this.controller.getTarget(new Vector3());
5252
- const center = this.viewportSystem.worldToSvg(target);
5473
+ const center = this.viewportSystem.worldToModel(target);
5253
5474
  const zoom = this.viewportSystem.zoomFactor;
5254
- const roll = this.interactionsSystem.bearing * RAD2DEG;
5475
+ const roll = this.viewportSystem.bearing * RAD2DEG;
5255
5476
  const pitch = this.controller.polarAngle * RAD2DEG;
5256
5477
  const ptScale = this.viewportSystem.pxToSvgScale;
5257
5478
  return { center, zoom, roll, pitch, ptScale };
@@ -5348,21 +5569,6 @@ function asEventAPI(system) {
5348
5569
  clear: system.clear.bind(system)
5349
5570
  };
5350
5571
  }
5351
- function clientToCanvas(event, domElement, target) {
5352
- target = target ?? new Vector2();
5353
- const { left, top } = domElement.getBoundingClientRect();
5354
- const clientX = "clientX" in event ? event.clientX : event.x;
5355
- const clientY = "clientY" in event ? event.clientY : event.y;
5356
- target.set(clientX - left, clientY - top);
5357
- return target;
5358
- }
5359
- function canvasToNDC(coordinates, canvas, target) {
5360
- target = target ?? new Vector2();
5361
- const { width, height } = canvas.getBoundingClientRect();
5362
- const uv = [coordinates.x / width, coordinates.y / height];
5363
- target.set(uv[0] * 2 - 1, -uv[1] * 2 + 1);
5364
- return target;
5365
- }
5366
5572
  class Handler {
5367
5573
  /**
5368
5574
  * @param viewportSystem The viewport system instance
@@ -5377,7 +5583,7 @@ class Handler {
5377
5583
  this.viewportSystem = viewportSystem;
5378
5584
  this.domElement = domElement;
5379
5585
  this.eventManager = eventManager;
5380
- this.controller = viewportSystem.cameraController;
5586
+ this.controller = viewportSystem.controller;
5381
5587
  }
5382
5588
  /**
5383
5589
  * Per-frame update for this handler.
@@ -5588,13 +5794,15 @@ class PitchHandler extends Handler {
5588
5794
  __publicField(this, "isValid");
5589
5795
  __publicField(this, "firstMove");
5590
5796
  __publicField(this, "lastPoints");
5797
+ __publicField(this, "p0", new Vector2());
5798
+ __publicField(this, "p1", new Vector2());
5591
5799
  __publicField(this, "prevTwoFingerAction");
5592
5800
  __publicField(this, "onPitchStart", (e) => {
5593
5801
  const pointers = e.pointers.sort((a, b) => a.pointerId - b.pointerId);
5594
- const p0 = clientToCanvas(pointers[0], this.domElement);
5595
- const p1 = clientToCanvas(pointers[1], this.domElement);
5596
- this.lastPoints = [p0, p1];
5597
- if (this.isVertical(p0.clone().sub(p1))) {
5802
+ this.p0.copy(eventToCanvas(pointers[0]));
5803
+ this.p1.copy(eventToCanvas(pointers[1]));
5804
+ this.lastPoints = [new Vector2().copy(this.p0), new Vector2().copy(this.p1)];
5805
+ if (this.isVertical(this.p0.sub(this.p1))) {
5598
5806
  this.isValid = false;
5599
5807
  }
5600
5808
  });
@@ -5609,18 +5817,19 @@ class PitchHandler extends Handler {
5609
5817
  const lastPoints = this.lastPoints;
5610
5818
  if (!lastPoints) return;
5611
5819
  const pointers = e.pointers.sort((a, b) => a.pointerId - b.pointerId);
5612
- const p0 = clientToCanvas(pointers[0], this.domElement);
5613
- const p1 = clientToCanvas(pointers[1], this.domElement);
5614
- const vectorA = p0.clone().sub(lastPoints[0]);
5615
- const vectorB = p1.clone().sub(lastPoints[1]);
5616
- this.isValid = this.gestureBeginsVertically(vectorA, vectorB, e.timeStamp);
5820
+ this.p0.copy(eventToCanvas(pointers[0]));
5821
+ this.p1.copy(eventToCanvas(pointers[1]));
5822
+ this.p0.sub(lastPoints[0]);
5823
+ this.p1.sub(lastPoints[1]);
5824
+ this.isValid = this.gestureBeginsVertically(this.p0, this.p1, e.timeStamp);
5617
5825
  if (!this.isValid) return;
5618
5826
  if (this.prevTwoFingerAction === void 0) {
5619
5827
  this.prevTwoFingerAction = this.controller.touches.two;
5620
5828
  this.controller.touches.two = CameraController.ACTION.NONE;
5621
5829
  }
5622
- this.lastPoints = [p0, p1];
5623
- const yDeltaAverage = (vectorA.y + vectorB.y) / 2;
5830
+ lastPoints[0].add(this.p0);
5831
+ lastPoints[1].add(this.p1);
5832
+ const yDeltaAverage = (this.p0.y + this.p1.y) / 2;
5624
5833
  const degreesPerPixelMoved = -0.5;
5625
5834
  const deltaAngle = yDeltaAverage * degreesPerPixelMoved * DEG2RAD$1;
5626
5835
  void this.controller.rotatePolarTo(this.controller.polarAngle + deltaAngle, false);
@@ -5665,12 +5874,10 @@ class PitchHandler extends Handler {
5665
5874
  updatePolarAngles() {
5666
5875
  this.controller.minPolarAngle = this.minPitch * DEG2RAD$1;
5667
5876
  this.controller.maxPolarAngle = this.maxPitch * DEG2RAD$1;
5668
- if (this.controller.polarAngle < this.controller.minPolarAngle) {
5877
+ if (this.controller.polarAngle < this.controller.minPolarAngle)
5669
5878
  void this.controller.rotatePolarTo(this.controller.minPolarAngle, false);
5670
- }
5671
- if (this.controller.polarAngle > this.controller.maxPolarAngle) {
5879
+ if (this.controller.polarAngle > this.controller.maxPolarAngle)
5672
5880
  void this.controller.rotatePolarTo(this.controller.maxPolarAngle, false);
5673
- }
5674
5881
  }
5675
5882
  gestureBeginsVertically(vectorA, vectorB, timeStamp) {
5676
5883
  if (this.isValid !== void 0) return this.isValid;
@@ -5702,35 +5909,36 @@ class RollHandler extends Handler {
5702
5909
  __publicField(this, "rotationThreshold", 25);
5703
5910
  // Threshold tracking (Mapbox-style)
5704
5911
  __publicField(this, "startVector");
5705
- __publicField(this, "vector");
5912
+ __publicField(this, "vector", new Vector2());
5913
+ __publicField(this, "p0", new Vector2());
5914
+ __publicField(this, "p1", new Vector2());
5706
5915
  __publicField(this, "minDiameter", 0);
5707
5916
  __publicField(this, "prevAngle", 0);
5708
5917
  // Camera and pivot vectors
5918
+ __publicField(this, "pivotNDC", new Vector2());
5709
5919
  __publicField(this, "pivotWorld", new Vector3());
5710
5920
  __publicField(this, "targetWorld", new Vector3());
5711
5921
  __publicField(this, "cameraPosition", new Vector3());
5712
5922
  __publicField(this, "cameraForward", new Vector3());
5713
5923
  __publicField(this, "rotationMatrix", new Matrix4());
5714
5924
  __publicField(this, "onRotateStart", (e) => {
5715
- console.log("onRotateStart");
5716
5925
  const pointers = e.pointers;
5717
- const p0 = clientToCanvas(pointers[0], this.domElement);
5718
- const p1 = clientToCanvas(pointers[1], this.domElement);
5719
- this.startVector = p0.sub(p1);
5926
+ this.p0.copy(eventToCanvas(pointers[0]));
5927
+ this.p1.copy(eventToCanvas(pointers[1]));
5928
+ this.startVector = new Vector2().copy(this.p0).sub(this.p1);
5720
5929
  this.minDiameter = this.startVector.length();
5721
5930
  });
5722
5931
  __publicField(this, "onRotateEnd", () => {
5723
5932
  this.isRolling = false;
5724
5933
  this.startVector = void 0;
5725
- this.vector = void 0;
5726
5934
  this.minDiameter = 0;
5727
5935
  });
5728
5936
  __publicField(this, "onRotate", (e) => {
5729
5937
  const pointers = e.pointers;
5730
5938
  if (!this.isRolling) {
5731
- const p0 = clientToCanvas(pointers[0], this.domElement);
5732
- const p1 = clientToCanvas(pointers[1], this.domElement);
5733
- this.vector = p0.sub(p1);
5939
+ this.p0.copy(eventToCanvas(pointers[0]));
5940
+ this.p1.copy(eventToCanvas(pointers[1]));
5941
+ this.vector.copy(this.p0).sub(this.p1);
5734
5942
  if (this.isBelowThreshold(this.vector)) return;
5735
5943
  this.isRolling = true;
5736
5944
  this.prevAngle = e.rotation;
@@ -5738,7 +5946,7 @@ class RollHandler extends Handler {
5738
5946
  const deltaAngle = (e.rotation - this.prevAngle) * -DEG2RAD$1;
5739
5947
  this.prevAngle = e.rotation;
5740
5948
  if (Math.abs(deltaAngle) < 1e-3) return;
5741
- this.setPivot(e);
5949
+ if (!this.setPivot(e)) return;
5742
5950
  this.rotationMatrix.makeRotationZ(deltaAngle);
5743
5951
  this.cameraPosition.sub(this.pivotWorld).applyMatrix4(this.rotationMatrix).add(this.pivotWorld);
5744
5952
  this.cameraForward.applyMatrix4(this.rotationMatrix);
@@ -5786,13 +5994,12 @@ class RollHandler extends Handler {
5786
5994
  this.eventManager.off("rotateend", this.onRotateEnd);
5787
5995
  }
5788
5996
  setPivot(e) {
5789
- const pivotScreen = e.center;
5790
- const pivotNDC = canvasToNDC(pivotScreen, this.domElement);
5791
- const pivotWorld2D = this.viewportSystem.screenTo("world", pivotNDC);
5792
- this.pivotWorld.set(pivotWorld2D.x, pivotWorld2D.y, 0);
5997
+ this.viewportSystem.canvasToNDC(e.offsetCenter, this.pivotNDC);
5998
+ if (!this.viewportSystem.ndcToWorld(this.pivotNDC, this.pivotWorld)) return false;
5793
5999
  this.controller.getPosition(this.cameraPosition);
5794
6000
  this.controller.getTarget(this.targetWorld);
5795
6001
  this.cameraForward.copy(this.targetWorld).sub(this.cameraPosition);
6002
+ return true;
5796
6003
  }
5797
6004
  /**
5798
6005
  * Check if rotation is below threshold (Mapbox-style).
@@ -5812,7 +6019,6 @@ class RollHandler extends Handler {
5812
6019
  const startVector = this.startVector;
5813
6020
  if (!startVector) return false;
5814
6021
  const bearingDeltaSinceStart = this.getBearingDelta(vector, startVector);
5815
- console.log("bearingDeltaSinceStart", vector, startVector);
5816
6022
  return Math.abs(bearingDeltaSinceStart) < threshold;
5817
6023
  }
5818
6024
  /**
@@ -5824,16 +6030,6 @@ class RollHandler extends Handler {
5824
6030
  getBearingDelta(a, b) {
5825
6031
  return a.angleTo(b) * RAD2DEG;
5826
6032
  }
5827
- /**
5828
- * Normalize angle to be between -π and π
5829
- * @param angle Angle in radians
5830
- * @returns Normalized angle in radians
5831
- */
5832
- normalizeAngle(angle) {
5833
- while (angle > Math.PI) angle -= 2 * Math.PI;
5834
- while (angle < -Math.PI) angle += 2 * Math.PI;
5835
- return angle;
5836
- }
5837
6033
  }
5838
6034
  class ZoomHandler extends Handler {
5839
6035
  reset(enableTransition = true) {
@@ -5868,12 +6064,16 @@ class InteractionsSystem {
5868
6064
  __publicField(this, "handlers");
5869
6065
  __publicField(this, "handlerArray");
5870
6066
  __publicField(this, "canvas");
5871
- __publicField(this, "mousePointer", new Vector2());
5872
6067
  __publicField(this, "eventManager");
6068
+ __publicField(this, "mousePointerNDC", new Vector2());
6069
+ __publicField(this, "mousePointerWorld", new Vector3());
6070
+ __publicField(this, "mousePointerModel", new Vector2());
6071
+ __publicField(this, "canvasListeners");
5873
6072
  __publicField(this, "dragStart");
5874
6073
  __publicField(this, "dragThreshold", 15);
5875
6074
  __publicField(this, "isDragging", false);
5876
6075
  __publicField(this, "prevBearing", 0);
6076
+ this.renderer = renderer;
5877
6077
  this.events = events;
5878
6078
  this.viewportSystem = viewportSystem;
5879
6079
  this.layerSystem = layerSystem;
@@ -5881,8 +6081,6 @@ class InteractionsSystem {
5881
6081
  this.eventManager = new EventManager(this.canvas, {
5882
6082
  recognizers: [Rotate, [Pan, { event: "pitch", pointers: 2 }, "rotate"]]
5883
6083
  });
5884
- this.configureCameraControls();
5885
- this.attachCanvasListeners();
5886
6084
  const handlers = {
5887
6085
  pan: new PanHandler(viewportSystem, this.canvas, this.eventManager),
5888
6086
  zoom: new ZoomHandler(viewportSystem, this.canvas, this.eventManager),
@@ -5891,17 +6089,32 @@ class InteractionsSystem {
5891
6089
  };
5892
6090
  this.handlers = handlers;
5893
6091
  this.handlerArray = Object.values(handlers);
5894
- this.handlers.pan.enable();
5895
- this.handlers.zoom.enable();
5896
6092
  }
5897
6093
  /**
5898
- * Get bearing angle between current camera orientation and true north (in radians).
5899
- * Angle is in range [0, 2π), going clockwise from north.
6094
+ * Initializes the interactions system and attaches event listeners.
6095
+ * Should be called once as part of renderer initialization.
5900
6096
  */
5901
- // TODO: Move somewhere else
5902
- get bearing() {
5903
- const tau = Math.PI * 2;
5904
- return MathUtils.euclideanModulo(-this.viewportSystem.cameraController.azimuthAngle, tau);
6097
+ init() {
6098
+ this.attachCanvasListeners();
6099
+ if (!this.renderer.isExternalMode) {
6100
+ const controller = this.viewportSystem.controller;
6101
+ controller.connect(this.canvas);
6102
+ controller.addEventListener("transitionstart", () => {
6103
+ this.events.emit("navigation:change");
6104
+ });
6105
+ this.handlers.pan.enable();
6106
+ this.handlers.zoom.enable();
6107
+ }
6108
+ }
6109
+ /**
6110
+ * Disposes the interactions system.
6111
+ * WARNING: This method is final and cannot be undone. To re-enable interactions, create a new renderer instance.
6112
+ */
6113
+ dispose() {
6114
+ for (const handler of this.handlerArray) handler.disable();
6115
+ this.viewportSystem.controller.disconnect();
6116
+ this.detachCanvasListeners();
6117
+ this.eventManager.destroy();
5905
6118
  }
5906
6119
  /**
5907
6120
  * Update camera position and directions.
@@ -5910,77 +6123,67 @@ class InteractionsSystem {
5910
6123
  * @returns true if re-rendering is needed
5911
6124
  */
5912
6125
  updateControls(delta) {
5913
- let needsUpdate = this.viewportSystem.cameraController.update(delta);
6126
+ let needsUpdate = this.viewportSystem.controller.update(delta);
5914
6127
  for (const handler of this.handlerArray) {
5915
6128
  if (handler.isEnabled()) {
5916
6129
  needsUpdate = handler.update(delta) || needsUpdate;
5917
6130
  }
5918
6131
  }
5919
- if (this.bearing !== this.prevBearing) {
5920
- this.prevBearing = this.bearing;
5921
- this.events.emit("navigation:roll", this.bearing);
6132
+ if (this.viewportSystem.bearing !== this.prevBearing) {
6133
+ this.prevBearing = this.viewportSystem.bearing;
6134
+ this.events.emit("navigation:roll", this.viewportSystem.bearing);
5922
6135
  }
5923
6136
  return needsUpdate;
5924
6137
  }
5925
- /** Disconnect the interactions system. */
5926
- disconnect() {
5927
- this.viewportSystem.cameraController.disconnect();
5928
- for (const handler of this.handlerArray) {
5929
- handler.disable();
5930
- }
5931
- }
5932
- configureCameraControls() {
5933
- const controller = this.viewportSystem.cameraController;
5934
- controller.draggingSmoothTime = 0;
5935
- controller.dollyToCursor = true;
5936
- controller.mouseButtons = {
5937
- left: CameraController.ACTION.NONE,
5938
- middle: CameraController.ACTION.NONE,
5939
- right: CameraController.ACTION.NONE,
5940
- wheel: CameraController.ACTION.NONE
5941
- };
5942
- controller.touches = {
5943
- one: CameraController.ACTION.NONE,
5944
- two: CameraController.ACTION.NONE,
5945
- three: CameraController.ACTION.NONE
5946
- };
5947
- controller.connect(this.canvas);
5948
- controller.addEventListener("transitionstart", () => {
5949
- this.events.emit("navigation:change");
5950
- });
5951
- }
5952
6138
  attachCanvasListeners() {
5953
- this.canvas.addEventListener("pointerdown", (event) => {
6139
+ if (this.canvasListeners) return;
6140
+ const pointerdown = (event) => {
5954
6141
  this.isDragging = false;
5955
6142
  this.dragStart = { x: event.offsetX, y: event.offsetY };
5956
- });
5957
- this.canvas.addEventListener("pointerup", (event) => {
6143
+ };
6144
+ const pointerup = (event) => {
5958
6145
  if (!this.dragStart) return;
5959
6146
  const dX = event.offsetX - this.dragStart.x;
5960
6147
  const dY = event.offsetY - this.dragStart.y;
5961
6148
  this.dragStart = void 0;
5962
6149
  if (dX * dX + dY * dY > this.dragThreshold) this.isDragging = true;
5963
- });
6150
+ };
5964
6151
  const mouseEventsMap = {
5965
6152
  mousemove: "pointer:move",
5966
6153
  mouseout: "pointer:out",
5967
6154
  click: "pointer:click"
5968
6155
  };
5969
6156
  const mouseEventKeys = Object.keys(mouseEventsMap);
5970
- mouseEventKeys.forEach((type) => {
5971
- this.canvas.addEventListener(type, (event) => {
5972
- const eventType = mouseEventsMap[type];
5973
- const isDragging = type === "click" && this.isDragging;
5974
- const hasListeners = this.events.hasListeners(eventType);
5975
- if (isDragging || !hasListeners) return;
5976
- clientToCanvas(event, this.canvas, this.mousePointer);
5977
- canvasToNDC(this.mousePointer, this.canvas, this.mousePointer);
5978
- const intersections = this.viewportSystem.getIntersectedObjects(this.mousePointer);
5979
- const point = this.viewportSystem.screenTo("svg", this.mousePointer);
5980
- const defs = this.layerSystem.getIntersectedDefs(intersections);
5981
- this.events.emit(eventType, { event, point, defs });
5982
- });
5983
- });
6157
+ const sharedMouseHandler = (type, event) => {
6158
+ const eventType = mouseEventsMap[type];
6159
+ const isDragging = type === "click" && this.isDragging;
6160
+ const hasListeners = this.events.hasListeners(eventType);
6161
+ if (isDragging || !hasListeners) return;
6162
+ const mousePointer = eventToCanvas(event);
6163
+ this.viewportSystem.canvasToNDC(mousePointer, this.mousePointerNDC);
6164
+ if (!this.viewportSystem.ndcToWorld(this.mousePointerNDC, this.mousePointerWorld)) return;
6165
+ const intersections = this.viewportSystem.getIntersectedObjects(this.mousePointerNDC);
6166
+ const point = this.viewportSystem.worldToModel(this.mousePointerWorld, this.mousePointerModel);
6167
+ const defs = this.layerSystem.getIntersectedDefs(intersections);
6168
+ this.events.emit(eventType, { event, point: { x: point.x, y: point.y }, defs });
6169
+ };
6170
+ const mousemove = (event) => sharedMouseHandler("mousemove", event);
6171
+ const mouseout = (event) => sharedMouseHandler("mouseout", event);
6172
+ const click = (event) => sharedMouseHandler("click", event);
6173
+ const canvasListeners = { pointerdown, pointerup, mousemove, mouseout, click };
6174
+ this.canvas.addEventListener("pointerdown", pointerdown);
6175
+ this.canvas.addEventListener("pointerup", pointerup);
6176
+ mouseEventKeys.forEach((type) => this.canvas.addEventListener(type, canvasListeners[type]));
6177
+ this.canvasListeners = canvasListeners;
6178
+ }
6179
+ detachCanvasListeners() {
6180
+ if (!this.canvasListeners) return;
6181
+ this.canvas.removeEventListener("pointerdown", this.canvasListeners.pointerdown);
6182
+ this.canvas.removeEventListener("pointerup", this.canvasListeners.pointerup);
6183
+ this.canvas.removeEventListener("mousemove", this.canvasListeners.mousemove);
6184
+ this.canvas.removeEventListener("mouseout", this.canvasListeners.mouseout);
6185
+ this.canvas.removeEventListener("click", this.canvasListeners.click);
6186
+ this.canvasListeners = void 0;
5984
6187
  }
5985
6188
  }
5986
6189
  class Renderer {
@@ -6000,13 +6203,17 @@ class Renderer {
6000
6203
  __publicField(this, "viewportSystem");
6001
6204
  __publicField(this, "interactionsSystem");
6002
6205
  __publicField(this, "controlsSystem");
6206
+ __publicField(this, "controlsAPI");
6207
+ __publicField(this, "eventsAPI");
6208
+ __publicField(this, "viewportAPI");
6003
6209
  __publicField(this, "clock");
6004
6210
  __publicField(this, "renderer");
6005
- __publicField(this, "viewport");
6211
+ __publicField(this, "visibleRectValue");
6212
+ __publicField(this, "memoryInfoExtension", null);
6213
+ __publicField(this, "memoryInfo");
6214
+ __publicField(this, "initialized", false);
6215
+ __publicField(this, "disposed", false);
6006
6216
  __publicField(this, "needsRedraw", true);
6007
- __publicField(this, "memoryInfoExtension");
6008
- __publicField(this, "memoryInfo", "");
6009
- var _a2, _b;
6010
6217
  const { canvas, gl, debugLog = false, ui } = opts;
6011
6218
  this.canvas = canvas;
6012
6219
  this.debugLog = debugLog;
@@ -6021,42 +6228,84 @@ class Renderer {
6021
6228
  this.renderer = new WebGLRenderer(rendererOptions);
6022
6229
  this.renderer.setSize(this.canvas.clientWidth, this.canvas.clientHeight, false);
6023
6230
  this.renderer.setPixelRatio(window.devicePixelRatio);
6231
+ this.renderer.autoClear = !this.isExternalMode;
6024
6232
  this.eventSystem = new EventSystem();
6025
6233
  this.viewportSystem = new ViewportSystem(this, this.eventSystem);
6026
6234
  this.layerSystem = new LayerSystem(this);
6027
6235
  this.interactionsSystem = new InteractionsSystem(this, this.eventSystem, this.viewportSystem, this.layerSystem);
6028
6236
  this.controlsSystem = new ControlsSystem(this, this.viewportSystem, this.interactionsSystem);
6029
- this.memoryInfoExtension = this.renderer.getContext().getExtension("GMAN_webgl_memory");
6030
- this.canvas.addEventListener("webglcontextlost", (e) => this.onContextLost(e), false);
6031
- this.canvas.addEventListener("webglcontextrestored", (e) => this.onContextRestored(e), false);
6032
- void ((_b = (_a2 = this.ui) == null ? void 0 : _a2.stats) == null ? void 0 : _b.init(this.renderer.getContext()));
6237
+ this.initContext(this.renderer.getContext());
6033
6238
  BatchedMesh.useMultiDraw = this.renderer.extensions.has("WEBGL_multi_draw");
6034
6239
  }
6035
6240
  /**
6036
6241
  * {@link ControlsAPI} instance for controlling the viewport
6037
6242
  */
6038
6243
  get controls() {
6039
- return asControlsAPI(this.controlsSystem);
6244
+ if (this.controlsAPI) return this.controlsAPI;
6245
+ const api = asControlsAPI(this.controlsSystem);
6246
+ const guard = (name) => this.assertInitialized(`controls.${name}`) && this.assertNotDisposed(`controls.${name}`) && this.assertNotExternalMode(`controls.${name}`);
6247
+ this.controlsAPI = {
6248
+ handlers: api.handlers,
6249
+ configure: api.configure,
6250
+ zoomBy: guardFn(guard, api.zoomBy, Promise.resolve()),
6251
+ zoomTo: guardFn(guard, api.zoomTo, Promise.resolve()),
6252
+ panBy: guardFn(guard, api.panBy, Promise.resolve()),
6253
+ panTo: guardFn(guard, api.panTo, Promise.resolve()),
6254
+ rollBy: guardFn(guard, api.rollBy, Promise.resolve()),
6255
+ rollTo: guardFn(guard, api.rollTo, Promise.resolve()),
6256
+ pitchBy: guardFn(guard, api.pitchBy, Promise.resolve()),
6257
+ pitchTo: guardFn(guard, api.pitchTo, Promise.resolve()),
6258
+ resetCamera: guardFn(guard, api.resetCamera, Promise.resolve()),
6259
+ setCameraBounds: guardFn(guard, api.setCameraBounds),
6260
+ getCameraState: guardFn(guard, api.getCameraState, {
6261
+ center: { x: 0, y: 0 },
6262
+ roll: 0,
6263
+ pitch: 0,
6264
+ zoom: 1,
6265
+ ptScale: 1
6266
+ })
6267
+ };
6268
+ return this.controlsAPI;
6040
6269
  }
6041
6270
  /**
6042
6271
  * {@link EventsAPI} instance for subscribing to internal events
6043
6272
  */
6044
6273
  get events() {
6045
- return asEventAPI(this.eventSystem);
6274
+ if (this.eventsAPI) return this.eventsAPI;
6275
+ this.eventsAPI = asEventAPI(this.eventSystem);
6276
+ return this.eventsAPI;
6277
+ }
6278
+ /**
6279
+ * {@link ViewportAPI} instance for view transforms and external transforms.
6280
+ */
6281
+ get viewport() {
6282
+ if (this.viewportAPI) return this.viewportAPI;
6283
+ const api = asViewportAPI(this.viewportSystem);
6284
+ const guard = (name) => this.assertInitialized(`viewport.${name}`) && this.assertNotDisposed(`viewport.${name}`);
6285
+ const guardExternal = (name) => guard(name) && this.assertExternalMode(`viewport.${name}`);
6286
+ this.viewportAPI = {
6287
+ canvasToSvg: guardFn(guard, api.canvasToSvg, { x: 0, y: 0 }),
6288
+ setStaticTransform: guardFn(guardExternal, api.setStaticTransform),
6289
+ setDynamicTransform: guardFn(guardExternal, api.setDynamicTransform)
6290
+ };
6291
+ return this.viewportAPI;
6046
6292
  }
6047
6293
  /**
6048
6294
  * Optional sub-rectangle of the viewport that is used for positioning scene's viewbox
6049
6295
  */
6050
6296
  get visibleRect() {
6051
- return this.viewport;
6297
+ return this.visibleRectValue;
6052
6298
  }
6053
6299
  /**
6054
6300
  * Optional sub-rectangle of the viewport that is used for positioning scene's viewbox
6055
6301
  */
6056
6302
  set visibleRect(rect) {
6057
- this.viewport = rect;
6058
- this.viewportSystem.updateViewport();
6059
- this.update();
6303
+ if (!this.assertNotExternalMode("visibleRect")) return;
6304
+ this.visibleRectValue = rect;
6305
+ if (this.initialized && !this.disposed) {
6306
+ this.viewportSystem.updateViewport();
6307
+ this.update();
6308
+ }
6060
6309
  }
6061
6310
  /**
6062
6311
  * Underlying {@link WebGLRenderer} instance
@@ -6072,64 +6321,73 @@ class Renderer {
6072
6321
  return [this.canvas.width, this.canvas.height];
6073
6322
  }
6074
6323
  /**
6075
- * Sets the renderer to external mode, where parts of rendering process are not managed by the renderer (e.g. Mapbox GL JS).
6076
- * @param staticTransformMatrix static transform matrix to apply to the scene
6324
+ * Returns true if the renderer is in external mode, meaning that webgl context is managed outside of the renderer
6325
+ * (for example, when using Mapbox GL JS as a host context). In this mode renderer will not clear the canvas before
6326
+ * rendering, and will not automatically compute scene and camera transformations. Clients are responsible for setting
6327
+ * the matrices manually by using {@link Renderer.viewport} methods.
6077
6328
  */
6078
- // TODO: Move somewhere
6079
- setExternalTransform(staticTransformMatrix) {
6080
- this.renderer.autoClear = false;
6081
- this.interactionsSystem.disconnect();
6082
- this.viewportSystem.setExternalTransform(staticTransformMatrix);
6329
+ get isExternalMode() {
6330
+ return this.gl !== void 0;
6083
6331
  }
6084
6332
  /**
6085
- * Update scene matrix from dynamic transform matrix.
6086
- * @param dynamicTransformMatrix dynamic transform matrix (changes every frame)
6333
+ * Returns true if the renderer is initialized, meaning that the viewport and scene have been set up.
6334
+ */
6335
+ get isInitialized() {
6336
+ return this.initialized;
6337
+ }
6338
+ /**
6339
+ * Returns true if the renderer is disposed, meaning that all WebGL resources have been released.
6087
6340
  */
6088
- updateExternalCamera(dynamicTransformMatrix) {
6089
- this.viewportSystem.updateExternalCamera(dynamicTransformMatrix);
6341
+ get isDisposed() {
6342
+ return this.disposed;
6090
6343
  }
6091
6344
  /**
6092
- * Initialize the scene and start the rendering loop
6345
+ * Initializes viewport and scene with the given scene definition.
6346
+ * Should be called once on startup. Repeated calls will produce console warnings.
6093
6347
  * @param sceneDef {@link SceneDef} to render
6094
- * @param startLoop whether to start the rendering loop
6095
6348
  */
6096
- start(sceneDef, startLoop = true) {
6097
- this.clock.start();
6349
+ init(sceneDef) {
6350
+ if (!this.assertNotDisposed("init")) return;
6351
+ if (!this.assertNotInitialized("init")) return;
6098
6352
  this.viewportSystem.initViewport(sceneDef);
6353
+ this.interactionsSystem.init();
6099
6354
  this.viewportSystem.scene.add(this.layerSystem.buildScene(sceneDef));
6100
- if (startLoop) this.renderer.setAnimationLoop(() => this.render());
6355
+ this.initialized = true;
6356
+ }
6357
+ /**
6358
+ * Start the rendering loop
6359
+ */
6360
+ start() {
6361
+ if (!this.assertNotDisposed("start")) return;
6362
+ if (!this.assertInitialized("start")) return;
6363
+ if (this.clock.running) return;
6364
+ this.clock.start();
6365
+ this.renderer.setAnimationLoop(() => this.render());
6101
6366
  }
6102
6367
  /**
6103
6368
  * Update the given defs to make them reflect the current state
6104
6369
  * @param defs {@link RenderableDef} array to update
6105
6370
  */
6106
6371
  update(...defs) {
6372
+ if (!this.assertNotDisposed("update")) return;
6373
+ if (!this.assertInitialized("update")) return;
6107
6374
  this.layerSystem.updateDefs(defs);
6108
6375
  this.needsRedraw = true;
6109
6376
  }
6110
6377
  /**
6111
- * Converts coordinates from canvas space to SVG space.
6112
- * @param point point in canvas space (relative to the canvas's top left corner), in css pixels
6113
- * @returns point in SVG space
6114
- */
6115
- screenToSvg(point) {
6116
- const vector2 = new Vector2(point.x, point.y);
6117
- canvasToNDC(vector2, this.canvas, vector2);
6118
- return this.viewportSystem.screenTo("svg", vector2);
6119
- }
6120
- /**
6121
- * Main rendering loop
6378
+ * Render a single frame
6122
6379
  */
6123
6380
  render() {
6124
6381
  var _a2, _b, _c, _d, _e, _f;
6382
+ if (!this.assertNotDisposed("render")) return;
6383
+ if (!this.assertInitialized("render")) return;
6125
6384
  (_b = (_a2 = this.ui) == null ? void 0 : _a2.stats) == null ? void 0 : _b.begin();
6126
- if (this.gl !== void 0) this.renderer.resetState();
6385
+ if (this.isExternalMode) this.renderer.resetState();
6127
6386
  else this.resizeCanvasToDisplaySize();
6128
6387
  this.viewportSystem.updatePtScale();
6129
- const delta = this.clock.getDelta();
6130
6388
  const hasDefsUpdated = this.layerSystem.processPendingUpdates();
6131
- const hasControlsUpdated = this.interactionsSystem.updateControls(delta);
6132
- const needsRedraw = this.needsRedraw || hasControlsUpdated || hasDefsUpdated || this.ui;
6389
+ const hasControlsUpdated = this.interactionsSystem.updateControls(this.clock.getDelta());
6390
+ const needsRedraw = this.needsRedraw || hasControlsUpdated || hasDefsUpdated || this.isExternalMode || this.ui;
6133
6391
  if (needsRedraw) {
6134
6392
  this.renderer.render(this.viewportSystem.scene, this.viewportSystem.camera);
6135
6393
  this.needsRedraw = false;
@@ -6146,11 +6404,21 @@ class Renderer {
6146
6404
  this.clock.stop();
6147
6405
  }
6148
6406
  /**
6149
- * Dispose all WebGL resources
6407
+ * Dispose all WebGL resources. This calls {@link Renderer.stop} internally.
6408
+ * WARNING: This method is final and cannot be undone. Attempting to use the renderer after calling this method
6409
+ * will result in a console warning and no-op methods. If you need to re-initialize the renderer, create a new instance.
6150
6410
  */
6151
6411
  dispose() {
6412
+ if (this.disposed) return;
6413
+ this.stop();
6414
+ this.interactionsSystem.dispose();
6152
6415
  this.layerSystem.disposeScene();
6153
6416
  this.viewportSystem.scene.clear();
6417
+ this.renderer.clear();
6418
+ this.renderer.info.reset();
6419
+ this.renderer.dispose();
6420
+ this.updateMemoryInfo();
6421
+ this.disposed = true;
6154
6422
  }
6155
6423
  // https://webgl2fundamentals.org/webgl/lessons/webgl-resizing-the-canvas.html
6156
6424
  resizeCanvasToDisplaySize() {
@@ -6167,35 +6435,93 @@ class Renderer {
6167
6435
  }
6168
6436
  }
6169
6437
  updateMemoryInfo() {
6170
- var _a2;
6438
+ var _a2, _b, _c, _d;
6171
6439
  if (this.memoryInfoExtension && ((_a2 = this.ui) == null ? void 0 : _a2.memoryInfoPanel)) {
6172
6440
  const memoryInfo = this.memoryInfoExtension.getMemoryInfo();
6173
6441
  memoryInfo.resources["drawCalls"] = this.renderer.info.render.calls;
6174
- const memoryInfoContent = JSON.stringify(memoryInfo.memory, null, 2);
6175
- const elapsedTime = this.clock.getElapsedTime() * 1e3;
6176
- if (memoryInfoContent !== this.memoryInfo) {
6442
+ if (memoryInfo.memory["texture"] !== ((_b = this.memoryInfo) == null ? void 0 : _b.memory["texture"]) || memoryInfo.memory["buffer"] !== ((_c = this.memoryInfo) == null ? void 0 : _c.memory["buffer"]) || memoryInfo.memory["renderbuffer"] !== ((_d = this.memoryInfo) == null ? void 0 : _d.memory["renderbuffer"])) {
6443
+ const elapsedTime = this.clock.getElapsedTime() * 1e3;
6177
6444
  const logMarker = `memoryInfo [${elapsedTime.toFixed(2)}ms since start]`;
6178
6445
  if (this.debugLog) console.log(logMarker, memoryInfo);
6179
- console.log("Buffers", this.memoryInfoExtension.getResourcesInfo(WebGLBuffer));
6180
- this.memoryInfo = memoryInfoContent;
6446
+ this.memoryInfo = memoryInfo;
6447
+ this.ui.memoryInfoPanel.textContent = JSON.stringify(memoryInfo, null, 2);
6181
6448
  }
6182
- this.ui.memoryInfoPanel.textContent = JSON.stringify(memoryInfo, null, 2);
6183
6449
  }
6184
6450
  }
6185
- // FIXME: Test with mapbox
6186
6451
  onContextLost(event) {
6452
+ var _a2, _b;
6187
6453
  event.preventDefault();
6188
6454
  console.log("webglcontextlost event", event);
6189
- this.renderer.setAnimationLoop(null);
6190
- this.clock.stop();
6191
- if (this.ui) setTimeout(() => this.renderer.forceContextRestore(), 0);
6455
+ const stats = (_a2 = this.ui) == null ? void 0 : _a2.stats;
6456
+ const context = this.renderer.getContext();
6457
+ if (stats && "deleteQuery" in context) {
6458
+ const gpuQueries = stats.gpuQueries;
6459
+ for (const queryInfo of gpuQueries) {
6460
+ this.renderer.getContext().deleteQuery(queryInfo.query);
6461
+ }
6462
+ stats.gpuQueries = [];
6463
+ if (stats.gpuPanel) {
6464
+ stats.dom.removeChild((_b = stats.gpuPanel) == null ? void 0 : _b.canvas);
6465
+ stats.gpuPanel = null;
6466
+ stats._panelId--;
6467
+ }
6468
+ }
6469
+ this.stop();
6192
6470
  }
6193
6471
  onContextRestored(event) {
6194
6472
  event.preventDefault();
6195
6473
  console.log("webglcontextrestored event", event);
6196
- this.renderer.setAnimationLoop(() => this.render());
6197
- this.clock.start();
6474
+ this.initContext(this.renderer.getContext());
6475
+ this.needsRedraw = true;
6476
+ this.start();
6477
+ }
6478
+ initContext(context) {
6479
+ var _a2, _b;
6480
+ this.memoryInfoExtension = context.getExtension("GMAN_webgl_memory");
6481
+ void ((_b = (_a2 = this.ui) == null ? void 0 : _a2.stats) == null ? void 0 : _b.init(context));
6482
+ }
6483
+ assertNotDisposed(funcName) {
6484
+ if (this.disposed) {
6485
+ console.warn(`[Renderer.${funcName}]: Renderer is used after being disposed. Please create a new instance.`);
6486
+ return false;
6487
+ }
6488
+ return true;
6489
+ }
6490
+ assertInitialized(funcName) {
6491
+ if (!this.initialized) {
6492
+ console.warn(`[Renderer.${funcName}]: Renderer is not initialized. Please call init() before using it.`);
6493
+ return false;
6494
+ }
6495
+ return true;
6198
6496
  }
6497
+ assertNotInitialized(funcName) {
6498
+ if (this.initialized) {
6499
+ console.warn(`[Renderer.${funcName}]: Renderer is already initialized. Please call init() only once.`);
6500
+ return false;
6501
+ }
6502
+ return true;
6503
+ }
6504
+ assertNotExternalMode(funcName) {
6505
+ if (this.isExternalMode) {
6506
+ console.warn(`[Renderer.${funcName}]: This operation is not supported in external mode.`);
6507
+ return false;
6508
+ }
6509
+ return true;
6510
+ }
6511
+ assertExternalMode(funcName) {
6512
+ if (!this.isExternalMode) {
6513
+ console.warn(`[Renderer.${funcName}]: This operation is only supported in external mode.`);
6514
+ return false;
6515
+ }
6516
+ return true;
6517
+ }
6518
+ }
6519
+ function guardFn(guard, fn, ...fallback) {
6520
+ return (...args) => {
6521
+ const name = fn.name.split(" ").at(-1);
6522
+ if (!guard(name)) return fallback[0];
6523
+ return fn(...args);
6524
+ };
6199
6525
  }
6200
6526
  export {
6201
6527
  Polygon,