@expofp/renderer 1.5.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +90 -36
  2. package/dist/index.js +734 -379
  3. package/package.json +3 -1
package/dist/index.js CHANGED
@@ -2,9 +2,10 @@ 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
+ import createLog from "debug";
8
9
  import { LineMaterial, LineSegmentsGeometry } from "three/examples/jsm/Addons.js";
9
10
  import { MaxRectsPacker, Rectangle } from "maxrects-packer";
10
11
  import { converter, parse } from "culori";
@@ -336,7 +337,7 @@ const dimColorVertexImpl = (
336
337
  `
337
338
  void setDimAmount() {
338
339
  float instanceDim = 0.;
339
- #ifdef USE_BATCH_UNIFORMS
340
+ #ifdef USE_BATCH_UNIFORMS
340
341
  instanceDim = batch_skipDimInstance;
341
342
  #endif
342
343
  #ifdef TROIKA_DERIVED_MATERIAL_1
@@ -366,6 +367,34 @@ const dimColorFrag = (
366
367
  return vec4(m * col.a, col.a);
367
368
  }`
368
369
  );
370
+ function createLogger(namespace) {
371
+ const info = createLog(namespace);
372
+ info.log = console.info.bind(console);
373
+ const debug = info.extend("debug");
374
+ debug.log = console.debug.bind(console);
375
+ const warn = info.extend("warn");
376
+ warn.log = console.warn.bind(console);
377
+ const error = info.extend("error");
378
+ error.log = console.error.bind(console);
379
+ return {
380
+ debug,
381
+ info,
382
+ warn,
383
+ error
384
+ };
385
+ }
386
+ function printTree(object, logger2) {
387
+ if (!logger2.enabled) return;
388
+ const isGroup = object.isGroup;
389
+ const name = object.name.split(":").at(-1);
390
+ if (isGroup) {
391
+ console.groupCollapsed(name);
392
+ object.children.forEach((child) => printTree(child, logger2));
393
+ console.groupEnd();
394
+ } else {
395
+ logger2.log(`${name}<${object.type}>, render order: ${object.renderOrder}`);
396
+ }
397
+ }
369
398
  function isObject(item) {
370
399
  return !!item && typeof item === "object" && !Array.isArray(item);
371
400
  }
@@ -394,7 +423,7 @@ function getSquareTextureSize(capacity, pixelsPerInstance) {
394
423
  }
395
424
  function getSquareTextureInfo(arrayType, channels, pixelsPerInstance, capacity) {
396
425
  if (channels === 3) {
397
- console.warn('"channels" cannot be 3. Set to 4. More info: https://github.com/mrdoob/three.js/pull/23228');
426
+ logger$b.debug('"channels" cannot be 3. Set to 4. More info: https://github.com/mrdoob/three.js/pull/23228');
398
427
  channels = 4;
399
428
  }
400
429
  const size = getSquareTextureSize(capacity, pixelsPerInstance);
@@ -416,6 +445,7 @@ function getSquareTextureInfo(arrayType, channels, pixelsPerInstance, capacity)
416
445
  }
417
446
  return { array, size, type, format };
418
447
  }
448
+ const logger$b = createLogger("SquareDataTexture");
419
449
  class SquareDataTexture extends DataTexture {
420
450
  /**
421
451
  * @param arrayType The constructor for the TypedArray.
@@ -453,7 +483,7 @@ class SquareDataTexture extends DataTexture {
453
483
  setUniformAt(id, name, value) {
454
484
  const schema = this.uniformMap.get(name);
455
485
  if (!schema) {
456
- console.warn(`SquareDataTexture.setUniformAt: uniform ${name} not found`);
486
+ logger$b.debug(`setUniformAt: uniform ${name} not found`);
457
487
  return;
458
488
  }
459
489
  const { offset, size } = schema;
@@ -474,7 +504,7 @@ class SquareDataTexture extends DataTexture {
474
504
  getUniformAt(id, name, target) {
475
505
  const schema = this.uniformMap.get(name);
476
506
  if (!schema) {
477
- console.warn(`SquareDataTexture.getUniformAt: uniform ${name} not found`);
507
+ logger$b.debug(`getUniformAt: uniform ${name} not found`);
478
508
  return 0;
479
509
  }
480
510
  const { offset, size } = schema;
@@ -620,6 +650,7 @@ class SquareDataTexture extends DataTexture {
620
650
  }
621
651
  const componentsArray = ["r", "g", "b", "a"];
622
652
  const batchIdName = "batchId";
653
+ const logger$a = createLogger("BatchedMesh");
623
654
  const _BatchedMesh = class _BatchedMesh extends BatchedMesh$1 {
624
655
  /**
625
656
  * @param instanceCount the max number of individual geometries planned to be added.
@@ -699,7 +730,7 @@ const _BatchedMesh = class _BatchedMesh extends BatchedMesh$1 {
699
730
  if (_BatchedMesh.useMultiDraw) return;
700
731
  const batchCount = this.batchCount;
701
732
  const gl = renderer.getContext();
702
- if (geometry.index == null) return console.warn("No index buffer", (_a2 = this.parent) == null ? void 0 : _a2.name);
733
+ if (geometry.index == null) return logger$a.debug("No index buffer", (_a2 = this.parent) == null ? void 0 : _a2.name);
703
734
  const type = this.getIndexType(gl, geometry.index);
704
735
  gl.drawElements(gl.TRIANGLES, batchCount, type, 0);
705
736
  renderer.info.update(batchCount, gl.TRIANGLES, 1);
@@ -714,7 +745,7 @@ const _BatchedMesh = class _BatchedMesh extends BatchedMesh$1 {
714
745
  */
715
746
  getUniformAt(id, name, target) {
716
747
  if (!this.uniformsTexture) {
717
- console.warn(`BatchedMesh.getUniformAt: uniforms texture not initialized`);
748
+ logger$a.debug(`getUniformAt: uniforms texture not initialized`);
718
749
  return 0;
719
750
  }
720
751
  return this.uniformsTexture.getUniformAt(id, name, target);
@@ -727,7 +758,7 @@ const _BatchedMesh = class _BatchedMesh extends BatchedMesh$1 {
727
758
  */
728
759
  setUniformAt(instanceId, name, value) {
729
760
  if (!this.uniformsTexture) {
730
- console.warn(`BatchedMesh.setUniformAt: uniforms texture not initialized`);
761
+ logger$a.debug(`setUniformAt: uniforms texture not initialized`);
731
762
  return;
732
763
  }
733
764
  this.uniformsTexture.setUniformAt(instanceId, name, value);
@@ -747,7 +778,7 @@ const _BatchedMesh = class _BatchedMesh extends BatchedMesh$1 {
747
778
  }
748
779
  dispose() {
749
780
  var _a2;
750
- this.geometry.setIndex(this.indexBuffer ?? null);
781
+ if (this.indexBuffer) this.geometry.setIndex(this.indexBuffer);
751
782
  super.dispose();
752
783
  this.uniformSchema = {};
753
784
  (_a2 = this.uniformsTexture) == null ? void 0 : _a2.dispose();
@@ -1082,29 +1113,18 @@ function isVisible(object) {
1082
1113
  if (!object.visible) return false;
1083
1114
  return [...traverseAncestorsGenerator(object)].every((obj) => obj.visible);
1084
1115
  }
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
1116
  class RenderableSystem {
1099
1117
  /**
1100
1118
  * @param type readable name of the system's type for debugging
1101
1119
  * @param renderer {@link Renderer}
1120
+ * @param logger {@link Logger}
1102
1121
  */
1103
- constructor(type, renderer) {
1122
+ constructor(type, renderer, logger2) {
1104
1123
  __publicField(this, "mapDefToObject", /* @__PURE__ */ new Map());
1105
1124
  __publicField(this, "mapObjectToDefs", /* @__PURE__ */ new Map());
1106
1125
  this.type = type;
1107
1126
  this.renderer = renderer;
1127
+ this.logger = logger2;
1108
1128
  }
1109
1129
  /**
1110
1130
  * Update a def with its current properties.
@@ -1131,7 +1151,7 @@ class RenderableSystem {
1131
1151
  * @param layerDef {@link TypedLayerDef} layer definition to update
1132
1152
  */
1133
1153
  updateLayer(group, layerDef) {
1134
- if (this.renderer.debugLog) console.log(`Updating ${this.type} layer ${layerDef.name}`, layerDef);
1154
+ this.logger.debug(`Updating layer ${layerDef.name} %O`, layerDef);
1135
1155
  this.updateLayerImpl(group, layerDef);
1136
1156
  }
1137
1157
  /**
@@ -1236,7 +1256,7 @@ class RenderableSystem {
1236
1256
  registerDefObject(def, object, instanceIds) {
1237
1257
  const ids = Array.isArray(instanceIds) ? instanceIds : [instanceIds];
1238
1258
  if (ids.length === 0) {
1239
- console.warn(`[RenderableSystem] Tried to register def with empty instanceIds:`, def);
1259
+ this.logger.debug(`Tried to register def with empty instanceIds %O`, def);
1240
1260
  return;
1241
1261
  }
1242
1262
  this.mapDefToObject.set(def, { object, instanceIds: ids });
@@ -1295,7 +1315,7 @@ class RenderableSystem {
1295
1315
  getObjectInstanceByDef(def) {
1296
1316
  const mapping = this.mapDefToObject.get(def);
1297
1317
  if (!mapping) {
1298
- console.warn(`[RenderableSystem] No object mapping found for def:`, def);
1318
+ this.logger.debug(`No object mapping found for def %O`, def);
1299
1319
  return void 0;
1300
1320
  }
1301
1321
  return mapping;
@@ -1316,13 +1336,14 @@ class RenderableSystem {
1316
1336
  return Array.from(this.mapObjectToDefs.keys());
1317
1337
  }
1318
1338
  }
1339
+ const logger$9 = createLogger("image");
1319
1340
  class ImageSystem extends RenderableSystem {
1320
1341
  /**
1321
1342
  * @param materialSystem {@link MaterialSystem}
1322
1343
  * @param renderer {@link Renderer}
1323
1344
  */
1324
1345
  constructor(materialSystem, renderer) {
1325
- super("image", renderer);
1346
+ super("image", renderer, logger$9);
1326
1347
  /** Textures memory limit in megabytes */
1327
1348
  __publicField(this, "memoryLimitMb");
1328
1349
  __publicField(this, "packer");
@@ -1332,7 +1353,7 @@ class ImageSystem extends RenderableSystem {
1332
1353
  __publicField(this, "scaleMatrix", new Matrix4());
1333
1354
  this.materialSystem = materialSystem;
1334
1355
  const atlasTextureSize = renderer.context.capabilities.maxTextureSize;
1335
- console.log(`Max texture size: ${atlasTextureSize}`);
1356
+ logger$9.debug(`Max texture size: ${atlasTextureSize}`);
1336
1357
  const padding = 1;
1337
1358
  this.packer = new MaxRectsPacker(atlasTextureSize, atlasTextureSize, padding, { pot: false });
1338
1359
  }
@@ -1382,10 +1403,10 @@ class ImageSystem extends RenderableSystem {
1382
1403
  resizeTextures() {
1383
1404
  var _a2;
1384
1405
  if (!this.memoryLimitMb) {
1385
- console.warn("Memory limit is not set, unable to resize textures.");
1406
+ logger$9.debug("Memory limit is not set, unable to resize textures.");
1386
1407
  return;
1387
1408
  }
1388
- console.log(`Resizing textures to fit memory limit: ${this.memoryLimitMb} MB`);
1409
+ logger$9.debug(`Resizing textures to fit memory limit: ${this.memoryLimitMb} MB`);
1389
1410
  const texturesToResize = [];
1390
1411
  let totalResizable = 0;
1391
1412
  let totalNonResizable = 0;
@@ -1402,15 +1423,15 @@ class ImageSystem extends RenderableSystem {
1402
1423
  }
1403
1424
  const budget = this.memoryLimitMb * 1024 * 1024 - totalNonResizable;
1404
1425
  if (budget < 0) {
1405
- console.warn("Memory limit is too low, unable to resize textures.");
1426
+ logger$9.debug("Memory limit is too low, unable to resize textures.");
1406
1427
  return;
1407
1428
  }
1408
1429
  const resizeFactor = Math.sqrt(budget / totalResizable);
1409
1430
  if (resizeFactor >= 1) {
1410
- console.log("Textures are already within the memory limit, no need to resize");
1431
+ logger$9.debug("Textures are already within the memory limit, no need to resize");
1411
1432
  return;
1412
1433
  }
1413
- console.log(`Resize factor: ${resizeFactor}`);
1434
+ logger$9.debug(`Resize factor: ${resizeFactor}`);
1414
1435
  let newTotal = totalNonResizable;
1415
1436
  for (const mesh of texturesToResize) {
1416
1437
  const material = mesh.material;
@@ -1418,14 +1439,14 @@ class ImageSystem extends RenderableSystem {
1418
1439
  const resizedTexture = resizeTexture(texture, resizeFactor);
1419
1440
  const textureDim = `${texture.image.width}x${texture.image.height}`;
1420
1441
  const resizedDim = `${resizedTexture.image.width}x${resizedTexture.image.height}`;
1421
- console.log(`Resized atlas for ${mesh.name || ((_a2 = mesh.parent) == null ? void 0 : _a2.name)}, from ${textureDim} to ${resizedDim}`);
1442
+ logger$9.debug(`Resized atlas for ${mesh.name || ((_a2 = mesh.parent) == null ? void 0 : _a2.name)}, from ${textureDim} to ${resizedDim}`);
1422
1443
  newTotal += getTextureSizeBytes(resizedTexture);
1423
1444
  material.map = resizedTexture;
1424
1445
  material.needsUpdate = true;
1425
1446
  texture.dispose();
1426
1447
  mesh.userData["nonResizable"] = true;
1427
1448
  }
1428
- console.log(`New memory usage after resizing: ${newTotal} bytes`);
1449
+ logger$9.debug(`New memory usage after resizing: ${newTotal} bytes`);
1429
1450
  }
1430
1451
  updateDefImpl(imageDef, mesh, instanceIds) {
1431
1452
  const bounds = imageDef.bounds;
@@ -1454,8 +1475,8 @@ class ImageSystem extends RenderableSystem {
1454
1475
  const boundsArea = boundsWidth * boundsHeight;
1455
1476
  const ratio = sourceArea / boundsArea;
1456
1477
  if (ratio > 1e3) {
1457
- console.log(`Image bounds: ${boundsWidth}x${boundsHeight}`, `Image source: ${sourceWidth}x${sourceHeight}`);
1458
- console.warn(`Image bounds area is ${ratio.toFixed(2)} times smaller than the image.`);
1478
+ logger$9.debug(`Image bounds: ${boundsWidth}x${boundsHeight}`, `Image source: ${sourceWidth}x${sourceHeight}`);
1479
+ logger$9.warn(`Image bounds area is ${ratio.toFixed(2)} times smaller than the image.`);
1459
1480
  }
1460
1481
  const rect = new Rectangle(image.source.width, image.source.height);
1461
1482
  rect.data = [imageWithIndex];
@@ -1466,7 +1487,7 @@ class ImageSystem extends RenderableSystem {
1466
1487
  }
1467
1488
  }
1468
1489
  this.packer.addArray(rectangles);
1469
- this.packer.bins.forEach((bin) => console.log(`Bin: ${bin.width}x${bin.height}, ${bin.rects.length} rectangles`));
1490
+ this.packer.bins.forEach((bin) => logger$9.debug(`Bin: ${bin.width}x${bin.height}, ${bin.rects.length} rectangles`));
1470
1491
  return this.packer.bins;
1471
1492
  }
1472
1493
  }
@@ -1480,7 +1501,7 @@ function createAtlas(bin) {
1480
1501
  ctx.drawImage(rect.data[0].def.source, rect.x, rect.y, rect.width, rect.height);
1481
1502
  }
1482
1503
  const t1 = performance.now();
1483
- console.log(`Create atlas took ${(t1 - t0).toFixed(2)} milliseconds.`);
1504
+ logger$9.debug(`Create atlas took ${(t1 - t0).toFixed(2)} milliseconds.`);
1484
1505
  return createTexture(canvas);
1485
1506
  }
1486
1507
  function resizeTexture(texture, resizeFactor) {
@@ -1503,13 +1524,14 @@ function getTextureSizeBytes(texture) {
1503
1524
  const imageBytes = texture.image.width * texture.image.height * 4 * (texture.generateMipmaps ? 1.33 : 1);
1504
1525
  return Math.ceil(imageBytes);
1505
1526
  }
1527
+ const logger$8 = createLogger("line");
1506
1528
  class LineSystem extends RenderableSystem {
1507
1529
  /**
1508
1530
  * @param materialSystem {@link MaterialSystem}
1509
1531
  * @param renderer {@link Renderer}
1510
1532
  */
1511
1533
  constructor(materialSystem, renderer) {
1512
- super("line", renderer);
1534
+ super("line", renderer, logger$8);
1513
1535
  __publicField(this, "lineColor", new Color());
1514
1536
  this.materialSystem = materialSystem;
1515
1537
  }
@@ -1698,13 +1720,14 @@ class Polygon {
1698
1720
  return this;
1699
1721
  }
1700
1722
  }
1723
+ const logger$7 = createLogger("mesh");
1701
1724
  class MeshSystem extends RenderableSystem {
1702
1725
  /**
1703
1726
  * @param materialSystem {@link MaterialSystem}
1704
1727
  * @param renderer {@link Renderer}
1705
1728
  */
1706
1729
  constructor(materialSystem, renderer) {
1707
- super("mesh", renderer);
1730
+ super("mesh", renderer, logger$7);
1708
1731
  __publicField(this, "toRgbConverter", converter("rgb"));
1709
1732
  __publicField(this, "meshColor", new Color());
1710
1733
  this.materialSystem = materialSystem;
@@ -1815,16 +1838,18 @@ class MeshSystem extends RenderableSystem {
1815
1838
  });
1816
1839
  }
1817
1840
  }
1841
+ const logger$6 = createLogger("text");
1818
1842
  class TextSystem extends RenderableSystem {
1819
1843
  /**
1820
1844
  * @param materialSystem {@link MaterialSystem}
1821
1845
  * @param renderer {@link Renderer}
1822
1846
  */
1823
1847
  constructor(materialSystem, renderer) {
1824
- super("text", renderer);
1848
+ super("text", renderer, logger$6);
1825
1849
  __publicField(this, "initialTextScale", new Vector2(1, -1));
1826
1850
  __publicField(this, "textColor", new Color());
1827
1851
  __publicField(this, "pendingUpdates", /* @__PURE__ */ new Map());
1852
+ __publicField(this, "sdfAtlases", /* @__PURE__ */ new Set());
1828
1853
  __publicField(this, "alignmentOffset", new Vector2());
1829
1854
  __publicField(this, "alignmentDirection", new Vector2());
1830
1855
  __publicField(this, "localPosition", new Vector2());
@@ -1834,6 +1859,10 @@ class TextSystem extends RenderableSystem {
1834
1859
  __publicField(this, "localToMax", new Vector2());
1835
1860
  this.materialSystem = materialSystem;
1836
1861
  }
1862
+ dispose() {
1863
+ super.dispose();
1864
+ this.sdfAtlases.forEach((texture) => texture.dispose());
1865
+ }
1837
1866
  buildLayer(layer) {
1838
1867
  const group = new Group();
1839
1868
  const batchedText = this.buildBatchedText(layer);
@@ -1911,7 +1940,12 @@ class TextSystem extends RenderableSystem {
1911
1940
  for (const { textDef, instanceIds } of mappingData) {
1912
1941
  this.registerDefObject(textDef, batchedText, instanceIds);
1913
1942
  }
1914
- batchedText.addEventListener("synccomplete", () => this.renderer.update());
1943
+ batchedText.addEventListener("synccomplete", () => {
1944
+ var _a2;
1945
+ const sdfTexture = (_a2 = batchedText.textRenderInfo) == null ? void 0 : _a2.sdfTexture;
1946
+ if (sdfTexture) this.sdfAtlases.add(sdfTexture);
1947
+ this.renderer.update();
1948
+ });
1915
1949
  return batchedText;
1916
1950
  }
1917
1951
  // TODO: Simplify
@@ -1999,6 +2033,7 @@ function setAnchorsAndAlignment(text, alignment) {
1999
2033
  text.anchorY = alignment.vertical === "bottom" ? "bottom" : "top";
2000
2034
  text.textAlign = alignment.text ?? alignment.horizontal;
2001
2035
  }
2036
+ const logger$5 = createLogger("layer");
2002
2037
  class LayerSystem {
2003
2038
  /**
2004
2039
  * @param renderer {@link Renderer}
@@ -2044,6 +2079,7 @@ class LayerSystem {
2044
2079
  }
2045
2080
  /**
2046
2081
  * Update the given defs immediately, or queue them for update if update buffering is enabled.
2082
+ * NOTE: Currently update buffering is disabled, as observed performance gains are negligible. Need to revisit this.
2047
2083
  * @param defs {@link RenderableDef} array
2048
2084
  */
2049
2085
  updateDefs(defs) {
@@ -2069,12 +2105,8 @@ class LayerSystem {
2069
2105
  this.updateDef(def);
2070
2106
  processed++;
2071
2107
  }
2072
- const took = performance.now() - startTime;
2073
- if (processed && this.renderer.debugLog) {
2074
- console.log(
2075
- `LayerSystem: processed ${processed} defs in ${took.toFixed(2)}ms, ${this.pendingDefs.size} remaining`
2076
- );
2077
- }
2108
+ const took = (performance.now() - startTime).toFixed(2);
2109
+ if (processed) logger$5.debug(`processed ${processed} defs in ${took}ms, ${this.pendingDefs.size} remaining`);
2078
2110
  return processed > 0;
2079
2111
  }
2080
2112
  /**
@@ -2084,11 +2116,8 @@ class LayerSystem {
2084
2116
  */
2085
2117
  buildScene(sceneDef) {
2086
2118
  this.initRenderOrder(sceneDef.rootLayer);
2087
- if (this.renderer.debugLog)
2088
- console.log(
2089
- "Render order",
2090
- this.layerDefRenderOrder.map((layer) => this.getFullLayerName(layer))
2091
- );
2119
+ const renderOrder = this.layerDefRenderOrder.map((layer) => this.getFullLayerName(layer));
2120
+ logger$5.debug("Render order %O", renderOrder);
2092
2121
  const rootGroup = new Group();
2093
2122
  rootGroup.name = sceneDef.rootLayer.name;
2094
2123
  this.mapLayerDefsToObjects.set(sceneDef.rootLayer, rootGroup);
@@ -2097,7 +2126,7 @@ class LayerSystem {
2097
2126
  rootGroup.add(this.buildLayer(child));
2098
2127
  }
2099
2128
  this.updateLayer(sceneDef.rootLayer, false);
2100
- if (this.renderer.debugLog) printTree(rootGroup);
2129
+ printTree(rootGroup, logger$5.debug);
2101
2130
  if (sceneDef.memoryLimit) {
2102
2131
  this.imageSystem.memoryLimitMb = sceneDef.memoryLimit;
2103
2132
  this.imageSystem.resizeTextures();
@@ -2141,7 +2170,7 @@ class LayerSystem {
2141
2170
  }
2142
2171
  buildLayer(layerDef, parentPrefix = "") {
2143
2172
  const layerFullName = parentPrefix + layerDef.name;
2144
- console.log(`Building layer ${layerFullName}...`);
2173
+ logger$5.debug(`Building layer ${layerFullName}...`);
2145
2174
  let layerObject;
2146
2175
  if (isShapeLayer(layerDef)) layerObject = this.meshSystem.buildLayer(layerDef);
2147
2176
  else if (isImageLayer(layerDef)) layerObject = this.imageSystem.buildLayer(layerDef);
@@ -4739,25 +4768,36 @@ const subsetOfTHREE = {
4739
4768
  Plane
4740
4769
  };
4741
4770
  CameraControls.install({ THREE: subsetOfTHREE });
4771
+ const logger$4 = createLogger("cameraController");
4742
4772
  class CameraController extends CameraControls {
4743
4773
  /**
4744
4774
  * @param camera {@link PerspectiveCamera} instance
4745
- * @param renderer {@link Renderer} instance
4746
4775
  */
4747
- constructor(camera, renderer) {
4776
+ constructor(camera) {
4748
4777
  super(camera);
4749
- this.renderer = renderer;
4778
+ this.dollyToCursor = true;
4779
+ this.draggingSmoothTime = 0;
4780
+ void this.rotatePolarTo(0, false);
4781
+ this.mouseButtons = {
4782
+ left: CameraController.ACTION.NONE,
4783
+ middle: CameraController.ACTION.NONE,
4784
+ right: CameraController.ACTION.NONE,
4785
+ wheel: CameraController.ACTION.NONE
4786
+ };
4787
+ this.touches = {
4788
+ one: CameraController.ACTION.NONE,
4789
+ two: CameraController.ACTION.NONE,
4790
+ three: CameraController.ACTION.NONE
4791
+ };
4750
4792
  }
4751
4793
  update(delta) {
4752
- var _a2;
4753
4794
  const needsUpdate = super.update(delta);
4754
- if (needsUpdate && ((_a2 = this.renderer) == null ? void 0 : _a2.debugLog)) {
4755
- const position = this.camera.position.toArray().map((value) => value.toFixed(2)).join(", ");
4756
- const target = this._target.toArray().map((value) => value.toFixed(2)).join(", ");
4757
- const spherical = [this._spherical.theta * RAD2DEG, this._spherical.phi * RAD2DEG, this._spherical.radius].map((value) => value.toFixed(2)).join(", ");
4758
- console.log(`position: [${position}]
4759
- target: [${target}]
4760
- spherical: [${spherical}]`);
4795
+ if (needsUpdate) {
4796
+ const position = this.camera.position.toArray().map((value) => +value.toFixed(2));
4797
+ const target = this._target.toArray().map((value) => +value.toFixed(2));
4798
+ const { theta, phi, radius } = this._spherical;
4799
+ const spherical = [theta * RAD2DEG, phi * RAD2DEG, radius].map((value) => +value.toFixed(2));
4800
+ logger$4.debug("camera update %O", { position, target, spherical });
4761
4801
  }
4762
4802
  return needsUpdate;
4763
4803
  }
@@ -4767,31 +4807,30 @@ class CameraSystem {
4767
4807
  * @param renderer {@link Renderer} instance
4768
4808
  */
4769
4809
  constructor(renderer) {
4770
- /** External camera instance. Used to render the scene in external mode (e.g. Mapbox GL JS). */
4771
- __publicField(this, "externalCamera");
4810
+ /** {@link PerspectiveCamera} instance. Used to render the scene in internal mode. */
4811
+ __publicField(this, "camera");
4772
4812
  /** {@link CameraController} instance. Used to smoothly animate the camera. */
4773
4813
  __publicField(this, "controller");
4774
- __publicField(this, "camera");
4775
- __publicField(this, "zoomIdentityDistance");
4814
+ /**
4815
+ * Cached previous viewport height used to preserve zoom across resizes.
4816
+ * Note: we intentionally keep this separate from the derived identity distance.
4817
+ */
4818
+ __publicField(this, "prevViewportHeightPx");
4819
+ /** [min, max] zoom factors */
4776
4820
  __publicField(this, "zoomBounds");
4821
+ /** Default FOV for the camera. Taken from Mapbox GL JS. */
4822
+ __publicField(this, "defaultFov", 36.87);
4777
4823
  this.renderer = renderer;
4778
- const [w, h] = renderer.size;
4779
- this.camera = new PerspectiveCamera(90, w / (h || 1));
4824
+ const h = renderer.size[1];
4825
+ this.prevViewportHeightPx = h;
4826
+ this.camera = new PerspectiveCamera(this.defaultFov);
4780
4827
  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;
4828
+ this.controller = new CameraController(this.camera);
4785
4829
  this.controller.distance = this.zoomIdentityDistance;
4786
4830
  }
4787
- /** Current camera instance. */
4788
- get currentCamera() {
4789
- return this.externalCamera ?? this.camera;
4790
- }
4791
4831
  /** Current camera zoom factor. */
4792
4832
  get zoomFactor() {
4793
- const distance = this.controller.distance;
4794
- return distance ? this.zoomIdentityDistance / distance : 1;
4833
+ return this.zoomFactorForHeight(this.renderer.size[1]);
4795
4834
  }
4796
4835
  /**
4797
4836
  * Calculates the camera distance from the scene's plane for a given zoom factor.
@@ -4799,7 +4838,8 @@ class CameraSystem {
4799
4838
  * @returns Corresponding camera distance on the Z axis
4800
4839
  */
4801
4840
  zoomFactorToDistance(zoomFactor) {
4802
- return zoomFactor > 0 ? this.zoomIdentityDistance / zoomFactor : this.zoomIdentityDistance;
4841
+ if (zoomFactor <= 0) return this.zoomIdentityDistance;
4842
+ return this.zoomIdentityDistance / zoomFactor;
4803
4843
  }
4804
4844
  /**
4805
4845
  * Initializes the camera with the given zoom bounds.
@@ -4813,41 +4853,189 @@ class CameraSystem {
4813
4853
  updateCamera() {
4814
4854
  if (!this.zoomBounds) return;
4815
4855
  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]);
4856
+ if (w <= 0 || h <= 0) return;
4857
+ const zoomFactor = this.zoomFactorForHeight(this.prevViewportHeightPx);
4858
+ const newZoomIdentity = this.zoomIdentityDistanceForHeight(h);
4859
+ const maxDistance = Math.abs(newZoomIdentity / this.zoomBounds[0]);
4860
+ const minDistance = Math.abs(newZoomIdentity / this.zoomBounds[1]);
4820
4861
  this.camera.aspect = w / (h || 1);
4821
- this.computeCameraClipPlanes(minDistance, maxDistance);
4862
+ this.camera.near = 0.01;
4863
+ this.camera.far = Math.max(maxDistance, this.camera.near) * 2;
4822
4864
  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
4865
  this.controller.minDistance = minDistance;
4849
4866
  this.controller.maxDistance = maxDistance;
4850
4867
  void this.controller.dollyTo(this.zoomFactorToDistance(zoomFactor), false);
4868
+ this.prevViewportHeightPx = h;
4869
+ }
4870
+ /**
4871
+ * Distance from the scene plane corresponding to zoomFactor = 1 for the current viewport height.
4872
+ * Derived from camera FOV and renderer height (in pixels).
4873
+ */
4874
+ get zoomIdentityDistance() {
4875
+ return this.zoomIdentityDistanceForHeight(this.renderer.size[1]);
4876
+ }
4877
+ /**
4878
+ * Calculates the zoom identity distance for a given viewport height.
4879
+ * @param viewportHeightPx Renderer height in pixels
4880
+ * @returns Zoom identity distance
4881
+ */
4882
+ zoomIdentityDistanceForHeight(viewportHeightPx) {
4883
+ if (viewportHeightPx <= 0) return 0;
4884
+ return viewportHeightPx * 0.5 / Math.tan(this.camera.fov * DEG2RAD$1 / 2);
4885
+ }
4886
+ /**
4887
+ * Calculates the zoom factor for a given viewport height.
4888
+ * @param viewportHeightPx Renderer height in pixels
4889
+ * @returns Zoom factor
4890
+ */
4891
+ zoomFactorForHeight(viewportHeightPx) {
4892
+ const zid = this.zoomIdentityDistanceForHeight(viewportHeightPx);
4893
+ if (zid === 0) return 1;
4894
+ return zid / (this.controller.distance || zid);
4895
+ }
4896
+ }
4897
+ const logger$3 = createLogger("external");
4898
+ class ExternalSystem {
4899
+ /**
4900
+ * @param pickingSystem {@link PickingSystem} instance
4901
+ */
4902
+ constructor(pickingSystem) {
4903
+ /** External camera instance */
4904
+ __publicField(this, "camera", new Camera());
4905
+ __publicField(this, "staticTransformMatrix", new Matrix4());
4906
+ __publicField(this, "intersectionPoint", new Vector3());
4907
+ /**
4908
+ * Scratch NDC coordinate used by external ptScale. Kept as a field to avoid allocating a Vector2 each frame.
4909
+ * (This is always screen center: NDC (0,0))
4910
+ */
4911
+ __publicField(this, "ndcCenter", new Vector2(0, 0));
4912
+ /**
4913
+ * Scratch clip-space vector used when projecting SVG points through the external camera matrix.
4914
+ * Kept as a field to avoid allocating a Vector4 each frame.
4915
+ */
4916
+ __publicField(this, "clipPoint", new Vector4());
4917
+ /**
4918
+ * Scratch pixel points used by external ptScale estimation.
4919
+ * p0 is the screen center in drawing-buffer pixels; p1/p2 are projected offsets from the anchor point.
4920
+ */
4921
+ __publicField(this, "px0", new Vector2());
4922
+ __publicField(this, "px1", new Vector2());
4923
+ __publicField(this, "px2", new Vector2());
4924
+ this.pickingSystem = pickingSystem;
4925
+ }
4926
+ /**
4927
+ * Set static part of an svg -> px transform matrix
4928
+ * @param staticTransformMatrix static transform matrix to apply to the scene
4929
+ */
4930
+ setStaticTransform(staticTransformMatrix) {
4931
+ if (!this.validateMatrix(staticTransformMatrix, "setStaticTransform")) return;
4932
+ this.staticTransformMatrix.fromArray(staticTransformMatrix);
4933
+ }
4934
+ /**
4935
+ * Set dynamic part of an svg -> px transform matrix. Should be called every frame.
4936
+ * @param dynamicTransformMatrix dynamic transform matrix (changes every frame)
4937
+ */
4938
+ setDynamicTransform(dynamicTransformMatrix) {
4939
+ if (!this.validateMatrix(dynamicTransformMatrix, "setDynamicTransform")) return;
4940
+ this.camera.projectionMatrix.fromArray(dynamicTransformMatrix).multiply(this.staticTransformMatrix);
4941
+ this.camera.projectionMatrixInverse.copy(this.camera.projectionMatrix).invert();
4942
+ }
4943
+ /**
4944
+ * Estimates pixel→SVG scale for external context rendering (e.g. Mapbox).
4945
+ *
4946
+ * In external mode we don't own a camera rig/controller, so we can't derive scale from "camera distance".
4947
+ * Instead, we:
4948
+ * 1) Find the point on the SVG plane that is currently under the screen center.
4949
+ * 2) Measure how much the screen position changes when moving 1 SVG unit in X/Y around that point.
4950
+ * 3) Convert that local plane→screen mapping into a single "zoom-like" scalar that is stable under tilt+rotate.
4951
+ *
4952
+ * This matches internal mode semantics better because the internal orbit target is also kept under screen center.
4953
+ * @param viewportSize Size of the viewport in drawing-buffer pixels
4954
+ * @returns Pixel-to-SVG scale factor (px → svg units)
4955
+ */
4956
+ pxToSvgScale(viewportSize) {
4957
+ const M = this.camera.projectionMatrix;
4958
+ const [viewportW, viewportH] = viewportSize;
4959
+ if (viewportW <= 0 || viewportH <= 0) return;
4960
+ const intersectionPoint = this.pickingSystem.intersectPlane(this.ndcCenter, this.camera, this.intersectionPoint);
4961
+ if (!intersectionPoint) return;
4962
+ const anchorX = intersectionPoint.x;
4963
+ const anchorY = intersectionPoint.y;
4964
+ const clip = this.clipPoint;
4965
+ const p0 = this.px0.set(viewportW * 0.5, viewportH * 0.5);
4966
+ const p1 = this.px1;
4967
+ const p2 = this.px2;
4968
+ const svgToPixels = (x, y, out) => {
4969
+ clip.set(x, y, 0, 1).applyMatrix4(M);
4970
+ if (clip.w === 0) return false;
4971
+ const ndcX = clip.x / clip.w;
4972
+ const ndcY = clip.y / clip.w;
4973
+ out.set((ndcX + 1) * 0.5 * viewportW, (1 - ndcY) * 0.5 * viewportH);
4974
+ return true;
4975
+ };
4976
+ const svgStep = 1;
4977
+ if (!svgToPixels(anchorX + svgStep, anchorY, p1)) return;
4978
+ if (!svgToPixels(anchorX, anchorY + svgStep, p2)) return;
4979
+ const pxDeltaPerSvgX = p1.sub(p0).divideScalar(svgStep);
4980
+ const pxDeltaPerSvgY = p2.sub(p0).divideScalar(svgStep);
4981
+ const pixelsSqPerSvgX = pxDeltaPerSvgX.dot(pxDeltaPerSvgX);
4982
+ const pixelsSqPerSvgY = pxDeltaPerSvgY.dot(pxDeltaPerSvgY);
4983
+ const pixelsSqCross = pxDeltaPerSvgX.dot(pxDeltaPerSvgY);
4984
+ const sumPixelsSq = pixelsSqPerSvgX + pixelsSqPerSvgY;
4985
+ const areaPixelsSq = pixelsSqPerSvgX * pixelsSqPerSvgY - pixelsSqCross * pixelsSqCross;
4986
+ const maxStretchDiscriminant = Math.max(0, sumPixelsSq * sumPixelsSq - 4 * areaPixelsSq);
4987
+ const maxPixelsSqPerSvg = 0.5 * (sumPixelsSq + Math.sqrt(maxStretchDiscriminant));
4988
+ const pxPerSvg = Math.sqrt(maxPixelsSqPerSvg);
4989
+ if (!Number.isFinite(pxPerSvg) || pxPerSvg <= 0) return;
4990
+ return 1 / pxPerSvg;
4991
+ }
4992
+ validateMatrix(matrix, name) {
4993
+ if (matrix.length !== 16) {
4994
+ logger$3.warn(`${name}: Matrix must be 16 elements long`);
4995
+ return false;
4996
+ }
4997
+ return true;
4998
+ }
4999
+ }
5000
+ class PickingSystem {
5001
+ /** */
5002
+ constructor() {
5003
+ __publicField(this, "raycaster", new Raycaster());
5004
+ __publicField(this, "ndcPoint", new Vector2());
5005
+ __publicField(this, "viewboxPlane", new Plane(new Vector3(0, 0, 1), 0));
5006
+ this.raycaster.layers.set(INTERACTIVE_LAYER);
5007
+ }
5008
+ /**
5009
+ * Gets the objects intersected by the raycaster.
5010
+ * @param ndcCoords raycast point in NDC (normalized device coordinates)
5011
+ * @param scene {@link Scene} instance
5012
+ * @param camera {@link Camera} instance
5013
+ * @returns Array of {@link Intersection} instances
5014
+ */
5015
+ getIntersectedObjects(ndcCoords, scene, camera) {
5016
+ this.setRaycasterFromCamera(ndcCoords, camera);
5017
+ const intersections = this.raycaster.intersectObject(scene, true);
5018
+ return intersections.filter((i) => isVisible(i.object));
5019
+ }
5020
+ /**
5021
+ * Intersects the xy-plane with the raycaster.
5022
+ * @param ndcCoords raycast point in NDC (normalized device coordinates
5023
+ * @param camera {@link Camera} instance
5024
+ * @param out Output vector
5025
+ * @returns Intersection point in world space or null if no intersection.
5026
+ */
5027
+ intersectPlane(ndcCoords, camera, out) {
5028
+ this.setRaycasterFromCamera(ndcCoords, camera);
5029
+ return this.raycaster.ray.intersectPlane(this.viewboxPlane, out) ?? void 0;
5030
+ }
5031
+ setRaycasterFromCamera(ndcCoords, camera) {
5032
+ if (camera.isPerspectiveCamera || camera.isOrthographicCamera) {
5033
+ this.ndcPoint.set(ndcCoords.x, ndcCoords.y);
5034
+ this.raycaster.setFromCamera(this.ndcPoint, camera);
5035
+ } else {
5036
+ this.raycaster.ray.origin.set(0, 0, 0).unproject(camera);
5037
+ this.raycaster.ray.direction.set(ndcCoords.x, ndcCoords.y, 1).unproject(camera).sub(this.raycaster.ray.origin).normalize();
5038
+ }
4851
5039
  }
4852
5040
  }
4853
5041
  class SceneSystem {
@@ -4857,29 +5045,22 @@ class SceneSystem {
4857
5045
  constructor(renderer) {
4858
5046
  /** {@link Scene} instance */
4859
5047
  __publicField(this, "scene");
4860
- /** World matrix - SVGWorld transform */
5048
+ /** World matrix - modelworld transform */
4861
5049
  __publicField(this, "worldMatrix", new Matrix4());
4862
- /** Inverse world matrix - WorldSVG transform */
5050
+ /** Inverse world matrix - worldmodel transform */
4863
5051
  __publicField(this, "inverseWorldMatrix", new Matrix4());
5052
+ __publicField(this, "tempVector3", new Vector3());
4864
5053
  __publicField(this, "translationMatrix", new Matrix4());
4865
5054
  __publicField(this, "scaleMatrix", new Matrix4());
4866
- __publicField(this, "scaleVector", new Vector3());
4867
5055
  __publicField(this, "visibleRectOffsetMatrix", new Matrix4());
4868
5056
  __publicField(this, "viewbox");
4869
5057
  this.renderer = renderer;
4870
5058
  this.scene = new Scene();
4871
5059
  this.scene.matrixAutoUpdate = false;
4872
5060
  }
4873
- /** Scene scale factor (SVG to pixel) */
5061
+ /** Scene scale factor (model space to world space) */
4874
5062
  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
- }
5063
+ return this.scene.matrix.elements[0];
4883
5064
  }
4884
5065
  /**
4885
5066
  * Initializes the scene with the given SVG viewbox.
@@ -4889,28 +5070,48 @@ class SceneSystem {
4889
5070
  this.viewbox = viewbox;
4890
5071
  this.updateScene();
4891
5072
  }
4892
- /**
4893
- * Updates the scene transform when the renderer size changes.
4894
- */
5073
+ /** Updates the scene transform from the current viewbox and renderer size. */
4895
5074
  updateScene() {
4896
5075
  if (!this.viewbox) return;
5076
+ this.composeMatrices(this.viewbox);
5077
+ }
5078
+ /**
5079
+ * Converts a point from model coordinates to world coordinates.
5080
+ * @param modelCoords Point in model coordinates
5081
+ * @param out Output vector
5082
+ * @returns Point in world coordinates
5083
+ */
5084
+ modelToWorld(modelCoords, out) {
5085
+ const worldPoint = this.tempVector3.set(modelCoords.x, modelCoords.y, 0).applyMatrix4(this.worldMatrix);
5086
+ out.set(worldPoint.x, worldPoint.y, 0);
5087
+ return out;
5088
+ }
5089
+ /**
5090
+ * Converts a point from world coordinates to model coordinates. Z axis is ignored.
5091
+ * @param worldCoords Point in world coordinates
5092
+ * @param out Output vector
5093
+ * @returns Point in SVG coordinates
5094
+ */
5095
+ worldToModel(worldCoords, out) {
5096
+ const modelPoint = this.tempVector3.copy(worldCoords).applyMatrix4(this.inverseWorldMatrix);
5097
+ out.set(modelPoint.x, modelPoint.y);
5098
+ return out;
5099
+ }
5100
+ composeMatrices(viewbox) {
4897
5101
  const dpr = this.renderer.context.getPixelRatio();
4898
5102
  const visibleRect = this.renderer.visibleRect;
4899
- const [viewBoxWidth, viewBoxHeight] = this.viewbox.size;
5103
+ const [viewBoxWidth, viewBoxHeight] = viewbox.size;
4900
5104
  const [visibleRectWidth, visibleRectHeight] = (visibleRect == null ? void 0 : visibleRect.size.clone().multiplyScalar(dpr)) ?? this.renderer.size;
4901
5105
  const scaleFactor = Math.min(visibleRectWidth / viewBoxWidth, visibleRectHeight / viewBoxHeight);
4902
- const [centerX, centerY] = this.viewbox.center;
5106
+ const [centerX, centerY] = viewbox.center;
4903
5107
  this.translationMatrix.makeTranslation(-centerX, -centerY, 0);
4904
5108
  this.scaleMatrix.makeScale(scaleFactor, scaleFactor, 1);
4905
5109
  if (visibleRect) {
4906
5110
  const visibleRectCenter = visibleRect.center.clone().multiplyScalar(dpr);
4907
- const canvasCenter = new Vector2(...this.renderer.size).multiplyScalar(0.5);
5111
+ const canvasCenter = { x: this.renderer.size[0] / 2, y: this.renderer.size[1] / 2 };
4908
5112
  const offset = visibleRectCenter.sub(canvasCenter);
4909
5113
  this.visibleRectOffsetMatrix.makeTranslation(offset.x, offset.y, 0);
4910
5114
  }
4911
- this.composeMatrices();
4912
- }
4913
- composeMatrices() {
4914
5115
  this.worldMatrix.copy(this.translationMatrix).premultiply(this.scaleMatrix);
4915
5116
  if (this.renderer.visibleRect) this.worldMatrix.premultiply(this.visibleRectOffsetMatrix);
4916
5117
  this.scene.matrix.copy(this.worldMatrix);
@@ -4918,25 +5119,25 @@ class SceneSystem {
4918
5119
  this.scene.matrixWorldNeedsUpdate = true;
4919
5120
  }
4920
5121
  }
5122
+ const logger$2 = createLogger("viewport");
4921
5123
  class ViewportSystem {
4922
5124
  /**
4923
5125
  * @param renderer {@link Renderer} instance
4924
5126
  * @param eventSystem {@link EventSystem} instance
4925
5127
  */
4926
5128
  constructor(renderer, eventSystem) {
5129
+ __publicField(this, "pickingSystem");
5130
+ __publicField(this, "externalSystem");
4927
5131
  __publicField(this, "sceneSystem");
4928
5132
  __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
5133
  __publicField(this, "pxToSvgScaleThreshold", 1e-4);
4933
5134
  __publicField(this, "prevPxToSvgScale");
4934
- __publicField(this, "externalStaticTransformMatrix", new Matrix4());
4935
5135
  this.renderer = renderer;
4936
5136
  this.eventSystem = eventSystem;
5137
+ this.pickingSystem = new PickingSystem();
4937
5138
  this.sceneSystem = new SceneSystem(renderer);
4938
5139
  this.cameraSystem = new CameraSystem(renderer);
4939
- this.raycaster.layers.set(INTERACTIVE_LAYER);
5140
+ this.externalSystem = new ExternalSystem(this.pickingSystem);
4940
5141
  }
4941
5142
  /** {@link Scene} instance */
4942
5143
  get scene() {
@@ -4944,10 +5145,10 @@ class ViewportSystem {
4944
5145
  }
4945
5146
  /** Current {@link Camera} instance */
4946
5147
  get camera() {
4947
- return this.cameraSystem.currentCamera;
5148
+ return this.renderer.isExternalMode ? this.externalSystem.camera : this.cameraSystem.camera;
4948
5149
  }
4949
5150
  /** {@link CameraController} instance */
4950
- get cameraController() {
5151
+ get controller() {
4951
5152
  return this.cameraSystem.controller;
4952
5153
  }
4953
5154
  /** Current camera zoom factor. */
@@ -4960,14 +5161,22 @@ class ViewportSystem {
4960
5161
  }
4961
5162
  /** Pixel to SVG scale factor */
4962
5163
  get pxToSvgScale() {
4963
- return 1 / (this.scaleFactor * this.zoomFactor);
5164
+ return this.renderer.isExternalMode ? this.externalSystem.pxToSvgScale(this.renderer.size) ?? this.prevPxToSvgScale ?? 1 : 1 / (this.scaleFactor * this.zoomFactor);
5165
+ }
5166
+ /**
5167
+ * Get bearing angle between current camera orientation and true north (in radians).
5168
+ * Angle is in range [0, 2π), going clockwise from north.
5169
+ */
5170
+ get bearing() {
5171
+ const tau = Math.PI * 2;
5172
+ return MathUtils.euclideanModulo(-this.controller.azimuthAngle, tau);
4964
5173
  }
4965
5174
  /**
4966
5175
  * Initializes the viewport and zoom bounds with the given scene definition.
4967
5176
  * @param sceneDef {@link SceneDef} scene definition
4968
5177
  */
4969
5178
  initViewport(sceneDef) {
4970
- this.sceneSystem.initScene(sceneDef.viewbox);
5179
+ if (!this.renderer.isExternalMode) this.sceneSystem.initScene(sceneDef.viewbox);
4971
5180
  this.cameraSystem.initCamera([0.1, sceneDef.viewbox.size.width > 1e5 ? 100 : 35]);
4972
5181
  }
4973
5182
  /** Updates the viewport when the renderer size changes. */
@@ -4981,77 +5190,110 @@ class ViewportSystem {
4981
5190
  updatePtScale() {
4982
5191
  const pxToSvgScale = this.pxToSvgScale;
4983
5192
  if (Math.abs(pxToSvgScale - (this.prevPxToSvgScale ?? 0)) < this.pxToSvgScaleThreshold) return;
4984
- if (this.renderer.debugLog) console.log("pxToSvgScale", +pxToSvgScale.toFixed(3));
5193
+ logger$2.debug(`pxToSvgScale ${+pxToSvgScale.toFixed(3)}`);
4985
5194
  this.eventSystem.emit("viewport:ptscale", pxToSvgScale);
4986
5195
  this.prevPxToSvgScale = pxToSvgScale;
4987
5196
  }
4988
5197
  /**
4989
5198
  * Gets the objects intersected by the raycaster.
4990
- * @param normalizedCoords raycast point in NDC (normalized device coordinates
5199
+ * @param ndcCoords raycast point in NDC (normalized device coordinates)
4991
5200
  * @returns Array of {@link Intersection} instances
4992
5201
  */
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;
5202
+ getIntersectedObjects(ndcCoords) {
5203
+ return this.pickingSystem.getIntersectedObjects(ndcCoords, this.scene, this.camera);
4998
5204
  }
4999
5205
  /**
5000
- * Converts a point from SVG coordinates to world coordinates.
5001
- * @param svgCoords Point in SVG coordinates
5206
+ * Converts a point from model coordinates to world coordinates.
5207
+ * @param modelCoords Point in model coordinates
5208
+ * @param out Optional output vector
5002
5209
  * @returns Point in world coordinates
5003
5210
  */
5004
- svgToWorld(svgCoords) {
5005
- const svg3D = new Vector3(...svgCoords, 0);
5006
- svg3D.applyMatrix4(this.sceneSystem.worldMatrix);
5007
- return new Vector2(svg3D.x, svg3D.y);
5211
+ modelToWorld(modelCoords, out = new Vector3()) {
5212
+ return this.sceneSystem.modelToWorld(modelCoords, out);
5008
5213
  }
5009
5214
  /**
5010
- * Converts a point from world coordinates to SVG coordinates. Z axis is ignored.
5215
+ * Converts a point from world coordinates to model coordinates. Z axis is ignored.
5011
5216
  * @param worldCoords Point in world coordinates
5012
- * @returns Point in SVG coordinates
5217
+ * @param out Optional output vector
5218
+ * @returns Point in model coordinates
5013
5219
  */
5014
- worldToSvg(worldCoords) {
5015
- const svgCoords = worldCoords.clone().applyMatrix4(this.sceneSystem.inverseWorldMatrix);
5016
- return new Vector2(svgCoords.x, svgCoords.y);
5220
+ worldToModel(worldCoords, out = new Vector2()) {
5221
+ return this.sceneSystem.worldToModel(worldCoords, out);
5017
5222
  }
5018
5223
  /**
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
5224
+ * Converts a point from screen coordinates to world space.
5225
+ * @param ndcCoords Point in NDC (normalized device coordinates)
5226
+ * @param out Optional output vector
5227
+ * @returns Point in world space
5023
5228
  */
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 };
5229
+ ndcToWorld(ndcCoords, out = new Vector3()) {
5230
+ return this.pickingSystem.intersectPlane(ndcCoords, this.camera, out);
5029
5231
  }
5030
5232
  /**
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
5233
+ * Convert canvas coordinates (relative to the canvas's top left corner)
5234
+ * to NDC (normalized device coordinates).
5235
+ * @param point object defining the coordinates relative to the canvas's top left corner
5236
+ * @param out Optional output vector
5237
+ * @returns Point in NDC space
5034
5238
  */
5035
- zoomFactorToDistance(zoomFactor) {
5036
- return this.cameraSystem.zoomFactorToDistance(zoomFactor);
5239
+ canvasToNDC(point, out = new Vector2()) {
5240
+ const [w, h] = this.renderer.size;
5241
+ if (w <= 0 || h <= 0) {
5242
+ logger$2.warn("canvasToNDC: renderer size is 0");
5243
+ return out.set(0, 0);
5244
+ }
5245
+ const dpr = this.renderer.context.getPixelRatio();
5246
+ const uv = [point.x / (w / dpr), point.y / (h / dpr)];
5247
+ const ndc = [uv[0] * 2 - 1, -uv[1] * 2 + 1];
5248
+ return out.set(MathUtils.clamp(ndc[0], -1, 1), MathUtils.clamp(ndc[1], -1, 1));
5249
+ }
5250
+ /**
5251
+ * Convert canvas coordinates (CSS pixels, relative to canvas top-left) to SVG coordinates.
5252
+ * @param point point in canvas space (CSS pixels)
5253
+ * @returns point in SVG coordinates or undefined if point is outside the SVG plane
5254
+ */
5255
+ canvasToSvg(point) {
5256
+ const vec2 = new Vector2();
5257
+ const vec3 = new Vector3();
5258
+ const ndcPoint = this.canvasToNDC(point, vec2);
5259
+ const worldPoint = this.ndcToWorld(ndcPoint, vec3);
5260
+ if (!worldPoint) return;
5261
+ return this.worldToModel(worldPoint, vec2);
5037
5262
  }
5038
5263
  /**
5039
- * Sets the external transform matrix.
5264
+ * Set static part of an svg -> px transform matrix
5040
5265
  * @param staticTransformMatrix static transform matrix to apply to the scene
5041
5266
  */
5042
- setExternalTransform(staticTransformMatrix) {
5043
- this.cameraSystem.externalCamera = new Camera();
5044
- this.externalStaticTransformMatrix.fromArray(staticTransformMatrix);
5267
+ setStaticTransform(staticTransformMatrix) {
5268
+ this.externalSystem.setStaticTransform(staticTransformMatrix);
5045
5269
  }
5046
5270
  /**
5047
- * Updates the external camera.
5048
- * @param dynamicTransformMatrix dynamic transform matrix to apply to the scene
5271
+ * Set dynamic part of an svg -> px transform matrix. Should be called every frame.
5272
+ * @param dynamicTransformMatrix dynamic transform matrix (changes every frame)
5049
5273
  */
5050
- updateExternalCamera(dynamicTransformMatrix) {
5051
- this.scene.matrix.fromArray(dynamicTransformMatrix).multiply(this.externalStaticTransformMatrix);
5052
- this.scene.matrixWorldNeedsUpdate = true;
5274
+ setDynamicTransform(dynamicTransformMatrix) {
5275
+ this.externalSystem.setDynamicTransform(dynamicTransformMatrix);
5276
+ }
5277
+ /**
5278
+ * Calculates the camera distance from the scene's plane for a given zoom factor.
5279
+ * @param zoomFactor Zoom factor
5280
+ * @returns Corresponding camera distance on the Z axis
5281
+ */
5282
+ zoomFactorToDistance(zoomFactor) {
5283
+ return this.cameraSystem.zoomFactorToDistance(zoomFactor);
5053
5284
  }
5054
5285
  }
5286
+ function asViewportAPI(viewportSystem) {
5287
+ return {
5288
+ canvasToSvg: viewportSystem.canvasToSvg.bind(viewportSystem),
5289
+ setStaticTransform: viewportSystem.setStaticTransform.bind(viewportSystem),
5290
+ setDynamicTransform: viewportSystem.setDynamicTransform.bind(viewportSystem)
5291
+ };
5292
+ }
5293
+ function eventToCanvas(event) {
5294
+ return { x: event.offsetX, y: event.offsetY };
5295
+ }
5296
+ const logger$1 = createLogger("controls");
5055
5297
  class ControlsSystem {
5056
5298
  /**
5057
5299
  * @param renderer {@link Renderer} instance
@@ -5063,7 +5305,7 @@ class ControlsSystem {
5063
5305
  this.renderer = renderer;
5064
5306
  this.viewportSystem = viewportSystem;
5065
5307
  this.interactionsSystem = interactionsSystem;
5066
- this.controller = viewportSystem.cameraController;
5308
+ this.controller = viewportSystem.controller;
5067
5309
  }
5068
5310
  /** Gesture handlers for camera controls. */
5069
5311
  get handlers() {
@@ -5092,7 +5334,9 @@ class ControlsSystem {
5092
5334
  const dpr = this.renderer.context.getPixelRatio();
5093
5335
  const visibleRect = this.renderer.visibleRect;
5094
5336
  const bearingAngle = -this.controller.azimuthAngle;
5095
- const worldRect = new Rect(this.viewportSystem.svgToWorld(rect.min), this.viewportSystem.svgToWorld(rect.max));
5337
+ const worldMin = this.viewportSystem.modelToWorld(rect.min);
5338
+ const worldMax = this.viewportSystem.modelToWorld(rect.max);
5339
+ const worldRect = new Rect(worldMin, worldMax);
5096
5340
  const worldPolygon = Polygon.fromRect(worldRect).rotate(bearingAngle, worldRect.center);
5097
5341
  const xValues = worldPolygon.vertices.map((p) => p.x);
5098
5342
  const yValues = worldPolygon.vertices.map((p) => p.y);
@@ -5100,7 +5344,11 @@ class ControlsSystem {
5100
5344
  [Math.min(...xValues), Math.min(...yValues)],
5101
5345
  [Math.max(...xValues), Math.max(...yValues)]
5102
5346
  );
5103
- const targetRect = visibleRect ? new Rect(visibleRect.min.clone().multiplyScalar(dpr), visibleRect.max.clone().multiplyScalar(dpr)) : new Rect([0, 0], this.renderer.size);
5347
+ if (sourceRect.size.x <= 0 || sourceRect.size.y <= 0) {
5348
+ logger$1.warn("zoomTo: sourceRect size is 0");
5349
+ return;
5350
+ }
5351
+ const targetRect = visibleRect ? new Rect(visibleRect.min.clone().multiplyScalar(dpr), visibleRect.max.clone().multiplyScalar(dpr)) : new Rect([0, 0], [...this.renderer.size]);
5104
5352
  if (paddingPercent) targetRect.addPadding(targetRect.size.x * paddingPercent, targetRect.size.y * paddingPercent);
5105
5353
  const zoomByWidth = targetRect.size.x / sourceRect.size.x;
5106
5354
  const zoomByHeight = targetRect.size.y / sourceRect.size.y;
@@ -5108,7 +5356,7 @@ class ControlsSystem {
5108
5356
  const zoom = maxZoom ? Math.min(minZoom, maxZoom) : minZoom;
5109
5357
  const translate = sourceRect.center;
5110
5358
  if (visibleRect) {
5111
- const offset = new Vector2(...this.renderer.size).multiplyScalar(0.5).sub(targetRect.center).multiplyScalar(1 / zoom).rotateAround({ x: 0, y: 0 }, bearingAngle);
5359
+ const offset = new Vector2(...this.renderer.size).multiplyScalar(0.5).sub(targetRect.center).multiplyScalar(1 / (zoom || 1)).rotateAround({ x: 0, y: 0 }, bearingAngle);
5112
5360
  translate.add(offset);
5113
5361
  }
5114
5362
  const enableTransition = !immediate;
@@ -5125,8 +5373,8 @@ class ControlsSystem {
5125
5373
  * @returns Promise that resolves when the pan animation completes
5126
5374
  */
5127
5375
  panBy(x, y, immediate) {
5128
- const svgOrigin = this.viewportSystem.svgToWorld(new Vector2(0, 0));
5129
- const svgOffset = this.viewportSystem.svgToWorld(new Vector2(x, y));
5376
+ const svgOrigin = this.viewportSystem.modelToWorld({ x: 0, y: 0 });
5377
+ const svgOffset = this.viewportSystem.modelToWorld({ x, y });
5130
5378
  const worldOffset = new Vector3(svgOffset.x - svgOrigin.x, svgOffset.y - svgOrigin.y, 0);
5131
5379
  const currentTarget = this.controller.getTarget(new Vector3());
5132
5380
  const newTarget = currentTarget.add(worldOffset);
@@ -5140,7 +5388,7 @@ class ControlsSystem {
5140
5388
  * @returns Promise that resolves when the pan animation completes
5141
5389
  */
5142
5390
  panTo(x, y, immediate) {
5143
- const worldCoords = this.viewportSystem.svgToWorld(new Vector2(x, y));
5391
+ const worldCoords = this.viewportSystem.modelToWorld({ x, y });
5144
5392
  return this.controller.moveTo(worldCoords.x, worldCoords.y, 0, !immediate);
5145
5393
  }
5146
5394
  /**
@@ -5169,7 +5417,6 @@ class ControlsSystem {
5169
5417
  const spherical = this.controller.getSpherical(new Spherical());
5170
5418
  const azimuthAngle = spherical.theta;
5171
5419
  const deltaAngleRad = shortestRotationAngle(targetAngleRad, azimuthAngle);
5172
- console.log("rollTo", deltaAngleRad * RAD2DEG, targetAngleRad * RAD2DEG, azimuthAngle * RAD2DEG);
5173
5420
  return this.rollBy(-deltaAngleRad * RAD2DEG, immediate);
5174
5421
  }
5175
5422
  /**
@@ -5238,8 +5485,8 @@ class ControlsSystem {
5238
5485
  this.controller.setBoundary(void 0);
5239
5486
  return;
5240
5487
  }
5241
- const worldMin = this.viewportSystem.svgToWorld(rect.min);
5242
- const worldMax = this.viewportSystem.svgToWorld(rect.max);
5488
+ const worldMin = this.viewportSystem.modelToWorld(rect.min);
5489
+ const worldMax = this.viewportSystem.modelToWorld(rect.max);
5243
5490
  const boundary = new Box3(new Vector3(worldMin.x, worldMin.y, 0), new Vector3(worldMax.x, worldMax.y, 0));
5244
5491
  this.controller.setBoundary(boundary);
5245
5492
  }
@@ -5249,9 +5496,9 @@ class ControlsSystem {
5249
5496
  */
5250
5497
  getCameraState() {
5251
5498
  const target = this.controller.getTarget(new Vector3());
5252
- const center = this.viewportSystem.worldToSvg(target);
5499
+ const center = this.viewportSystem.worldToModel(target);
5253
5500
  const zoom = this.viewportSystem.zoomFactor;
5254
- const roll = this.interactionsSystem.bearing * RAD2DEG;
5501
+ const roll = this.viewportSystem.bearing * RAD2DEG;
5255
5502
  const pitch = this.controller.polarAngle * RAD2DEG;
5256
5503
  const ptScale = this.viewportSystem.pxToSvgScale;
5257
5504
  return { center, zoom, roll, pitch, ptScale };
@@ -5348,21 +5595,6 @@ function asEventAPI(system) {
5348
5595
  clear: system.clear.bind(system)
5349
5596
  };
5350
5597
  }
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
5598
  class Handler {
5367
5599
  /**
5368
5600
  * @param viewportSystem The viewport system instance
@@ -5377,7 +5609,7 @@ class Handler {
5377
5609
  this.viewportSystem = viewportSystem;
5378
5610
  this.domElement = domElement;
5379
5611
  this.eventManager = eventManager;
5380
- this.controller = viewportSystem.cameraController;
5612
+ this.controller = viewportSystem.controller;
5381
5613
  }
5382
5614
  /**
5383
5615
  * Per-frame update for this handler.
@@ -5588,13 +5820,15 @@ class PitchHandler extends Handler {
5588
5820
  __publicField(this, "isValid");
5589
5821
  __publicField(this, "firstMove");
5590
5822
  __publicField(this, "lastPoints");
5823
+ __publicField(this, "p0", new Vector2());
5824
+ __publicField(this, "p1", new Vector2());
5591
5825
  __publicField(this, "prevTwoFingerAction");
5592
5826
  __publicField(this, "onPitchStart", (e) => {
5593
5827
  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))) {
5828
+ this.p0.copy(eventToCanvas(pointers[0]));
5829
+ this.p1.copy(eventToCanvas(pointers[1]));
5830
+ this.lastPoints = [new Vector2().copy(this.p0), new Vector2().copy(this.p1)];
5831
+ if (this.isVertical(this.p0.sub(this.p1))) {
5598
5832
  this.isValid = false;
5599
5833
  }
5600
5834
  });
@@ -5609,18 +5843,19 @@ class PitchHandler extends Handler {
5609
5843
  const lastPoints = this.lastPoints;
5610
5844
  if (!lastPoints) return;
5611
5845
  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);
5846
+ this.p0.copy(eventToCanvas(pointers[0]));
5847
+ this.p1.copy(eventToCanvas(pointers[1]));
5848
+ this.p0.sub(lastPoints[0]);
5849
+ this.p1.sub(lastPoints[1]);
5850
+ this.isValid = this.gestureBeginsVertically(this.p0, this.p1, e.timeStamp);
5617
5851
  if (!this.isValid) return;
5618
5852
  if (this.prevTwoFingerAction === void 0) {
5619
5853
  this.prevTwoFingerAction = this.controller.touches.two;
5620
5854
  this.controller.touches.two = CameraController.ACTION.NONE;
5621
5855
  }
5622
- this.lastPoints = [p0, p1];
5623
- const yDeltaAverage = (vectorA.y + vectorB.y) / 2;
5856
+ lastPoints[0].add(this.p0);
5857
+ lastPoints[1].add(this.p1);
5858
+ const yDeltaAverage = (this.p0.y + this.p1.y) / 2;
5624
5859
  const degreesPerPixelMoved = -0.5;
5625
5860
  const deltaAngle = yDeltaAverage * degreesPerPixelMoved * DEG2RAD$1;
5626
5861
  void this.controller.rotatePolarTo(this.controller.polarAngle + deltaAngle, false);
@@ -5665,12 +5900,10 @@ class PitchHandler extends Handler {
5665
5900
  updatePolarAngles() {
5666
5901
  this.controller.minPolarAngle = this.minPitch * DEG2RAD$1;
5667
5902
  this.controller.maxPolarAngle = this.maxPitch * DEG2RAD$1;
5668
- if (this.controller.polarAngle < this.controller.minPolarAngle) {
5903
+ if (this.controller.polarAngle < this.controller.minPolarAngle)
5669
5904
  void this.controller.rotatePolarTo(this.controller.minPolarAngle, false);
5670
- }
5671
- if (this.controller.polarAngle > this.controller.maxPolarAngle) {
5905
+ if (this.controller.polarAngle > this.controller.maxPolarAngle)
5672
5906
  void this.controller.rotatePolarTo(this.controller.maxPolarAngle, false);
5673
- }
5674
5907
  }
5675
5908
  gestureBeginsVertically(vectorA, vectorB, timeStamp) {
5676
5909
  if (this.isValid !== void 0) return this.isValid;
@@ -5702,35 +5935,36 @@ class RollHandler extends Handler {
5702
5935
  __publicField(this, "rotationThreshold", 25);
5703
5936
  // Threshold tracking (Mapbox-style)
5704
5937
  __publicField(this, "startVector");
5705
- __publicField(this, "vector");
5938
+ __publicField(this, "vector", new Vector2());
5939
+ __publicField(this, "p0", new Vector2());
5940
+ __publicField(this, "p1", new Vector2());
5706
5941
  __publicField(this, "minDiameter", 0);
5707
5942
  __publicField(this, "prevAngle", 0);
5708
5943
  // Camera and pivot vectors
5944
+ __publicField(this, "pivotNDC", new Vector2());
5709
5945
  __publicField(this, "pivotWorld", new Vector3());
5710
5946
  __publicField(this, "targetWorld", new Vector3());
5711
5947
  __publicField(this, "cameraPosition", new Vector3());
5712
5948
  __publicField(this, "cameraForward", new Vector3());
5713
5949
  __publicField(this, "rotationMatrix", new Matrix4());
5714
5950
  __publicField(this, "onRotateStart", (e) => {
5715
- console.log("onRotateStart");
5716
5951
  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);
5952
+ this.p0.copy(eventToCanvas(pointers[0]));
5953
+ this.p1.copy(eventToCanvas(pointers[1]));
5954
+ this.startVector = new Vector2().copy(this.p0).sub(this.p1);
5720
5955
  this.minDiameter = this.startVector.length();
5721
5956
  });
5722
5957
  __publicField(this, "onRotateEnd", () => {
5723
5958
  this.isRolling = false;
5724
5959
  this.startVector = void 0;
5725
- this.vector = void 0;
5726
5960
  this.minDiameter = 0;
5727
5961
  });
5728
5962
  __publicField(this, "onRotate", (e) => {
5729
5963
  const pointers = e.pointers;
5730
5964
  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);
5965
+ this.p0.copy(eventToCanvas(pointers[0]));
5966
+ this.p1.copy(eventToCanvas(pointers[1]));
5967
+ this.vector.copy(this.p0).sub(this.p1);
5734
5968
  if (this.isBelowThreshold(this.vector)) return;
5735
5969
  this.isRolling = true;
5736
5970
  this.prevAngle = e.rotation;
@@ -5738,7 +5972,7 @@ class RollHandler extends Handler {
5738
5972
  const deltaAngle = (e.rotation - this.prevAngle) * -DEG2RAD$1;
5739
5973
  this.prevAngle = e.rotation;
5740
5974
  if (Math.abs(deltaAngle) < 1e-3) return;
5741
- this.setPivot(e);
5975
+ if (!this.setPivot(e)) return;
5742
5976
  this.rotationMatrix.makeRotationZ(deltaAngle);
5743
5977
  this.cameraPosition.sub(this.pivotWorld).applyMatrix4(this.rotationMatrix).add(this.pivotWorld);
5744
5978
  this.cameraForward.applyMatrix4(this.rotationMatrix);
@@ -5786,13 +6020,12 @@ class RollHandler extends Handler {
5786
6020
  this.eventManager.off("rotateend", this.onRotateEnd);
5787
6021
  }
5788
6022
  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);
6023
+ this.viewportSystem.canvasToNDC(e.offsetCenter, this.pivotNDC);
6024
+ if (!this.viewportSystem.ndcToWorld(this.pivotNDC, this.pivotWorld)) return false;
5793
6025
  this.controller.getPosition(this.cameraPosition);
5794
6026
  this.controller.getTarget(this.targetWorld);
5795
6027
  this.cameraForward.copy(this.targetWorld).sub(this.cameraPosition);
6028
+ return true;
5796
6029
  }
5797
6030
  /**
5798
6031
  * Check if rotation is below threshold (Mapbox-style).
@@ -5812,7 +6045,6 @@ class RollHandler extends Handler {
5812
6045
  const startVector = this.startVector;
5813
6046
  if (!startVector) return false;
5814
6047
  const bearingDeltaSinceStart = this.getBearingDelta(vector, startVector);
5815
- console.log("bearingDeltaSinceStart", vector, startVector);
5816
6048
  return Math.abs(bearingDeltaSinceStart) < threshold;
5817
6049
  }
5818
6050
  /**
@@ -5824,16 +6056,6 @@ class RollHandler extends Handler {
5824
6056
  getBearingDelta(a, b) {
5825
6057
  return a.angleTo(b) * RAD2DEG;
5826
6058
  }
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
6059
  }
5838
6060
  class ZoomHandler extends Handler {
5839
6061
  reset(enableTransition = true) {
@@ -5868,12 +6090,16 @@ class InteractionsSystem {
5868
6090
  __publicField(this, "handlers");
5869
6091
  __publicField(this, "handlerArray");
5870
6092
  __publicField(this, "canvas");
5871
- __publicField(this, "mousePointer", new Vector2());
5872
6093
  __publicField(this, "eventManager");
6094
+ __publicField(this, "mousePointerNDC", new Vector2());
6095
+ __publicField(this, "mousePointerWorld", new Vector3());
6096
+ __publicField(this, "mousePointerModel", new Vector2());
6097
+ __publicField(this, "canvasListeners");
5873
6098
  __publicField(this, "dragStart");
5874
6099
  __publicField(this, "dragThreshold", 15);
5875
6100
  __publicField(this, "isDragging", false);
5876
6101
  __publicField(this, "prevBearing", 0);
6102
+ this.renderer = renderer;
5877
6103
  this.events = events;
5878
6104
  this.viewportSystem = viewportSystem;
5879
6105
  this.layerSystem = layerSystem;
@@ -5881,8 +6107,6 @@ class InteractionsSystem {
5881
6107
  this.eventManager = new EventManager(this.canvas, {
5882
6108
  recognizers: [Rotate, [Pan, { event: "pitch", pointers: 2 }, "rotate"]]
5883
6109
  });
5884
- this.configureCameraControls();
5885
- this.attachCanvasListeners();
5886
6110
  const handlers = {
5887
6111
  pan: new PanHandler(viewportSystem, this.canvas, this.eventManager),
5888
6112
  zoom: new ZoomHandler(viewportSystem, this.canvas, this.eventManager),
@@ -5891,17 +6115,32 @@ class InteractionsSystem {
5891
6115
  };
5892
6116
  this.handlers = handlers;
5893
6117
  this.handlerArray = Object.values(handlers);
5894
- this.handlers.pan.enable();
5895
- this.handlers.zoom.enable();
5896
6118
  }
5897
6119
  /**
5898
- * Get bearing angle between current camera orientation and true north (in radians).
5899
- * Angle is in range [0, 2π), going clockwise from north.
6120
+ * Initializes the interactions system and attaches event listeners.
6121
+ * Should be called once as part of renderer initialization.
5900
6122
  */
5901
- // TODO: Move somewhere else
5902
- get bearing() {
5903
- const tau = Math.PI * 2;
5904
- return MathUtils.euclideanModulo(-this.viewportSystem.cameraController.azimuthAngle, tau);
6123
+ init() {
6124
+ this.attachCanvasListeners();
6125
+ if (!this.renderer.isExternalMode) {
6126
+ const controller = this.viewportSystem.controller;
6127
+ controller.connect(this.canvas);
6128
+ controller.addEventListener("transitionstart", () => {
6129
+ this.events.emit("navigation:change");
6130
+ });
6131
+ this.handlers.pan.enable();
6132
+ this.handlers.zoom.enable();
6133
+ }
6134
+ }
6135
+ /**
6136
+ * Disposes the interactions system.
6137
+ * WARNING: This method is final and cannot be undone. To re-enable interactions, create a new renderer instance.
6138
+ */
6139
+ dispose() {
6140
+ for (const handler of this.handlerArray) handler.disable();
6141
+ this.viewportSystem.controller.disconnect();
6142
+ this.detachCanvasListeners();
6143
+ this.eventManager.destroy();
5905
6144
  }
5906
6145
  /**
5907
6146
  * Update camera position and directions.
@@ -5910,87 +6149,75 @@ class InteractionsSystem {
5910
6149
  * @returns true if re-rendering is needed
5911
6150
  */
5912
6151
  updateControls(delta) {
5913
- let needsUpdate = this.viewportSystem.cameraController.update(delta);
6152
+ let needsUpdate = this.viewportSystem.controller.update(delta);
5914
6153
  for (const handler of this.handlerArray) {
5915
6154
  if (handler.isEnabled()) {
5916
6155
  needsUpdate = handler.update(delta) || needsUpdate;
5917
6156
  }
5918
6157
  }
5919
- if (this.bearing !== this.prevBearing) {
5920
- this.prevBearing = this.bearing;
5921
- this.events.emit("navigation:roll", this.bearing);
6158
+ if (this.viewportSystem.bearing !== this.prevBearing) {
6159
+ this.prevBearing = this.viewportSystem.bearing;
6160
+ this.events.emit("navigation:roll", this.viewportSystem.bearing);
5922
6161
  }
5923
6162
  return needsUpdate;
5924
6163
  }
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
6164
  attachCanvasListeners() {
5953
- this.canvas.addEventListener("pointerdown", (event) => {
6165
+ if (this.canvasListeners) return;
6166
+ const pointerdown = (event) => {
5954
6167
  this.isDragging = false;
5955
6168
  this.dragStart = { x: event.offsetX, y: event.offsetY };
5956
- });
5957
- this.canvas.addEventListener("pointerup", (event) => {
6169
+ };
6170
+ const pointerup = (event) => {
5958
6171
  if (!this.dragStart) return;
5959
6172
  const dX = event.offsetX - this.dragStart.x;
5960
6173
  const dY = event.offsetY - this.dragStart.y;
5961
6174
  this.dragStart = void 0;
5962
6175
  if (dX * dX + dY * dY > this.dragThreshold) this.isDragging = true;
5963
- });
6176
+ };
5964
6177
  const mouseEventsMap = {
5965
6178
  mousemove: "pointer:move",
5966
6179
  mouseout: "pointer:out",
5967
6180
  click: "pointer:click"
5968
6181
  };
5969
6182
  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
- });
6183
+ const sharedMouseHandler = (type, event) => {
6184
+ const eventType = mouseEventsMap[type];
6185
+ const isDragging = type === "click" && this.isDragging;
6186
+ const hasListeners = this.events.hasListeners(eventType);
6187
+ if (isDragging || !hasListeners) return;
6188
+ const mousePointer = eventToCanvas(event);
6189
+ this.viewportSystem.canvasToNDC(mousePointer, this.mousePointerNDC);
6190
+ if (!this.viewportSystem.ndcToWorld(this.mousePointerNDC, this.mousePointerWorld)) return;
6191
+ const intersections = this.viewportSystem.getIntersectedObjects(this.mousePointerNDC);
6192
+ const point = this.viewportSystem.worldToModel(this.mousePointerWorld, this.mousePointerModel);
6193
+ const defs = this.layerSystem.getIntersectedDefs(intersections);
6194
+ this.events.emit(eventType, { event, point: { x: point.x, y: point.y }, defs });
6195
+ };
6196
+ const mousemove = (event) => sharedMouseHandler("mousemove", event);
6197
+ const mouseout = (event) => sharedMouseHandler("mouseout", event);
6198
+ const click = (event) => sharedMouseHandler("click", event);
6199
+ const canvasListeners = { pointerdown, pointerup, mousemove, mouseout, click };
6200
+ this.canvas.addEventListener("pointerdown", pointerdown);
6201
+ this.canvas.addEventListener("pointerup", pointerup);
6202
+ mouseEventKeys.forEach((type) => this.canvas.addEventListener(type, canvasListeners[type]));
6203
+ this.canvasListeners = canvasListeners;
6204
+ }
6205
+ detachCanvasListeners() {
6206
+ if (!this.canvasListeners) return;
6207
+ this.canvas.removeEventListener("pointerdown", this.canvasListeners.pointerdown);
6208
+ this.canvas.removeEventListener("pointerup", this.canvasListeners.pointerup);
6209
+ this.canvas.removeEventListener("mousemove", this.canvasListeners.mousemove);
6210
+ this.canvas.removeEventListener("mouseout", this.canvasListeners.mouseout);
6211
+ this.canvas.removeEventListener("click", this.canvasListeners.click);
6212
+ this.canvasListeners = void 0;
5984
6213
  }
5985
6214
  }
6215
+ const logger = createLogger("renderer");
5986
6216
  class Renderer {
5987
6217
  /**
5988
6218
  * @param opts {@link RendererOptions}
5989
6219
  */
5990
6220
  constructor(opts) {
5991
- /** Whether to log debug information */
5992
- __publicField(this, "debugLog");
5993
- //FIXME: Add https://www.npmjs.com/package/debug
5994
6221
  /** {@link HTMLCanvasElement} that this renderer is rendering to */
5995
6222
  __publicField(this, "canvas");
5996
6223
  __publicField(this, "ui");
@@ -6000,16 +6227,19 @@ class Renderer {
6000
6227
  __publicField(this, "viewportSystem");
6001
6228
  __publicField(this, "interactionsSystem");
6002
6229
  __publicField(this, "controlsSystem");
6230
+ __publicField(this, "controlsAPI");
6231
+ __publicField(this, "eventsAPI");
6232
+ __publicField(this, "viewportAPI");
6003
6233
  __publicField(this, "clock");
6004
6234
  __publicField(this, "renderer");
6005
- __publicField(this, "viewport");
6235
+ __publicField(this, "visibleRectValue");
6236
+ __publicField(this, "memoryInfoExtension", null);
6237
+ __publicField(this, "memoryInfo");
6238
+ __publicField(this, "initialized", false);
6239
+ __publicField(this, "disposed", false);
6006
6240
  __publicField(this, "needsRedraw", true);
6007
- __publicField(this, "memoryInfoExtension");
6008
- __publicField(this, "memoryInfo", "");
6009
- var _a2, _b;
6010
- const { canvas, gl, debugLog = false, ui } = opts;
6241
+ const { canvas, gl, ui } = opts;
6011
6242
  this.canvas = canvas;
6012
- this.debugLog = debugLog;
6013
6243
  this.ui = ui;
6014
6244
  this.gl = gl;
6015
6245
  const rendererOptions = {
@@ -6021,42 +6251,86 @@ class Renderer {
6021
6251
  this.renderer = new WebGLRenderer(rendererOptions);
6022
6252
  this.renderer.setSize(this.canvas.clientWidth, this.canvas.clientHeight, false);
6023
6253
  this.renderer.setPixelRatio(window.devicePixelRatio);
6254
+ this.renderer.autoClear = !this.isExternalMode;
6024
6255
  this.eventSystem = new EventSystem();
6025
6256
  this.viewportSystem = new ViewportSystem(this, this.eventSystem);
6026
6257
  this.layerSystem = new LayerSystem(this);
6027
6258
  this.interactionsSystem = new InteractionsSystem(this, this.eventSystem, this.viewportSystem, this.layerSystem);
6028
6259
  this.controlsSystem = new ControlsSystem(this, this.viewportSystem, this.interactionsSystem);
6029
- this.memoryInfoExtension = this.renderer.getContext().getExtension("GMAN_webgl_memory");
6030
6260
  this.canvas.addEventListener("webglcontextlost", (e) => this.onContextLost(e), false);
6031
6261
  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()));
6262
+ this.initContext(this.renderer.getContext());
6033
6263
  BatchedMesh.useMultiDraw = this.renderer.extensions.has("WEBGL_multi_draw");
6034
6264
  }
6035
6265
  /**
6036
6266
  * {@link ControlsAPI} instance for controlling the viewport
6037
6267
  */
6038
6268
  get controls() {
6039
- return asControlsAPI(this.controlsSystem);
6269
+ if (this.controlsAPI) return this.controlsAPI;
6270
+ const api = asControlsAPI(this.controlsSystem);
6271
+ const guard = (name) => this.assertInitialized(`controls.${name}`) && this.assertNotDisposed(`controls.${name}`) && this.assertNotExternalMode(`controls.${name}`);
6272
+ this.controlsAPI = {
6273
+ handlers: api.handlers,
6274
+ configure: api.configure,
6275
+ zoomBy: guardFn(guard, api.zoomBy, Promise.resolve()),
6276
+ zoomTo: guardFn(guard, api.zoomTo, Promise.resolve()),
6277
+ panBy: guardFn(guard, api.panBy, Promise.resolve()),
6278
+ panTo: guardFn(guard, api.panTo, Promise.resolve()),
6279
+ rollBy: guardFn(guard, api.rollBy, Promise.resolve()),
6280
+ rollTo: guardFn(guard, api.rollTo, Promise.resolve()),
6281
+ pitchBy: guardFn(guard, api.pitchBy, Promise.resolve()),
6282
+ pitchTo: guardFn(guard, api.pitchTo, Promise.resolve()),
6283
+ resetCamera: guardFn(guard, api.resetCamera, Promise.resolve()),
6284
+ setCameraBounds: guardFn(guard, api.setCameraBounds),
6285
+ getCameraState: guardFn(guard, api.getCameraState, {
6286
+ center: { x: 0, y: 0 },
6287
+ roll: 0,
6288
+ pitch: 0,
6289
+ zoom: 1,
6290
+ ptScale: 1
6291
+ })
6292
+ };
6293
+ return this.controlsAPI;
6040
6294
  }
6041
6295
  /**
6042
6296
  * {@link EventsAPI} instance for subscribing to internal events
6043
6297
  */
6044
6298
  get events() {
6045
- return asEventAPI(this.eventSystem);
6299
+ if (this.eventsAPI) return this.eventsAPI;
6300
+ this.eventsAPI = asEventAPI(this.eventSystem);
6301
+ return this.eventsAPI;
6302
+ }
6303
+ /**
6304
+ * {@link ViewportAPI} instance for view transforms and external transforms.
6305
+ */
6306
+ get viewport() {
6307
+ if (this.viewportAPI) return this.viewportAPI;
6308
+ const api = asViewportAPI(this.viewportSystem);
6309
+ const guard = (name) => this.assertInitialized(`viewport.${name}`) && this.assertNotDisposed(`viewport.${name}`);
6310
+ const guardExternal = (name) => guard(name) && this.assertExternalMode(`viewport.${name}`);
6311
+ this.viewportAPI = {
6312
+ canvasToSvg: guardFn(guard, api.canvasToSvg, { x: 0, y: 0 }),
6313
+ setStaticTransform: guardFn(guardExternal, api.setStaticTransform),
6314
+ setDynamicTransform: guardFn(guardExternal, api.setDynamicTransform)
6315
+ };
6316
+ return this.viewportAPI;
6046
6317
  }
6047
6318
  /**
6048
6319
  * Optional sub-rectangle of the viewport that is used for positioning scene's viewbox
6049
6320
  */
6050
6321
  get visibleRect() {
6051
- return this.viewport;
6322
+ return this.visibleRectValue;
6052
6323
  }
6053
6324
  /**
6054
6325
  * Optional sub-rectangle of the viewport that is used for positioning scene's viewbox
6055
6326
  */
6056
6327
  set visibleRect(rect) {
6057
- this.viewport = rect;
6058
- this.viewportSystem.updateViewport();
6059
- this.update();
6328
+ if (!this.assertNotExternalMode("visibleRect")) return;
6329
+ this.visibleRectValue = rect;
6330
+ if (this.initialized && !this.disposed) {
6331
+ this.viewportSystem.updateViewport();
6332
+ this.update();
6333
+ }
6060
6334
  }
6061
6335
  /**
6062
6336
  * Underlying {@link WebGLRenderer} instance
@@ -6072,64 +6346,75 @@ class Renderer {
6072
6346
  return [this.canvas.width, this.canvas.height];
6073
6347
  }
6074
6348
  /**
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
6349
+ * Returns true if the renderer is in external mode, meaning that webgl context is managed outside of the renderer
6350
+ * (for example, when using Mapbox GL JS as a host context). In this mode renderer will not clear the canvas before
6351
+ * rendering, and will not automatically compute scene and camera transformations. Clients are responsible for setting
6352
+ * the matrices manually by using {@link Renderer.viewport} methods.
6077
6353
  */
6078
- // TODO: Move somewhere
6079
- setExternalTransform(staticTransformMatrix) {
6080
- this.renderer.autoClear = false;
6081
- this.interactionsSystem.disconnect();
6082
- this.viewportSystem.setExternalTransform(staticTransformMatrix);
6354
+ get isExternalMode() {
6355
+ return this.gl !== void 0;
6083
6356
  }
6084
6357
  /**
6085
- * Update scene matrix from dynamic transform matrix.
6086
- * @param dynamicTransformMatrix dynamic transform matrix (changes every frame)
6358
+ * Returns true if the renderer is initialized, meaning that the viewport and scene have been set up.
6359
+ */
6360
+ get isInitialized() {
6361
+ return this.initialized;
6362
+ }
6363
+ /**
6364
+ * Returns true if the renderer is disposed, meaning that all WebGL resources have been released.
6087
6365
  */
6088
- updateExternalCamera(dynamicTransformMatrix) {
6089
- this.viewportSystem.updateExternalCamera(dynamicTransformMatrix);
6366
+ get isDisposed() {
6367
+ return this.disposed;
6090
6368
  }
6091
6369
  /**
6092
- * Initialize the scene and start the rendering loop
6370
+ * Initializes viewport and scene with the given scene definition.
6371
+ * Should be called once on startup. Repeated calls will produce console warnings.
6093
6372
  * @param sceneDef {@link SceneDef} to render
6094
- * @param startLoop whether to start the rendering loop
6095
6373
  */
6096
- start(sceneDef, startLoop = true) {
6097
- this.clock.start();
6374
+ init(sceneDef) {
6375
+ if (!this.assertNotDisposed("init")) return;
6376
+ if (!this.assertNotInitialized("init")) return;
6098
6377
  this.viewportSystem.initViewport(sceneDef);
6378
+ this.interactionsSystem.init();
6099
6379
  this.viewportSystem.scene.add(this.layerSystem.buildScene(sceneDef));
6100
- if (startLoop) this.renderer.setAnimationLoop(() => this.render());
6380
+ this.initialized = true;
6381
+ logger.info("initialized");
6382
+ }
6383
+ /**
6384
+ * Start the rendering loop
6385
+ */
6386
+ start() {
6387
+ if (!this.assertNotDisposed("start")) return;
6388
+ if (!this.assertInitialized("start")) return;
6389
+ if (this.clock.running) return;
6390
+ this.clock.start();
6391
+ this.renderer.setAnimationLoop(() => this.render());
6392
+ logger.info("started");
6101
6393
  }
6102
6394
  /**
6103
6395
  * Update the given defs to make them reflect the current state
6104
6396
  * @param defs {@link RenderableDef} array to update
6105
6397
  */
6106
6398
  update(...defs) {
6399
+ if (!this.assertNotDisposed("update")) return;
6400
+ if (!this.assertInitialized("update")) return;
6107
6401
  this.layerSystem.updateDefs(defs);
6108
6402
  this.needsRedraw = true;
6109
6403
  }
6110
6404
  /**
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
6405
+ * Render a single frame
6122
6406
  */
6123
6407
  render() {
6124
6408
  var _a2, _b, _c, _d, _e, _f;
6409
+ if (!this.assertNotDisposed("render")) return;
6410
+ if (!this.assertInitialized("render")) return;
6125
6411
  (_b = (_a2 = this.ui) == null ? void 0 : _a2.stats) == null ? void 0 : _b.begin();
6126
- if (this.gl !== void 0) this.renderer.resetState();
6412
+ if (this.isExternalMode) this.renderer.resetState();
6127
6413
  else this.resizeCanvasToDisplaySize();
6128
6414
  this.viewportSystem.updatePtScale();
6129
- const delta = this.clock.getDelta();
6130
6415
  const hasDefsUpdated = this.layerSystem.processPendingUpdates();
6131
- const hasControlsUpdated = this.interactionsSystem.updateControls(delta);
6132
- const needsRedraw = this.needsRedraw || hasControlsUpdated || hasDefsUpdated || this.ui;
6416
+ const hasControlsUpdated = this.interactionsSystem.updateControls(this.clock.getDelta());
6417
+ const needsRedraw = this.needsRedraw || hasControlsUpdated || hasDefsUpdated || this.isExternalMode || this.ui;
6133
6418
  if (needsRedraw) {
6134
6419
  this.renderer.render(this.viewportSystem.scene, this.viewportSystem.camera);
6135
6420
  this.needsRedraw = false;
@@ -6144,13 +6429,25 @@ class Renderer {
6144
6429
  stop() {
6145
6430
  this.renderer.setAnimationLoop(null);
6146
6431
  this.clock.stop();
6432
+ logger.info("stopped");
6147
6433
  }
6148
6434
  /**
6149
- * Dispose all WebGL resources
6435
+ * Dispose all WebGL resources. This calls {@link Renderer.stop} internally.
6436
+ * WARNING: This method is final and cannot be undone. Attempting to use the renderer after calling this method
6437
+ * will result in a console warning and no-op methods. If you need to re-initialize the renderer, create a new instance.
6150
6438
  */
6151
6439
  dispose() {
6440
+ if (this.disposed) return;
6441
+ this.stop();
6442
+ this.interactionsSystem.dispose();
6152
6443
  this.layerSystem.disposeScene();
6153
6444
  this.viewportSystem.scene.clear();
6445
+ this.renderer.clear();
6446
+ this.renderer.info.reset();
6447
+ this.renderer.dispose();
6448
+ this.updateMemoryInfo();
6449
+ this.disposed = true;
6450
+ logger.info("disposed");
6154
6451
  }
6155
6452
  // https://webgl2fundamentals.org/webgl/lessons/webgl-resizing-the-canvas.html
6156
6453
  resizeCanvasToDisplaySize() {
@@ -6159,7 +6456,7 @@ class Renderer {
6159
6456
  const displayWidth = Math.floor(width * dpr);
6160
6457
  const displayHeight = Math.floor(height * dpr);
6161
6458
  if (this.canvas.width !== displayWidth || this.canvas.height !== displayHeight || this.renderer.getPixelRatio() !== dpr) {
6162
- if (this.debugLog) console.log("renderer resize", width, height, dpr);
6459
+ logger.debug("renderer resize", width, height, dpr);
6163
6460
  this.renderer.setSize(width, height, false);
6164
6461
  this.renderer.setPixelRatio(dpr);
6165
6462
  this.viewportSystem.updateViewport();
@@ -6167,35 +6464,93 @@ class Renderer {
6167
6464
  }
6168
6465
  }
6169
6466
  updateMemoryInfo() {
6170
- var _a2;
6467
+ var _a2, _b, _c, _d;
6171
6468
  if (this.memoryInfoExtension && ((_a2 = this.ui) == null ? void 0 : _a2.memoryInfoPanel)) {
6172
6469
  const memoryInfo = this.memoryInfoExtension.getMemoryInfo();
6173
6470
  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) {
6471
+ 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"])) {
6472
+ const elapsedTime = this.clock.getElapsedTime() * 1e3;
6177
6473
  const logMarker = `memoryInfo [${elapsedTime.toFixed(2)}ms since start]`;
6178
- if (this.debugLog) console.log(logMarker, memoryInfo);
6179
- console.log("Buffers", this.memoryInfoExtension.getResourcesInfo(WebGLBuffer));
6180
- this.memoryInfo = memoryInfoContent;
6474
+ logger.debug(logMarker, memoryInfo);
6475
+ this.memoryInfo = memoryInfo;
6476
+ this.ui.memoryInfoPanel.textContent = JSON.stringify(memoryInfo, null, 2);
6181
6477
  }
6182
- this.ui.memoryInfoPanel.textContent = JSON.stringify(memoryInfo, null, 2);
6183
6478
  }
6184
6479
  }
6185
- // FIXME: Test with mapbox
6186
6480
  onContextLost(event) {
6481
+ var _a2, _b;
6187
6482
  event.preventDefault();
6188
- console.log("webglcontextlost event", event);
6189
- this.renderer.setAnimationLoop(null);
6190
- this.clock.stop();
6191
- if (this.ui) setTimeout(() => this.renderer.forceContextRestore(), 0);
6483
+ logger.debug("webglcontextlost event", event);
6484
+ const stats = (_a2 = this.ui) == null ? void 0 : _a2.stats;
6485
+ const context = this.renderer.getContext();
6486
+ if (stats && "deleteQuery" in context) {
6487
+ const gpuQueries = stats.gpuQueries;
6488
+ for (const queryInfo of gpuQueries) {
6489
+ this.renderer.getContext().deleteQuery(queryInfo.query);
6490
+ }
6491
+ stats.gpuQueries = [];
6492
+ if (stats.gpuPanel) {
6493
+ stats.dom.removeChild((_b = stats.gpuPanel) == null ? void 0 : _b.canvas);
6494
+ stats.gpuPanel = null;
6495
+ stats._panelId--;
6496
+ }
6497
+ }
6498
+ this.stop();
6192
6499
  }
6193
6500
  onContextRestored(event) {
6194
6501
  event.preventDefault();
6195
- console.log("webglcontextrestored event", event);
6196
- this.renderer.setAnimationLoop(() => this.render());
6197
- this.clock.start();
6502
+ logger.debug("webglcontextrestored event", event);
6503
+ this.initContext(this.renderer.getContext());
6504
+ this.needsRedraw = true;
6505
+ this.start();
6506
+ }
6507
+ initContext(context) {
6508
+ var _a2, _b;
6509
+ this.memoryInfoExtension = context.getExtension("GMAN_webgl_memory");
6510
+ void ((_b = (_a2 = this.ui) == null ? void 0 : _a2.stats) == null ? void 0 : _b.init(context));
6511
+ }
6512
+ assertNotDisposed(funcName) {
6513
+ if (this.disposed) {
6514
+ logger.warn(`[${funcName}]: Renderer is used after being disposed. Please create a new instance.`);
6515
+ return false;
6516
+ }
6517
+ return true;
6198
6518
  }
6519
+ assertInitialized(funcName) {
6520
+ if (!this.initialized) {
6521
+ logger.warn(`${funcName}: Renderer is not initialized. Please call init() before using it.`);
6522
+ return false;
6523
+ }
6524
+ return true;
6525
+ }
6526
+ assertNotInitialized(funcName) {
6527
+ if (this.initialized) {
6528
+ logger.warn(`${funcName}: Renderer is already initialized. Please call init() only once.`);
6529
+ return false;
6530
+ }
6531
+ return true;
6532
+ }
6533
+ assertNotExternalMode(funcName) {
6534
+ if (this.isExternalMode) {
6535
+ logger.warn(`${funcName}: This operation is not supported in external mode.`);
6536
+ return false;
6537
+ }
6538
+ return true;
6539
+ }
6540
+ assertExternalMode(funcName) {
6541
+ if (!this.isExternalMode) {
6542
+ logger.warn(`${funcName}: This operation is only supported in external mode.`);
6543
+ return false;
6544
+ }
6545
+ return true;
6546
+ }
6547
+ }
6548
+ function guardFn(guard, fn, ...fallback) {
6549
+ return (...args) => {
6550
+ const name = fn.name.split(" ").at(-1);
6551
+ if (!guard(name)) return fallback[0];
6552
+ return fn(...args);
6553
+ };
6199
6554
  }
6200
6555
  export {
6201
6556
  Polygon,