@expofp/renderer 3.1.6 → 3.2.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.
package/dist/index.js CHANGED
@@ -1,11 +1,15 @@
1
- import { AlwaysDepth, AmbientLight, BatchedMesh, Box3, BufferAttribute, BufferGeometry, Camera, Clock, Color, DataTexture, DirectionalLight, DoubleSide, DynamicDrawUsage, FloatType, Frustum, Group, IntType, LessEqualDepth, LinearSRGBColorSpace, MathUtils, Matrix4, Mesh, MeshBasicMaterial, MeshPhongMaterial, PerspectiveCamera, Plane, PlaneGeometry, Quaternion, RGBAFormat, RGBAIntegerFormat, RGFormat, RGIntegerFormat, Raycaster, RedFormat, RedIntegerFormat, SRGBColorSpace, Scene, Sphere, Spherical, StreamDrawUsage, Texture, UnsignedIntType, Vector2, Vector3, Vector4, WebGLRenderer } from "three";
1
+ import { AlwaysDepth, BatchedMesh, Box3, BufferAttribute, BufferGeometry, Camera, Clock, Color, CustomBlending, DataTexture, DirectionalLight, DoubleSide, DynamicDrawUsage, FloatType, Frustum, Group, HemisphereLight, IntType, LessDepth, LessEqualDepth, LinearSRGBColorSpace, MathUtils, Matrix3, Matrix4, Mesh, MeshBasicMaterial, MeshPhysicalMaterial, MeshStandardMaterial, OneFactor, PMREMGenerator, PerspectiveCamera, Plane, PlaneGeometry, Quaternion, RGBAFormat, RGBAIntegerFormat, RGFormat, RGIntegerFormat, Raycaster, RedFormat, RedIntegerFormat, SRGBColorSpace, Scene, Sphere, Spherical, StreamDrawUsage, Texture, UnsignedIntType, Vector2, Vector3, Vector4, WebGLRenderer } from "three";
2
2
  import { traverseAncestorsGenerator } from "three/examples/jsm/utils/SceneUtils.js";
3
3
  import createLog from "debug";
4
4
  import { BatchedText, Text } from "troika-three-text";
5
- import { LineMaterial, LineSegmentsGeometry } from "three/examples/jsm/Addons.js";
5
+ import { BufferGeometryUtils, ColorEnvironment, LineMaterial, LineSegmentsGeometry } from "three/examples/jsm/Addons.js";
6
6
  import { MaxRectsPacker, Rectangle } from "maxrects-packer";
7
7
  import { colord, extend } from "colord";
8
8
  import namesPlugin from "colord/plugins/names";
9
+ import { MeshoptDecoder } from "three/addons/libs/meshopt_decoder.module.js";
10
+ import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
11
+ import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
12
+ import { GLTFExporter } from "three/examples/jsm/exporters/GLTFExporter.js";
9
13
  import { DEG2RAD, MathUtils as MathUtils$1, RAD2DEG } from "three/src/math/MathUtils.js";
10
14
  import { EventManager, Pan, Rotate } from "mjolnir.js";
11
15
  //#region src/util/logging.ts
@@ -94,7 +98,7 @@ function getSquareTextureSize(capacity, pixelsPerInstance) {
94
98
  */
95
99
  function getSquareTextureInfo(arrayType, channels, pixelsPerInstance, capacity) {
96
100
  if (channels === 3) {
97
- logger$14.debug("\"channels\" cannot be 3. Set to 4. More info: https://github.com/mrdoob/three.js/pull/23228");
101
+ logger$16.debug("\"channels\" cannot be 3. Set to 4. More info: https://github.com/mrdoob/three.js/pull/23228");
98
102
  channels = 4;
99
103
  }
100
104
  const size = getSquareTextureSize(capacity, pixelsPerInstance);
@@ -121,7 +125,7 @@ function getSquareTextureInfo(arrayType, channels, pixelsPerInstance, capacity)
121
125
  format
122
126
  };
123
127
  }
124
- var logger$14 = createLogger("SquareDataTexture");
128
+ var logger$16 = createLogger("SquareDataTexture");
125
129
  /**
126
130
  * A class that extends `DataTexture` to manage a square texture optimized for instances rendering.
127
131
  * It supports dynamic resizing, partial update based on rows, and allows setting/getting uniforms per instance.
@@ -163,7 +167,7 @@ var SquareDataTexture = class extends DataTexture {
163
167
  setUniformAt(id, name, value) {
164
168
  const schema = this.uniformMap.get(name);
165
169
  if (!schema) {
166
- logger$14.debug(`setUniformAt: uniform ${name} not found`);
170
+ logger$16.debug(`setUniformAt: uniform ${name} not found`);
167
171
  return;
168
172
  }
169
173
  const { offset, size } = schema;
@@ -181,7 +185,7 @@ var SquareDataTexture = class extends DataTexture {
181
185
  getUniformAt(id, name, target) {
182
186
  const schema = this.uniformMap.get(name);
183
187
  if (!schema) {
184
- logger$14.debug(`getUniformAt: uniform ${name} not found`);
188
+ logger$16.debug(`getUniformAt: uniform ${name} not found`);
185
189
  return 0;
186
190
  }
187
191
  const { offset, size } = schema;
@@ -300,7 +304,7 @@ var componentsArray = [
300
304
  //#endregion
301
305
  //#region src/batch/BatchedMesh.ts
302
306
  var batchIdName = "batchId";
303
- var logger$13 = createLogger("BatchedMesh");
307
+ var logger$15 = createLogger("BatchedMesh");
304
308
  /**
305
309
  * Class extending {@link ThreeBatchedMesh} to support per instance uniforms and disable WebGL multi draw
306
310
  */
@@ -323,7 +327,6 @@ var BatchedMesh$1 = class BatchedMesh$1 extends BatchedMesh {
323
327
  */
324
328
  constructor(instanceCount, vertexCount, indexCount, material) {
325
329
  super(instanceCount, vertexCount, indexCount, material);
326
- material.forceSinglePass = true;
327
330
  addDim(this);
328
331
  if (!BatchedMesh$1.useMultiDraw) this.addEventListener("added", () => {
329
332
  if (this.geometry.index !== null) {
@@ -376,7 +379,7 @@ var BatchedMesh$1 = class BatchedMesh$1 extends BatchedMesh {
376
379
  if (BatchedMesh$1.useMultiDraw) return;
377
380
  const batchCount = this.batchCount;
378
381
  const gl = renderer.getContext();
379
- if (geometry.index == null) return logger$13.debug("No index buffer", this.parent?.name);
382
+ if (geometry.index == null) return logger$15.debug("No index buffer", this.parent?.name);
380
383
  const type = this.getIndexType(gl, geometry.index);
381
384
  gl.drawElements(gl.TRIANGLES, batchCount, type, 0);
382
385
  renderer.info.update(batchCount, gl.TRIANGLES, 1);
@@ -391,7 +394,7 @@ var BatchedMesh$1 = class BatchedMesh$1 extends BatchedMesh {
391
394
  */
392
395
  getUniformAt(id, name, target) {
393
396
  if (!this.uniformsTexture) {
394
- logger$13.debug(`getUniformAt: uniforms texture not initialized`);
397
+ logger$15.debug(`getUniformAt: uniforms texture not initialized`);
395
398
  return 0;
396
399
  }
397
400
  return this.uniformsTexture.getUniformAt(id, name, target);
@@ -404,7 +407,7 @@ var BatchedMesh$1 = class BatchedMesh$1 extends BatchedMesh {
404
407
  */
405
408
  setUniformAt(instanceId, name, value) {
406
409
  if (!this.uniformsTexture) {
407
- logger$13.debug(`setUniformAt: uniforms texture not initialized`);
410
+ logger$15.debug(`setUniformAt: uniforms texture not initialized`);
408
411
  return;
409
412
  }
410
413
  this.uniformsTexture.setUniformAt(instanceId, name, value);
@@ -727,6 +730,17 @@ var BatchedText$1 = class extends BatchedText {
727
730
  if (pendingSyncs.length) Promise.all(pendingSyncs).then(repack);
728
731
  else repack();
729
732
  }
733
+ createDerivedMaterial(baseMaterial) {
734
+ const derivedMaterial = super.createDerivedMaterial(baseMaterial);
735
+ derivedMaterial.onBeforeCompile = (shader) => {
736
+ shader.vertexShader = shader.vertexShader.replace("void main() {", `
737
+ void main () {
738
+ // Uninitialized vec2 causes problems on Samsung Android devices
739
+ uTroikaPositionOffset = vec2(0., 0.);
740
+ `);
741
+ };
742
+ return derivedMaterial;
743
+ }
730
744
  repackBatchedGeometry() {
731
745
  const geometry = this.geometry;
732
746
  const batchedAttributes = geometry.attributes;
@@ -1067,7 +1081,6 @@ var dimColorFrag = `
1067
1081
  var POLYGON_OFFSET_MULTIPLIER = 12;
1068
1082
  var sharedParameters = {
1069
1083
  side: DoubleSide,
1070
- transparent: true,
1071
1084
  forceSinglePass: true,
1072
1085
  depthFunc: AlwaysDepth
1073
1086
  };
@@ -1075,6 +1088,7 @@ var sharedParameters = {
1075
1088
  var MaterialSystem = class {
1076
1089
  backgroundMaterial;
1077
1090
  viewport = new Vector4();
1091
+ lightingMaterials = [];
1078
1092
  /**
1079
1093
  * Creates a line material.
1080
1094
  * @param params {@link LineMaterialParameters}
@@ -1103,8 +1117,7 @@ var MaterialSystem = class {
1103
1117
  uniforms["resolution"].value.set(this.viewport.z, this.viewport.w);
1104
1118
  }
1105
1119
  };
1106
- addDimToMaterial(material);
1107
- this.addPolygonOffset(material);
1120
+ this.patchMaterial(material);
1108
1121
  return material;
1109
1122
  }
1110
1123
  /**
@@ -1113,14 +1126,21 @@ var MaterialSystem = class {
1113
1126
  * @returns MeshBasicMaterial instance
1114
1127
  */
1115
1128
  createColorMaterial(params = {}) {
1116
- const material = new (params.lightingEnable ? MeshPhongMaterial : MeshBasicMaterial)({
1129
+ const materialConstructor = params.lightingEnable ? MeshPhysicalMaterial : MeshBasicMaterial;
1130
+ const color = params.color ?? 16777215;
1131
+ const opacity = params.opacity ?? 1;
1132
+ const transparent = opacity < 1;
1133
+ const material = new materialConstructor({
1117
1134
  ...sharedParameters,
1118
- color: params.color ?? 16777215,
1119
- opacity: params.opacity ?? 1
1135
+ color,
1136
+ opacity,
1137
+ transparent
1120
1138
  });
1121
- if (params.depthEnable) material.depthFunc = LessEqualDepth;
1122
- addDimToMaterial(material);
1123
- this.addPolygonOffset(material);
1139
+ if (transparent) {
1140
+ material.depthWrite = false;
1141
+ material.depthFunc = LessDepth;
1142
+ } else if (params.depthEnable) material.depthFunc = LessEqualDepth;
1143
+ this.patchMaterial(material, { lightingEnable: params.lightingEnable });
1124
1144
  return material;
1125
1145
  }
1126
1146
  /**
@@ -1131,18 +1151,37 @@ var MaterialSystem = class {
1131
1151
  createTextureMaterial(params) {
1132
1152
  const material = new MeshBasicMaterial({
1133
1153
  ...sharedParameters,
1134
- map: params.map
1154
+ map: params.map,
1155
+ transparent: true,
1156
+ depthFunc: LessDepth
1135
1157
  });
1136
- if (params.depthEnable) material.depthFunc = LessEqualDepth;
1137
1158
  if (params.uvOffset) material.onBeforeCompile = (shader) => {
1138
1159
  shader.vertexShader = shader.vertexShader.replace("#include <uv_vertex>", `
1139
1160
  #include <uv_vertex>
1140
1161
  vMapUv = uv * uvOffset.zw + uvOffset.xy;
1141
1162
  `);
1142
1163
  };
1164
+ this.patchMaterial(material);
1165
+ return material;
1166
+ }
1167
+ /**
1168
+ * Apply the engine's cross-cutting material patches: dim shader hooks, polygon offset,
1169
+ * and (optional) registration with the lighting system. Used internally by every
1170
+ * factory in this class and exposed publicly so externally-built materials (e.g. PBR
1171
+ * materials cloned from GLTF source) can opt in to the same behavior.
1172
+ * @param material material to patch
1173
+ * @param opts patch options
1174
+ * @param opts.lightingEnable when true and the material is a MeshPhysicalMaterial,
1175
+ * configures it for the project's lighting pipeline and registers it
1176
+ */
1177
+ patchMaterial(material, opts = {}) {
1143
1178
  addDimToMaterial(material);
1144
1179
  this.addPolygonOffset(material);
1145
- return material;
1180
+ if (opts.lightingEnable && material instanceof MeshPhysicalMaterial) {
1181
+ material.ior = 1;
1182
+ material.specularIntensity = 0;
1183
+ this.lightingMaterials.push(material);
1184
+ }
1146
1185
  }
1147
1186
  /**
1148
1187
  * Creates a background material. Used for the background layer to support dimming the background.
@@ -1213,6 +1252,9 @@ function isTextDef(def) {
1213
1252
  function isLineDef(def) {
1214
1253
  return def.points !== void 0;
1215
1254
  }
1255
+ function isModelDef(def) {
1256
+ return def.url !== void 0;
1257
+ }
1216
1258
  function isLayerDef(def) {
1217
1259
  return def.children !== void 0;
1218
1260
  }
@@ -1228,6 +1270,9 @@ function isTextLayer(layer) {
1228
1270
  function isLineLayer(layer) {
1229
1271
  return layer.children[0] && isLineDef(layer.children[0]);
1230
1272
  }
1273
+ function isModelLayer(layer) {
1274
+ return layer.children[0] && isModelDef(layer.children[0]);
1275
+ }
1231
1276
  function isLayerLayer(layer) {
1232
1277
  return layer.children[0] && isLayerDef(layer.children[0]);
1233
1278
  }
@@ -1341,13 +1386,14 @@ var RenderableSystem = class {
1341
1386
  * @param firstUpdate whether this is the first update for this def. Set to true when the def is first added to the scene.
1342
1387
  */
1343
1388
  updateDef(def, firstUpdate = false) {
1344
- const mapping = this.getObjectInstanceByDef(def);
1345
- if (!mapping) return;
1346
- const { object: mesh, instanceIds } = mapping;
1347
- for (const instanceId of instanceIds) mesh.setVisibleAt(instanceId, !def.hidden);
1348
- if (def.hidden && !firstUpdate) return;
1349
- for (const instanceId of instanceIds) toggleInstanceDim(mesh, instanceId, def.dim);
1350
- this.updateDefImpl(def, mesh, instanceIds, firstUpdate);
1389
+ const mappings = this.getObjectInstanceByDef(def);
1390
+ if (!mappings) return;
1391
+ for (const { object: mesh, instanceIds } of mappings) {
1392
+ for (const instanceId of instanceIds) mesh.setVisibleAt(instanceId, !def.hidden);
1393
+ if (def.hidden && !firstUpdate) continue;
1394
+ for (const instanceId of instanceIds) toggleInstanceDim(mesh, instanceId, def.dim);
1395
+ this.updateDefImpl(def, mesh, instanceIds, firstUpdate);
1396
+ }
1351
1397
  }
1352
1398
  /**
1353
1399
  * Update an existing collection with a new layer definition.
@@ -1418,7 +1464,8 @@ var RenderableSystem = class {
1418
1464
  this.logger.debug(`Tried to register def with empty instanceIds %O`, def);
1419
1465
  return;
1420
1466
  }
1421
- this.mapDefToObject.set(def, {
1467
+ if (!this.mapDefToObject.has(def)) this.mapDefToObject.set(def, []);
1468
+ this.mapDefToObject.get(def).push({
1422
1469
  object,
1423
1470
  instanceIds: ids
1424
1471
  });
@@ -1427,29 +1474,57 @@ var RenderableSystem = class {
1427
1474
  this.updateDef(def, true);
1428
1475
  }
1429
1476
  /**
1430
- * Unregister a def and its associated object mapping.
1477
+ * Register a single instance mapping between a def and an object/instance, appending
1478
+ * one entry per call. Use when a single def maps to multiple instances across one or
1479
+ * more containers (e.g. a model spread across several BatchedMesh batches). Pushes
1480
+ * once per instance into the object→defs map so `getDefsByObject(obj)[batchId]` stays
1481
+ * aligned with sequential `BatchedMesh.addInstance` ids.
1482
+ * @param def def to register
1483
+ * @param object container the def's instance lives in
1484
+ * @param instanceId single instance id in the container
1485
+ */
1486
+ registerDefInstance(def, object, instanceId) {
1487
+ if (!this.mapDefToObject.has(def)) this.mapDefToObject.set(def, []);
1488
+ this.mapDefToObject.get(def).push({
1489
+ object,
1490
+ instanceIds: [instanceId]
1491
+ });
1492
+ if (!this.mapObjectToDefs.has(object)) this.mapObjectToDefs.set(object, []);
1493
+ this.mapObjectToDefs.get(object).push(def);
1494
+ this.updateDef(def, true);
1495
+ }
1496
+ /**
1497
+ * Unregister a def and its associated object mappings.
1431
1498
  * @param def def to unregister
1432
1499
  */
1433
1500
  unregisterDef(def) {
1434
- const mapping = this.mapDefToObject.get(def);
1435
- if (mapping) {
1436
- const { object } = mapping;
1437
- const defs = this.mapObjectToDefs.get(object);
1438
- if (defs) {
1439
- this.mapObjectToDefs.set(object, defs.filter((d) => d !== def));
1440
- if (this.mapObjectToDefs.get(object).length === 0) this.mapObjectToDefs.delete(object);
1501
+ const mappings = this.mapDefToObject.get(def);
1502
+ if (mappings) {
1503
+ for (const { object } of mappings) {
1504
+ const defs = this.mapObjectToDefs.get(object);
1505
+ if (defs) {
1506
+ this.mapObjectToDefs.set(object, defs.filter((d) => d !== def));
1507
+ if (this.mapObjectToDefs.get(object).length === 0) this.mapObjectToDefs.delete(object);
1508
+ }
1441
1509
  }
1442
1510
  this.mapDefToObject.delete(def);
1443
1511
  }
1444
1512
  }
1445
1513
  /**
1446
- * Unregister all defs associated with an object.
1514
+ * Unregister all defs associated with an object. Only removes the entries that point
1515
+ * at this object — defs with mappings to other containers keep those entries intact.
1447
1516
  * @param object object to unregister
1448
1517
  */
1449
1518
  unregisterObject(object) {
1450
1519
  const defs = this.mapObjectToDefs.get(object);
1451
1520
  if (defs) {
1452
- for (const def of defs) this.mapDefToObject.delete(def);
1521
+ for (const def of defs) {
1522
+ const mappings = this.mapDefToObject.get(def);
1523
+ if (!mappings) continue;
1524
+ const remaining = mappings.filter((m) => m.object !== object);
1525
+ if (remaining.length === 0) this.mapDefToObject.delete(def);
1526
+ else this.mapDefToObject.set(def, remaining);
1527
+ }
1453
1528
  this.mapObjectToDefs.delete(object);
1454
1529
  }
1455
1530
  }
@@ -1461,17 +1536,18 @@ var RenderableSystem = class {
1461
1536
  this.mapObjectToDefs.clear();
1462
1537
  }
1463
1538
  /**
1464
- * Lookup object/instance by def.
1539
+ * Lookup object/instance mappings by def. Returns an array because a single def can
1540
+ * map to multiple containers (e.g. a model spread across batches keyed by material).
1465
1541
  * @param def def to lookup
1466
- * @returns object and instance ids
1542
+ * @returns array of object/instance-id mappings, or undefined if none
1467
1543
  */
1468
1544
  getObjectInstanceByDef(def) {
1469
- const mapping = this.mapDefToObject.get(def);
1470
- if (!mapping) {
1545
+ const mappings = this.mapDefToObject.get(def);
1546
+ if (!mappings || mappings.length === 0) {
1471
1547
  this.logger.debug(`No object mapping found for def %O`, def);
1472
1548
  return;
1473
1549
  }
1474
- return mapping;
1550
+ return mappings;
1475
1551
  }
1476
1552
  /**
1477
1553
  * Lookup defs by object.
@@ -1491,7 +1567,7 @@ var RenderableSystem = class {
1491
1567
  };
1492
1568
  //#endregion
1493
1569
  //#region src/geometry/image.ts
1494
- var logger$12 = createLogger("image");
1570
+ var logger$14 = createLogger("image");
1495
1571
  /**
1496
1572
  * A system that handles the rendering of image defs.
1497
1573
  */
@@ -1509,10 +1585,10 @@ var ImageSystem = class extends RenderableSystem {
1509
1585
  * @param materialSystem {@link MaterialSystem}
1510
1586
  */
1511
1587
  constructor(ctx, materialSystem) {
1512
- super("image", ctx, logger$12);
1588
+ super("image", ctx, logger$14);
1513
1589
  this.materialSystem = materialSystem;
1514
1590
  const atlasTextureSize = ctx.three.capabilities.maxTextureSize;
1515
- logger$12.debug(`Max texture size: ${atlasTextureSize}`);
1591
+ logger$14.debug(`Max texture size: ${atlasTextureSize}`);
1516
1592
  this.packer = new MaxRectsPacker(atlasTextureSize, atlasTextureSize, 1, { pot: false });
1517
1593
  }
1518
1594
  dispose() {
@@ -1583,7 +1659,7 @@ var ImageSystem = class extends RenderableSystem {
1583
1659
  oldTexture?.dispose();
1584
1660
  mesh.visible = true;
1585
1661
  data.appliedScale = targetScale;
1586
- logger$12.debug(`Rendered atlas for ${mesh.name || mesh.parent?.name} at scale ${targetScale.toFixed(3)}`);
1662
+ logger$14.debug(`Rendered atlas for ${mesh.name || mesh.parent?.name} at scale ${targetScale.toFixed(3)}`);
1587
1663
  }
1588
1664
  }
1589
1665
  disposeObject(object) {
@@ -1603,7 +1679,7 @@ var ImageSystem = class extends RenderableSystem {
1603
1679
  }
1604
1680
  computeResizeFactor() {
1605
1681
  if (!this.memoryLimitMb) {
1606
- logger$12.debug("Memory limit is not set, atlases will not be scaled");
1682
+ logger$14.debug("Memory limit is not set, atlases will not be scaled");
1607
1683
  return 1;
1608
1684
  }
1609
1685
  let totalResizable = 0;
@@ -1614,17 +1690,17 @@ var ImageSystem = class extends RenderableSystem {
1614
1690
  else totalNonResizable += bytes;
1615
1691
  }
1616
1692
  if (totalResizable === 0) {
1617
- logger$12.debug("No resizable atlases, atlases will not be scaled");
1693
+ logger$14.debug("No resizable atlases, atlases will not be scaled");
1618
1694
  return 1;
1619
1695
  }
1620
1696
  const budget = this.memoryLimitMb * 1024 * 1024 - totalNonResizable;
1621
1697
  if (budget <= 0) {
1622
- logger$12.debug("Memory limit is too low, unable to resize textures.");
1698
+ logger$14.debug("Memory limit is too low, unable to resize textures.");
1623
1699
  return 1;
1624
1700
  }
1625
1701
  const factor = Math.sqrt(budget / totalResizable);
1626
1702
  if (factor >= 1) return 1;
1627
- logger$12.debug(`Resize factor: ${factor.toFixed(3)} (budget ${budget} bytes, resizable ${totalResizable} bytes)`);
1703
+ logger$14.debug(`Resize factor: ${factor.toFixed(3)} (budget ${budget} bytes, resizable ${totalResizable} bytes)`);
1628
1704
  return factor;
1629
1705
  }
1630
1706
  packImages(images) {
@@ -1646,8 +1722,8 @@ var ImageSystem = class extends RenderableSystem {
1646
1722
  const boundsHeight = image.bounds.size.y;
1647
1723
  const ratio = sourceArea / (boundsWidth * boundsHeight);
1648
1724
  if (ratio > 1e3) {
1649
- logger$12.debug(`Image bounds: ${boundsWidth}x${boundsHeight}`, `Image source: ${sourceWidth}x${sourceHeight}`);
1650
- logger$12.warn(`Image bounds area is ${ratio.toFixed(2)} times smaller than the image.`);
1725
+ logger$14.debug(`Image bounds: ${boundsWidth}x${boundsHeight}`, `Image source: ${sourceWidth}x${sourceHeight}`);
1726
+ logger$14.warn(`Image bounds area is ${ratio.toFixed(2)} times smaller than the image.`);
1651
1727
  }
1652
1728
  const rect = new Rectangle(image.source.width, image.source.height);
1653
1729
  rect.data = [imageWithIndex];
@@ -1656,7 +1732,7 @@ var ImageSystem = class extends RenderableSystem {
1656
1732
  } else existingRect.data.push(imageWithIndex);
1657
1733
  }
1658
1734
  this.packer.addArray(rectangles);
1659
- this.packer.bins.forEach((bin) => logger$12.debug(`Bin: ${bin.width}x${bin.height}, ${bin.rects.length} rectangles`));
1735
+ this.packer.bins.forEach((bin) => logger$14.debug(`Bin: ${bin.width}x${bin.height}, ${bin.rects.length} rectangles`));
1660
1736
  return this.packer.bins;
1661
1737
  }
1662
1738
  };
@@ -1668,7 +1744,7 @@ function createAtlas(bin, scale) {
1668
1744
  const ctx = canvas.getContext("2d");
1669
1745
  for (const rect of bin.rects) ctx.drawImage(rect.data[0].def.source, Math.floor(rect.x * scale), Math.floor(rect.y * scale), Math.floor(rect.width * scale), Math.floor(rect.height * scale));
1670
1746
  const t1 = performance.now();
1671
- logger$12.debug(`Create atlas (${canvas.width}x${canvas.height}) took ${(t1 - t0).toFixed(2)} milliseconds.`);
1747
+ logger$14.debug(`Create atlas (${canvas.width}x${canvas.height}) took ${(t1 - t0).toFixed(2)} milliseconds.`);
1672
1748
  return createTexture(canvas);
1673
1749
  }
1674
1750
  function createPlaceholderTexture() {
@@ -1687,7 +1763,7 @@ function getAtlasSizeBytes(width, height) {
1687
1763
  }
1688
1764
  //#endregion
1689
1765
  //#region src/geometry/line.ts
1690
- var logger$11 = createLogger("line");
1766
+ var logger$13 = createLogger("line");
1691
1767
  /**
1692
1768
  * A system that handles the rendering of line defs.
1693
1769
  */
@@ -1698,7 +1774,7 @@ var LineSystem = class extends RenderableSystem {
1698
1774
  * @param materialSystem {@link MaterialSystem}
1699
1775
  */
1700
1776
  constructor(ctx, materialSystem) {
1701
- super("line", ctx, logger$11);
1777
+ super("line", ctx, logger$13);
1702
1778
  this.materialSystem = materialSystem;
1703
1779
  }
1704
1780
  buildLayer(layer) {
@@ -2106,7 +2182,7 @@ function computeBoundingSphere(bounds, origin, out = new Sphere()) {
2106
2182
  }
2107
2183
  //#endregion
2108
2184
  //#region src/geometry/mesh.ts
2109
- var logger$10 = createLogger("mesh");
2185
+ var logger$12 = createLogger("mesh");
2110
2186
  extend([namesPlugin]);
2111
2187
  /**
2112
2188
  * A system that handles the rendering of shape defs.
@@ -2125,7 +2201,7 @@ var MeshSystem = class extends RenderableSystem {
2125
2201
  * @param materialSystem {@link MaterialSystem}
2126
2202
  */
2127
2203
  constructor(ctx, materialSystem) {
2128
- super("mesh", ctx, logger$10);
2204
+ super("mesh", ctx, logger$12);
2129
2205
  this.materialSystem = materialSystem;
2130
2206
  }
2131
2207
  buildLayer(layer) {
@@ -2164,24 +2240,25 @@ var MeshSystem = class extends RenderableSystem {
2164
2240
  const isPolygon = shape instanceof Polygon;
2165
2241
  const isRect = shape instanceof Rect;
2166
2242
  if (expectedShapeType === "polygon" && !isPolygon || expectedShapeType === "rect" && !isRect) {
2167
- logger$10.warn("Shape type changing not supported %O", shapeDef);
2243
+ logger$12.warn("Shape type changing not supported %O", shapeDef);
2168
2244
  return;
2169
2245
  }
2170
2246
  if (isPolygon) {
2171
2247
  const geometryRange = mesh.getGeometryRangeAt(geometryId);
2172
- if (shape.vertices.length != geometryRange?.reservedVertexCount || shape.indices.length * 3 != geometryRange.reservedIndexCount) {
2173
- logger$10.warn("Polygon geometry changing not supported %O", shapeDef);
2248
+ let newGeometry = this.buildPolygonGeometry(shape);
2249
+ if (mesh.userData["is3D"]) newGeometry = processGeometryFor3D(newGeometry);
2250
+ const { vertices, indices } = countGeometry(newGeometry);
2251
+ if (vertices != geometryRange?.reservedVertexCount || indices != Math.max(geometryRange.reservedIndexCount, 0)) {
2252
+ logger$12.warn("Polygon geometry changing not supported %O", shapeDef);
2174
2253
  return;
2175
2254
  }
2176
- const geometry = this.buildPolygonGeometry(shape);
2177
- if (mesh.geometry.hasAttribute("normal")) geometry.computeVertexNormals();
2178
- mesh.setGeometryAt(geometryId, geometry);
2255
+ mesh.setGeometryAt(geometryId, newGeometry);
2179
2256
  } else if (isRect) this.updateRect(shape, mesh, instanceId);
2180
2257
  }
2181
2258
  updateColor(shapeDef, mesh, instanceId) {
2182
2259
  const color = this.normalizeColor(shapeDef.color);
2183
2260
  if (!color) {
2184
- logger$10.warn(`Invalid color: ${shapeDef.color} %O`, shapeDef);
2261
+ logger$12.warn(`Invalid color: ${shapeDef.color} %O`, shapeDef);
2185
2262
  return;
2186
2263
  }
2187
2264
  mesh.setColorAt(instanceId, this.color.setRGB(color.r / 255, color.g / 255, color.b / 255, SRGBColorSpace));
@@ -2207,10 +2284,7 @@ var MeshSystem = class extends RenderableSystem {
2207
2284
  ({vertices, indices} = countGeometry(rectGeom));
2208
2285
  } else if (shapeDef.shape instanceof Polygon) {
2209
2286
  let geometry = this.buildPolygonGeometry(shapeDef.shape);
2210
- if (is3D) {
2211
- geometry = geometry.toNonIndexed();
2212
- geometry.computeVertexNormals();
2213
- }
2287
+ if (is3D) geometry = processGeometryFor3D(geometry);
2214
2288
  shapeDefToGeometry.set(shapeDef, geometry);
2215
2289
  ({vertices, indices} = countGeometry(geometry));
2216
2290
  }
@@ -2224,7 +2298,8 @@ var MeshSystem = class extends RenderableSystem {
2224
2298
  });
2225
2299
  const batchedMesh = new BatchedMesh$1(shapes.length, vertexCount, indexCount, material);
2226
2300
  const rectGeometryId = rectAdded ? batchedMesh.addGeometry(rectGeom) : void 0;
2227
- batchedMesh.setCustomSort((list) => this.sortInstances(batchedMesh, list));
2301
+ batchedMesh.setCustomSort((list) => this.sortInstances(batchedMesh, is3D, opacity < 1, list));
2302
+ if (is3D) batchedMesh.userData["is3D"] = true;
2228
2303
  for (const shapeDef of shapes) {
2229
2304
  let instanceId;
2230
2305
  let type;
@@ -2254,18 +2329,316 @@ var MeshSystem = class extends RenderableSystem {
2254
2329
  buildPolygonGeometry(polygon) {
2255
2330
  return new BufferGeometry().setFromPoints(polygon.vertices).setIndex(polygon.indices.flat());
2256
2331
  }
2257
- sortInstances(mesh, list) {
2332
+ sortInstances(mesh, is3D, isTransparent, list) {
2258
2333
  const shapeDefs = this.getDefsByObject(mesh);
2259
2334
  list.sort((a, b) => {
2260
2335
  const aDef = shapeDefs[a.index];
2261
2336
  const bDef = shapeDefs[b.index];
2262
- return (aDef.dim === false ? 1 : 0) - (bDef.dim === false ? 1 : 0);
2337
+ const aDim = aDef.dim === false ? 1 : 0;
2338
+ const bDim = bDef.dim === false ? 1 : 0;
2339
+ if (aDim !== bDim) return aDim - bDim;
2340
+ if (!is3D) return 0;
2341
+ return isTransparent ? sortTransparent(a, b) : sortOpaque(a, b);
2263
2342
  });
2264
2343
  }
2265
2344
  };
2345
+ function sortOpaque(a, b) {
2346
+ return a.z - b.z;
2347
+ }
2348
+ function sortTransparent(a, b) {
2349
+ return b.z - a.z;
2350
+ }
2351
+ function processGeometryFor3D(geometry) {
2352
+ let processedGeometry = geometry;
2353
+ processedGeometry = processedGeometry.toNonIndexed();
2354
+ processedGeometry.computeVertexNormals();
2355
+ processedGeometry = BufferGeometryUtils.toCreasedNormals(processedGeometry, Math.PI / 6);
2356
+ return processedGeometry;
2357
+ }
2358
+ //#endregion
2359
+ //#region src/geometry/model.ts
2360
+ var logger$11 = createLogger("model");
2361
+ /**
2362
+ * Converts the GLB Y-up coordinate system to the engine's Z-up convention.
2363
+ * Matches the rotation/scale applied at the scene root in {@link import("../loaders/glb.ts")}.
2364
+ */
2365
+ var Y_UP_TO_Z_UP = new Matrix4().makeRotationX(-Math.PI / 2).multiply(new Matrix4().makeScale(1, -1, 1));
2366
+ /**
2367
+ * A system that handles the rendering of external 3D model defs (e.g. GLB/glTF).
2368
+ * Loads models asynchronously, builds one {@link BatchedMesh} per (ModelDef, source
2369
+ * material) pair so each material clone can carry its def's opacity directly, and
2370
+ * routes shading through {@link MaterialSystem.patchMaterial} for dim + polygon offset.
2371
+ */
2372
+ var ModelSystem = class extends RenderableSystem {
2373
+ loader = new GLTFLoader();
2374
+ cachePromise = /* @__PURE__ */ new Map();
2375
+ pendingUpdates = /* @__PURE__ */ new Map();
2376
+ layerAborted = /* @__PURE__ */ new WeakSet();
2377
+ isDisposed = false;
2378
+ /**
2379
+ * @param ctx {@link RendererContext}
2380
+ * @param materialSystem {@link MaterialSystem}
2381
+ */
2382
+ constructor(ctx, materialSystem) {
2383
+ super("model", ctx, logger$11);
2384
+ this.materialSystem = materialSystem;
2385
+ const dracoLoader = new DRACOLoader();
2386
+ dracoLoader.setDecoderPath("https://www.gstatic.com/draco/versioned/decoders/1.5.7/");
2387
+ this.loader.setDRACOLoader(dracoLoader);
2388
+ this.loader.setMeshoptDecoder(MeshoptDecoder);
2389
+ }
2390
+ buildLayer(layer) {
2391
+ const group = new Group();
2392
+ if (layer.children.length === 0) return group;
2393
+ this.loadAndBuild(group, layer.children);
2394
+ return group;
2395
+ }
2396
+ updateDef(def, firstUpdate = false) {
2397
+ if (!this.getObjectInstanceByDef(def)) {
2398
+ this.pendingUpdates.set(def, def);
2399
+ return;
2400
+ }
2401
+ super.updateDef(def, firstUpdate);
2402
+ }
2403
+ disposeLayer(group) {
2404
+ this.layerAborted.add(group);
2405
+ super.disposeLayer(group);
2406
+ }
2407
+ dispose() {
2408
+ this.isDisposed = true;
2409
+ super.dispose();
2410
+ for (const cachePromise of this.cachePromise.values()) cachePromise.then((cache) => disposeGltfCache(cache)).catch(() => void 0);
2411
+ this.cachePromise.clear();
2412
+ this.pendingUpdates.clear();
2413
+ }
2414
+ disposeObject(object) {
2415
+ disposeObject(object, { textures: false });
2416
+ }
2417
+ updateDefImpl(def, mesh, instanceIds) {
2418
+ const extents = mesh.userData["modelExtents"];
2419
+ if (extents) {
2420
+ const matrix = composeInstanceMatrix(def, extents);
2421
+ for (const instanceId of instanceIds) mesh.setMatrixAt(instanceId, matrix);
2422
+ }
2423
+ const material = mesh.material;
2424
+ const opacity = def.opacity ?? 1;
2425
+ material.opacity = opacity;
2426
+ const srcTransparent = material.userData["srcTransparent"] === true;
2427
+ material.transparent = opacity < 1 || srcTransparent;
2428
+ material.depthWrite = !material.transparent;
2429
+ }
2430
+ async loadAndBuild(group, models) {
2431
+ let caches;
2432
+ try {
2433
+ caches = await Promise.all(models.map((def) => this.loadCached(def.url)));
2434
+ } catch (err) {
2435
+ logger$11.warn("Failed to load model batch %O", err);
2436
+ return;
2437
+ }
2438
+ if (this.isDisposed || this.layerAborted.has(group)) return;
2439
+ const batches = [];
2440
+ for (let i = 0; i < models.length; i++) {
2441
+ const cache = caches[i];
2442
+ if (!cache || cache.entries.length === 0) continue;
2443
+ batches.push(...this.buildBatchesForDef(models[i], cache));
2444
+ }
2445
+ if (this.isDisposed || this.layerAborted.has(group)) {
2446
+ for (const batch of batches) {
2447
+ this.disposeObject(batch);
2448
+ this.unregisterObject(batch);
2449
+ }
2450
+ return;
2451
+ }
2452
+ group.add(...batches);
2453
+ for (const batch of batches) batch.renderOrder = group.renderOrder;
2454
+ for (const [def, latest] of this.pendingUpdates) if (this.getObjectInstanceByDef(def)) {
2455
+ this.pendingUpdates.delete(def);
2456
+ this.updateDef(latest);
2457
+ }
2458
+ }
2459
+ buildBatchesForDef(def, cache) {
2460
+ const buckets = /* @__PURE__ */ new Map();
2461
+ for (const entry of cache.entries) {
2462
+ const key = `${entry.sourceMaterial.uuid}|${attributeSignature(entry.geometry)}`;
2463
+ let bucket = buckets.get(key);
2464
+ if (!bucket) {
2465
+ bucket = {
2466
+ sourceMaterial: entry.sourceMaterial,
2467
+ geometries: []
2468
+ };
2469
+ buckets.set(key, bucket);
2470
+ }
2471
+ bucket.geometries.push(entry.geometry);
2472
+ }
2473
+ const batches = [];
2474
+ for (const { sourceMaterial, geometries } of buckets.values()) {
2475
+ const material = this.createPatchedMaterial(sourceMaterial);
2476
+ let totalVertices = 0;
2477
+ let totalIndices = 0;
2478
+ for (const geometry of geometries) {
2479
+ totalVertices += geometry.attributes["position"].count;
2480
+ totalIndices += geometry.index?.count ?? 0;
2481
+ }
2482
+ const batch = new BatchedMesh$1(geometries.length, totalVertices, totalIndices, material);
2483
+ batch.userData["modelExtents"] = cache.extents;
2484
+ for (const geometry of geometries) {
2485
+ const geometryId = batch.addGeometry(geometry);
2486
+ const instanceId = batch.addInstance(geometryId);
2487
+ this.registerDefInstance(def, batch, instanceId);
2488
+ }
2489
+ batches.push(batch);
2490
+ }
2491
+ return batches;
2492
+ }
2493
+ createPatchedMaterial(source) {
2494
+ const physical = new MeshPhysicalMaterial();
2495
+ if (source instanceof MeshStandardMaterial || source instanceof MeshPhysicalMaterial) {
2496
+ physical.color.copy(source.color);
2497
+ physical.map = source.map;
2498
+ physical.normalMap = source.normalMap;
2499
+ physical.normalScale.copy(source.normalScale);
2500
+ physical.roughness = source.roughness;
2501
+ physical.roughnessMap = source.roughnessMap;
2502
+ physical.metalness = source.metalness;
2503
+ physical.metalnessMap = source.metalnessMap;
2504
+ physical.emissive.copy(source.emissive);
2505
+ physical.emissiveIntensity = source.emissiveIntensity;
2506
+ physical.emissiveMap = source.emissiveMap;
2507
+ physical.aoMap = source.aoMap;
2508
+ physical.aoMapIntensity = source.aoMapIntensity;
2509
+ physical.alphaMap = source.alphaMap;
2510
+ physical.alphaTest = source.alphaTest;
2511
+ }
2512
+ physical.side = source.side;
2513
+ physical.transparent = source.transparent;
2514
+ physical.opacity = source.opacity;
2515
+ physical.depthWrite = !source.transparent;
2516
+ physical.depthFunc = source.transparent ? LessDepth : LessEqualDepth;
2517
+ physical.userData["srcTransparent"] = source.transparent;
2518
+ this.materialSystem.patchMaterial(physical, { lightingEnable: true });
2519
+ return physical;
2520
+ }
2521
+ async loadCached(url) {
2522
+ let promise = this.cachePromise.get(url);
2523
+ if (!promise) {
2524
+ promise = this.parseGltf(url);
2525
+ this.cachePromise.set(url, promise);
2526
+ }
2527
+ try {
2528
+ return await promise;
2529
+ } catch (err) {
2530
+ logger$11.warn("Failed to load model %s: %O", url, err);
2531
+ this.cachePromise.delete(url);
2532
+ return;
2533
+ }
2534
+ }
2535
+ async parseGltf(url) {
2536
+ const entries = collectGltfEntries(await this.loader.loadAsync(url));
2537
+ const extents = new Box3();
2538
+ for (const { geometry } of entries) {
2539
+ if (!geometry.boundingBox) geometry.computeBoundingBox();
2540
+ extents.union(geometry.boundingBox);
2541
+ }
2542
+ return {
2543
+ extents,
2544
+ entries
2545
+ };
2546
+ }
2547
+ };
2548
+ /**
2549
+ * Traverses a loaded GLTF scene and collects baked geometries paired with their
2550
+ * source materials. Geometries are cloned, then transformed by
2551
+ * {@link Y_UP_TO_Z_UP} times the node's world matrix so vertices land in engine
2552
+ * space relative to the GLB root.
2553
+ * @param gltf parsed GLTF
2554
+ * @returns array of `{geometry, sourceMaterial}` entries, one per Mesh node
2555
+ */
2556
+ function collectGltfEntries(gltf) {
2557
+ const entries = [];
2558
+ gltf.scene.updateMatrixWorld(true);
2559
+ const tmp = new Matrix4();
2560
+ gltf.scene.traverse((node) => {
2561
+ if (!(node instanceof Mesh)) return;
2562
+ const mesh = node;
2563
+ if (!mesh.geometry || !mesh.material) return;
2564
+ const sourceMaterial = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
2565
+ if (Array.isArray(mesh.material) && mesh.material.length > 1) logger$11.debug("Multi-material mesh on node %s — only the first material is used.", mesh.name);
2566
+ const baked = mesh.geometry.clone();
2567
+ tmp.multiplyMatrices(Y_UP_TO_Z_UP, mesh.matrixWorld);
2568
+ baked.applyMatrix4(tmp);
2569
+ baked.computeBoundingBox();
2570
+ baked.computeBoundingSphere();
2571
+ entries.push({
2572
+ geometry: baked,
2573
+ sourceMaterial
2574
+ });
2575
+ });
2576
+ return entries;
2577
+ }
2578
+ /**
2579
+ * Disposes a cached GLB's baked geometries and the unique textures referenced by its
2580
+ * source materials. Source textures are shared across batches, so this is the only
2581
+ * place they should be released.
2582
+ * @param cache cache entry to dispose
2583
+ */
2584
+ function disposeGltfCache(cache) {
2585
+ const seen = /* @__PURE__ */ new Set();
2586
+ const textureProps = [
2587
+ "map",
2588
+ "normalMap",
2589
+ "roughnessMap",
2590
+ "metalnessMap",
2591
+ "emissiveMap",
2592
+ "aoMap",
2593
+ "alphaMap"
2594
+ ];
2595
+ for (const { sourceMaterial } of cache.entries) {
2596
+ const matRecord = sourceMaterial;
2597
+ for (const prop of textureProps) {
2598
+ const tex = matRecord[prop];
2599
+ if (tex && tex instanceof Texture && !seen.has(tex)) {
2600
+ seen.add(tex);
2601
+ tex.dispose();
2602
+ }
2603
+ }
2604
+ }
2605
+ for (const { geometry } of cache.entries) geometry.dispose();
2606
+ }
2607
+ /**
2608
+ * Stable identifier for a geometry's attribute set. Used to sub-bucket geometries that
2609
+ * share a source material but have different attribute names (e.g. with vs without uv),
2610
+ * since {@link BatchedMesh.addGeometry} requires homogeneous attributes per batch.
2611
+ * @param geometry geometry to summarize
2612
+ * @returns comma-separated sorted attribute names
2613
+ */
2614
+ function attributeSignature(geometry) {
2615
+ return Object.keys(geometry.attributes).sort().join(",");
2616
+ }
2617
+ /**
2618
+ * Composes a per-instance world matrix from a ModelDef's bounds and the loaded GLB's
2619
+ * engine-space extents: scale uniformly to fit bounds.size in XY, rotate around Z by
2620
+ * bounds.rotation, and translate so the model sits at bounds.center with its base at
2621
+ * bounds.elevation.
2622
+ * @param def model definition
2623
+ * @param extents engine-space bounding box of the loaded GLB
2624
+ * @returns world matrix to set on a BatchedMesh instance
2625
+ */
2626
+ function composeInstanceMatrix(def, extents) {
2627
+ const size = extents.getSize(tempSize);
2628
+ const center = extents.getCenter(tempCenter);
2629
+ const sx = size.x > 0 ? def.bounds.size.x / size.x : 1;
2630
+ const sy = size.y > 0 ? def.bounds.size.y / size.y : 1;
2631
+ const scale = Math.min(sx, sy);
2632
+ return new Matrix4().makeTranslation(-center.x, -center.y, -extents.min.z).premultiply(tempScale.makeScale(scale, scale, scale)).premultiply(tempRotation.makeRotationZ(def.bounds.rotation)).premultiply(tempTranslation.makeTranslation(def.bounds.center.x, def.bounds.center.y, def.bounds.elevation));
2633
+ }
2634
+ var tempSize = new Vector3();
2635
+ var tempCenter = new Vector3();
2636
+ var tempScale = new Matrix4();
2637
+ var tempRotation = new Matrix4();
2638
+ var tempTranslation = new Matrix4();
2266
2639
  //#endregion
2267
2640
  //#region src/geometry/text.ts
2268
- var logger$9 = createLogger("text");
2641
+ var logger$10 = createLogger("text");
2269
2642
  /**
2270
2643
  * A system that handles the rendering of text defs.
2271
2644
  */
@@ -2286,7 +2659,7 @@ var TextSystem = class extends RenderableSystem {
2286
2659
  * @param materialSystem {@link MaterialSystem}
2287
2660
  */
2288
2661
  constructor(ctx, materialSystem) {
2289
- super("text", ctx, logger$9);
2662
+ super("text", ctx, logger$10);
2290
2663
  this.materialSystem = materialSystem;
2291
2664
  }
2292
2665
  dispose() {
@@ -2364,6 +2737,9 @@ var TextSystem = class extends RenderableSystem {
2364
2737
  const textDefs = layer.children;
2365
2738
  const batchedText = new BatchedText$1();
2366
2739
  batchedText.material = this.materialSystem.createColorMaterial({ depthEnable: is3D });
2740
+ batchedText.material.transparent = false;
2741
+ batchedText.material.blending = CustomBlending;
2742
+ batchedText.material.blendSrcAlpha = OneFactor;
2367
2743
  const mappingData = [];
2368
2744
  let instanceId = 0;
2369
2745
  for (const textDef of textDefs) {
@@ -2458,7 +2834,7 @@ function setAnchorsAndAlignment(text, alignment) {
2458
2834
  }
2459
2835
  //#endregion
2460
2836
  //#region src/geometry/layer.ts
2461
- var logger$8 = createLogger("layer");
2837
+ var logger$9 = createLogger("layer");
2462
2838
  /**
2463
2839
  * A system that handles the rendering of layer defs and scene graph building.
2464
2840
  */
@@ -2469,6 +2845,7 @@ var LayerSystem = class {
2469
2845
  imageSystem;
2470
2846
  textSystem;
2471
2847
  lineSystem;
2848
+ modelSystem;
2472
2849
  systems;
2473
2850
  mapLayerDefsToObjects = /* @__PURE__ */ new Map();
2474
2851
  mapLayerDefToParent = /* @__PURE__ */ new Map();
@@ -2483,11 +2860,13 @@ var LayerSystem = class {
2483
2860
  this.imageSystem = new ImageSystem(this.ctx, this.materialSystem);
2484
2861
  this.textSystem = new TextSystem(this.ctx, this.materialSystem);
2485
2862
  this.lineSystem = new LineSystem(this.ctx, this.materialSystem);
2863
+ this.modelSystem = new ModelSystem(this.ctx, this.materialSystem);
2486
2864
  this.systems = [
2487
2865
  this.meshSystem,
2488
2866
  this.imageSystem,
2489
2867
  this.textSystem,
2490
- this.lineSystem
2868
+ this.lineSystem,
2869
+ this.modelSystem
2491
2870
  ];
2492
2871
  }
2493
2872
  /**
@@ -2513,14 +2892,14 @@ var LayerSystem = class {
2513
2892
  */
2514
2893
  buildScene(sceneDef) {
2515
2894
  const renderOrder = this.initRenderOrder(sceneDef.rootLayer).map((layer) => this.getFullLayerName(layer));
2516
- logger$8.debug("Render order %O", renderOrder);
2895
+ logger$9.debug("Render order %O", renderOrder);
2517
2896
  const rootGroup = new Group();
2518
2897
  rootGroup.name = sceneDef.rootLayer.name;
2519
2898
  this.mapLayerDefsToObjects.set(sceneDef.rootLayer, rootGroup);
2520
2899
  if (sceneDef.background) rootGroup.add(this.createBackgroundLayer(sceneDef.background));
2521
2900
  for (const child of sceneDef.rootLayer.children) rootGroup.add(this.buildLayer(child));
2522
2901
  this.updateLayer(sceneDef.rootLayer, false);
2523
- printTree(rootGroup, logger$8.debug);
2902
+ printTree(rootGroup, logger$9.debug);
2524
2903
  if (sceneDef.memoryLimit) this.imageSystem.memoryLimitMb = sceneDef.memoryLimit;
2525
2904
  this.imageSystem.renderAtlases();
2526
2905
  return rootGroup;
@@ -2553,6 +2932,7 @@ var LayerSystem = class {
2553
2932
  else if (isImageDef(def)) this.imageSystem.updateDef(def);
2554
2933
  else if (isTextDef(def)) this.textSystem.updateDef(def);
2555
2934
  else if (isLineDef(def)) this.lineSystem.updateDef(def);
2935
+ else if (isModelDef(def)) this.modelSystem.updateDef(def);
2556
2936
  else this.updateLayer(def, true);
2557
2937
  }
2558
2938
  disposeLayerTree(layerDef) {
@@ -2563,6 +2943,7 @@ var LayerSystem = class {
2563
2943
  else if (isImageLayer(layerDef)) this.imageSystem.disposeLayer(layerObject);
2564
2944
  else if (isTextLayer(layerDef)) this.textSystem.disposeLayer(layerObject);
2565
2945
  else if (isLineLayer(layerDef)) this.lineSystem.disposeLayer(layerObject);
2946
+ else if (isModelLayer(layerDef)) this.modelSystem.disposeLayer(layerObject);
2566
2947
  this.mapLayerDefsToObjects.delete(layerDef);
2567
2948
  this.mapLayerDefToParent.delete(layerDef);
2568
2949
  this.renderOrderMap.delete(layerDef);
@@ -2576,6 +2957,7 @@ var LayerSystem = class {
2576
2957
  else if (isLineLayer(layerDef)) this.lineSystem.updateLayer(group, layerDef);
2577
2958
  else if (isTextLayer(layerDef)) this.textSystem.updateLayer(group, layerDef);
2578
2959
  else if (isShapeLayer(layerDef)) this.meshSystem.updateLayer(group, layerDef);
2960
+ else if (isModelLayer(layerDef)) this.modelSystem.updateLayer(group, layerDef);
2579
2961
  this.setLayerName(group, group.name);
2580
2962
  }
2581
2963
  layerObject.visible = !layerDef.hidden && layerDef.children.length > 0;
@@ -2585,12 +2967,13 @@ var LayerSystem = class {
2585
2967
  }
2586
2968
  buildLayer(layerDef, parentPrefix = "") {
2587
2969
  const layerFullName = parentPrefix + layerDef.name;
2588
- logger$8.debug(`Building layer ${layerFullName}...`);
2970
+ logger$9.debug(`Building layer ${layerFullName}...`);
2589
2971
  let layerObject;
2590
2972
  if (isShapeLayer(layerDef)) layerObject = this.meshSystem.buildLayer(layerDef);
2591
2973
  else if (isImageLayer(layerDef)) layerObject = this.imageSystem.buildLayer(layerDef);
2592
2974
  else if (isTextLayer(layerDef)) layerObject = this.textSystem.buildLayer(layerDef);
2593
2975
  else if (isLineLayer(layerDef)) layerObject = this.lineSystem.buildLayer(layerDef);
2976
+ else if (isModelLayer(layerDef)) layerObject = this.modelSystem.buildLayer(layerDef);
2594
2977
  else {
2595
2978
  layerObject = new Group();
2596
2979
  layerDef.children.map((layer) => this.buildLayer(layer, parentPrefix + `${layerDef.name}:`)).forEach((g) => layerObject.add(g));
@@ -2610,11 +2993,8 @@ var LayerSystem = class {
2610
2993
  setRenderOrder(object, layer) {
2611
2994
  const renderOrder = this.renderOrderMap.get(layer);
2612
2995
  if (renderOrder === void 0) return;
2613
- if (object.isGroup) {
2614
- object.children.forEach((child) => this.setRenderOrder(child, layer));
2615
- return;
2616
- }
2617
2996
  object.renderOrder = renderOrder;
2997
+ if (object.isGroup) object.children.forEach((child) => this.setRenderOrder(child, layer));
2618
2998
  }
2619
2999
  createBackgroundLayer(color) {
2620
3000
  const backgroundMesh = new Mesh(new PlaneGeometry(2, 2), this.materialSystem.createBackgroundMaterial(color));
@@ -2670,219 +3050,154 @@ var LayerSystem = class {
2670
3050
  //#region src/geometry/lights.ts
2671
3051
  /** System for managing directional lights in 3D scenes. */
2672
3052
  var LightsSystem = class {
2673
- color = 16777215;
2674
- intensity = .5;
3053
+ skyColor = 16777215;
3054
+ groundColor = 0;
3055
+ intensity = Math.PI;
2675
3056
  mapSceneToLight = /* @__PURE__ */ new Map();
3057
+ pmremGenerator;
3058
+ /**
3059
+ * @param ctx {@link RendererContext} instance
3060
+ */
3061
+ constructor(ctx) {
3062
+ this.ctx = ctx;
3063
+ this.pmremGenerator = new PMREMGenerator(this.ctx.three);
3064
+ }
2676
3065
  /**
2677
3066
  * Initializes a directional light for the given scene.
2678
3067
  * @param scene {@link Scene} to add the light to
2679
3068
  */
2680
3069
  initLights(scene) {
2681
- const ambientLight = new AmbientLight(16777215, this.intensity);
2682
- scene.add(ambientLight);
2683
- const directionalLight = new DirectionalLight(this.color, this.intensity * Math.PI * 2);
3070
+ const hemisphereLightIntensity = this.intensity * .25;
3071
+ const hemisphereLight = new HemisphereLight(this.skyColor, this.groundColor, hemisphereLightIntensity);
3072
+ const matrixWorldInverse = new Matrix4().copy(scene.matrixWorld).invert();
3073
+ const position = new Vector3(0, 0, -1).applyMatrix4(matrixWorldInverse);
3074
+ hemisphereLight.position.copy(position);
3075
+ scene.add(hemisphereLight);
3076
+ const directionalLightIntensity = this.intensity * .5;
3077
+ const directionalLight = new DirectionalLight(this.skyColor, directionalLightIntensity);
2684
3078
  scene.add(directionalLight);
2685
3079
  scene.add(directionalLight.target);
2686
3080
  this.mapSceneToLight.set(scene, directionalLight);
3081
+ scene.environment = this.pmremGenerator.fromScene(new ColorEnvironment(), .04).texture;
3082
+ scene.environmentIntensity = .25;
2687
3083
  }
2688
3084
  /**
2689
3085
  * Updates the light direction to match the camera's viewing direction.
2690
- * @param scene {@link Scene} containing the light
3086
+ * @param sceneState {@link SceneState} containing the light
2691
3087
  * @param camera {@link Camera} to derive the light direction from
2692
3088
  */
2693
- updateLights(scene, camera) {
2694
- const light = this.mapSceneToLight.get(scene);
3089
+ updateLights(sceneState, camera) {
3090
+ const light = this.mapSceneToLight.get(sceneState.scene);
2695
3091
  if (!light) return;
2696
- const e = scene.matrixWorld.elements;
2697
- camera.getWorldDirection(light.position).multiply({
2698
- x: Math.sign(e[0]),
2699
- y: Math.sign(e[5]),
2700
- z: Math.sign(e[10])
2701
- }).normalize().negate();
3092
+ camera.getWorldDirection(light.position).transformDirection(sceneState.worldMatrixInverse).negate();
2702
3093
  }
2703
3094
  };
2704
3095
  //#endregion
2705
- //#region src/util/asserts.ts
2706
- /** Logger instance for assert warnings */
2707
- var logger$7 = createLogger("");
3096
+ //#region src/loaders/glb.ts
3097
+ var logger$8 = createLogger("glb");
2708
3098
  /**
2709
- * Asserts the renderer has not been disposed.
2710
- * @param renderer - Renderer instance to check
2711
- * @param funcName - Name of the calling function for error messages
2712
- * @returns false if disposed, true otherwise
3099
+ * Loads a GLB file and returns a Three.js model.
3100
+ * @param models - The models to load.
3101
+ * @returns An object containing parsed Three.js objects.
2713
3102
  */
2714
- function assertNotDisposed(renderer, funcName) {
2715
- if (renderer.isDisposed) {
2716
- logger$7.warn(`[${funcName}]: Renderer is used after being disposed. Please create a new instance.`);
2717
- return false;
2718
- }
2719
- return true;
2720
- }
2721
- /**
2722
- * Asserts the renderer has been initialized.
2723
- * @param renderer - Renderer instance to check
2724
- * @param funcName - Name of the calling function for error messages
2725
- * @returns false if not initialized, true otherwise
2726
- */
2727
- function assertInitialized(renderer, funcName) {
2728
- if (!renderer.isInitialized) {
2729
- logger$7.warn(`${funcName}: Renderer is not initialized. Please call init() before using it.`);
2730
- return false;
2731
- }
2732
- return true;
2733
- }
2734
- /**
2735
- * Asserts the renderer has not been initialized yet.
2736
- * @param renderer - Renderer instance to check
2737
- * @param funcName - Name of the calling function for error messages
2738
- * @returns false if already initialized, true otherwise
2739
- */
2740
- function assertNotInitialized(renderer, funcName) {
2741
- if (renderer.isInitialized) {
2742
- logger$7.warn(`${funcName}: Renderer is already initialized. Please call init() only once.`);
2743
- return false;
2744
- }
2745
- return true;
3103
+ async function loadGLB(models) {
3104
+ const loader = new GLTFLoader();
3105
+ const dracoLoader = new DRACOLoader();
3106
+ dracoLoader.setDecoderPath("https://www.gstatic.com/draco/versioned/decoders/1.5.7/");
3107
+ const root = new Group();
3108
+ for (const model of models) {
3109
+ if (model.isMeshopt) loader.setMeshoptDecoder(MeshoptDecoder);
3110
+ if (model.isDraco) loader.setDRACOLoader(dracoLoader);
3111
+ const loadedModel = await loader.loadAsync(model.url);
3112
+ logger$8.info("model loaded", loadedModel);
3113
+ root.add(...loadedModel.scene.children);
3114
+ loader.setMeshoptDecoder(null);
3115
+ loader.setDRACOLoader(null);
3116
+ }
3117
+ const bounds = new Box3();
3118
+ root.traverse((node) => {
3119
+ if (node.userData && Object.keys(node.userData).filter((key) => key !== "name").length > 0) logger$8.info(`node ${node.name} userData:`, node.userData);
3120
+ if (node instanceof Mesh && node.geometry) {
3121
+ const materialJSON = node.material.toJSON();
3122
+ logger$8.info(`node ${node.name} material %O:`, materialJSON);
3123
+ const geometry = node.geometry;
3124
+ if (!geometry.boundingBox) geometry.computeBoundingBox();
3125
+ const box = geometry.boundingBox.clone();
3126
+ box.applyMatrix4(node.matrixWorld);
3127
+ bounds.union(box);
3128
+ }
3129
+ });
3130
+ logger$8.info("model bounds", bounds);
3131
+ root.rotation.x = -Math.PI / 2;
3132
+ root.scale.set(1, -1, 1);
3133
+ return {
3134
+ sceneDef: {
3135
+ rootLayer: {
3136
+ children: [],
3137
+ name: "root"
3138
+ },
3139
+ viewbox: new Rect([bounds.min.x, bounds.min.z], [bounds.max.x, bounds.max.z])
3140
+ },
3141
+ root
3142
+ };
2746
3143
  }
2747
3144
  /**
2748
- * Asserts the renderer is not in external mode.
2749
- * @param renderer - Renderer instance to check
2750
- * @param funcName - Name of the calling function for error messages
2751
- * @returns false if in external mode, true otherwise
3145
+ * Loads a GLB file, converts it to a Three.js {@link Scene}, exports the scene as a GLTF
3146
+ * (JSON) file, and triggers a browser download of the result.
3147
+ * @param url - The URL of the GLB file.
3148
+ * @param isMeshopt - Whether the model file uses meshopt compression.
2752
3149
  */
2753
- function assertNotExternalMode(renderer, funcName) {
2754
- if (renderer.isExternalMode) {
2755
- logger$7.warn(`${funcName}: This operation is not supported in external mode.`);
2756
- return false;
2757
- }
2758
- return true;
3150
+ async function glb2gltf(url, isMeshopt) {
3151
+ const loader = new GLTFLoader();
3152
+ if (isMeshopt) loader.setMeshoptDecoder(MeshoptDecoder);
3153
+ const model = await loader.loadAsync(url);
3154
+ logger$8.info("glb loaded for export", model);
3155
+ const scene = new Scene();
3156
+ for (const child of model.scene.children) scene.add(child.clone());
3157
+ const gltf = await new GLTFExporter().parseAsync(scene);
3158
+ logger$8.info("scene exported as gltf");
3159
+ const filename = `${deriveBasename(url)}.gltf`;
3160
+ saveBlob(new Blob([JSON.stringify(gltf, null, 2)], { type: "application/json" }), filename);
2759
3161
  }
2760
3162
  /**
2761
- * Asserts that number array is a valid 4x4 matrix.
2762
- * @param matrix number array to check
2763
- * @param name name of the matrix for error messages
2764
- * @param log optional logger instance to use for warning messages (defaults to root logger)
2765
- * @returns true if matrix is valid, false otherwise
3163
+ * Loads a GLTF/GLB file and splits its top-level children into two GLB files based on
3164
+ * whether each child's name starts with `BasisTransform`. The matching children are saved
3165
+ * with a `-decor` suffix; the rest are saved with a `-base` suffix. Both files are
3166
+ * triggered as browser downloads. The output is intentionally uncompressed run a
3167
+ * meshopt/quantize pass offline (e.g. via `gltf-transform`) afterwards.
3168
+ * @param url - The URL of the GLTF/GLB file.
2766
3169
  */
2767
- function assertValidMatrix(matrix, name, log = logger$7) {
2768
- if (matrix.length !== 16) {
2769
- log.warn(`${name}: Matrix must be 16 elements long`);
2770
- return false;
2771
- }
2772
- return true;
3170
+ async function splitGltf(url) {
3171
+ const model = await new GLTFLoader().loadAsync(url);
3172
+ logger$8.info("gltf loaded for split", model);
3173
+ const baseScene = new Scene();
3174
+ const decorScene = new Scene();
3175
+ for (const child of model.scene.children) (child.name.startsWith("BasisTransform") ? decorScene : baseScene).add(child.clone());
3176
+ logger$8.info("split: base=%d decor=%d", baseScene.children.length, decorScene.children.length);
3177
+ const exporter = new GLTFExporter();
3178
+ const baseGlb = await exporter.parseAsync(baseScene, { binary: true });
3179
+ const decorGlb = await exporter.parseAsync(decorScene, { binary: true });
3180
+ const stem = deriveBasename(url);
3181
+ saveBlob(new Blob([baseGlb], { type: "model/gltf-binary" }), `${stem}-base.glb`);
3182
+ saveBlob(new Blob([decorGlb], { type: "model/gltf-binary" }), `${stem}-decor.glb`);
2773
3183
  }
2774
- /**
2775
- * Wraps an API object so that every method is guarded by a precondition check.
2776
- * When the guard (or a per-method override) fails, the corresponding fallback
2777
- * method is called instead of the real implementation.
2778
- * Non-function properties are passed through from `impl` (if present) or `fallback`.
2779
- * @param impl - Real implementation, or null when the backing system doesn't exist
2780
- * @param fallback - Noop / default implementation used when guard fails or impl is null
2781
- * @param guard - Default guard applied to every method
2782
- * @param guards - Optional per-method guard overrides (takes precedence over `guard`)
2783
- * @returns A new object conforming to T with every method wrapped
2784
- */
2785
- function guardAPI(impl, fallback, guard, guards) {
2786
- const guarded = {};
2787
- for (const [key, fallbackVal] of Object.entries(fallback)) if (typeof fallbackVal === "function") {
2788
- const implFn = impl?.[key];
2789
- const methodGuard = guards?.[key] ?? guard;
2790
- const fallbackFn = fallbackVal;
2791
- guarded[key] = (...args) => {
2792
- if (methodGuard(key) && implFn) return implFn(...args);
2793
- return fallbackFn(...args);
2794
- };
2795
- } else guarded[key] = impl?.[key] ?? fallbackVal;
2796
- return guarded;
3184
+ function deriveBasename(url) {
3185
+ return (url.split("?")[0].split("#")[0].split("/").pop() ?? "model").replace(/\.(glb|gltf)$/i, "");
2797
3186
  }
2798
- //#endregion
2799
- //#region src/space/external.ts
2800
- var externalLogger = createLogger("external");
2801
- /**
2802
- * Camera system in external mode. Exposes dummy camera instance as a wrapper for external camera transforms,
2803
- * and estimates zoom factor ndc -> world conversion.
2804
- */
2805
- var ExternalCameraSystem = class {
2806
- /** External camera instance */
2807
- camera = new Camera();
2808
- ndcLeft = new Vector2(-1, 0);
2809
- ndcRight = new Vector2(1, 0);
2810
- intersectionPointLeft = new Vector3();
2811
- intersectionPointRight = new Vector3();
2812
- prevZoomFactor;
2813
- /**
2814
- * @param ctx {@link RendererContext} instance
2815
- * @param pickingSystem {@link PickingSystem} instance
2816
- */
2817
- constructor(ctx, pickingSystem) {
2818
- this.ctx = ctx;
2819
- this.pickingSystem = pickingSystem;
2820
- }
2821
- /** Current zoom factor estimation */
2822
- get zoomFactor() {
2823
- const estimatedZoomFactor = this.estimateZoomFactor();
2824
- const newZoomFactor = estimatedZoomFactor ?? this.prevZoomFactor ?? 1;
2825
- if (estimatedZoomFactor) this.prevZoomFactor = estimatedZoomFactor;
2826
- return newZoomFactor;
2827
- }
2828
- /**
2829
- * Set the camera projection matrix.
2830
- * @param matrix Projection matrix
2831
- */
2832
- setCameraProjection(matrix) {
2833
- if (!assertValidMatrix(matrix, "setCameraProjection", externalLogger)) return;
2834
- this.camera.projectionMatrix.fromArray(matrix);
2835
- this.camera.projectionMatrixInverse.copy(this.camera.projectionMatrix).invert();
2836
- }
2837
- estimateZoomFactor() {
2838
- const [bufferW, bufferH] = this.ctx.getDrawingBufferSizePx();
2839
- if (bufferW <= 0 || bufferH <= 0) return;
2840
- const worldPoint1 = this.pickingSystem.intersectPlane(this.ndcLeft, this.camera, this.intersectionPointLeft);
2841
- const worldPoint2 = this.pickingSystem.intersectPlane(this.ndcRight, this.camera, this.intersectionPointRight);
2842
- if (!worldPoint1 || !worldPoint2) return;
2843
- const worldLength = worldPoint2.sub(worldPoint1).length();
2844
- if (worldLength === 0) return;
2845
- return bufferW / worldLength;
2846
- }
2847
- };
2848
- var originalProjectionMatrix = new Matrix4();
2849
- var patchedMeshes = /* @__PURE__ */ new WeakSet();
2850
- /**
2851
- * Three.js passes two matrices to the vertex shader as uniforms: projectionMatrix and modelViewMatrix.
2852
- * In external mode, the scale of matrix values might be (in mapbox case it certainly is) too big for float32 precision,
2853
- * thus introducing visual artifacts after multiplying them. This function introduces a workaround to premultiply
2854
- * matrices on the CPU side and pass them as a single uniform. In order to not break the shader, it passes an identity
2855
- * matrix as a second uniform. It uses material.onBeforeRender instead of object.onBeforeRender because it's called
2856
- * after three.js own matrix calculations (https://github.com/mrdoob/three.js/blob/33b6ce05a72be0b49cc84cdbb7b5cc972036eebc/src/renderers/WebGLRenderer.js#L2097).
2857
- * Since there is no material.onAfterRender, we use object.onAfterRender to restore camera projection matrix instead.
2858
- * Idempotent — already-patched meshes are skipped via a WeakSet, so this can safely be called every frame.
2859
- * @param scene {@link Scene} instance
2860
- */
2861
- function patchMatricesInExternalMode(scene) {
2862
- scene.traverse((child) => {
2863
- const mesh = child;
2864
- if (!mesh.isMesh || patchedMeshes.has(mesh)) return;
2865
- patchedMeshes.add(mesh);
2866
- const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
2867
- for (const material of materials) {
2868
- const onBeforeRender = material.onBeforeRender.bind(material);
2869
- material.onBeforeRender = (renderer, scene, camera, geometry, object, group) => {
2870
- onBeforeRender(renderer, scene, camera, geometry, object, group);
2871
- originalProjectionMatrix.copy(camera.projectionMatrix);
2872
- camera.projectionMatrix.multiply(object.matrixWorld);
2873
- object.modelViewMatrix.identity();
2874
- };
2875
- }
2876
- const onAfterRender = mesh.onAfterRender.bind(mesh);
2877
- mesh.onAfterRender = (renderer, scene, camera, geometry, object, group) => {
2878
- camera.projectionMatrix.copy(originalProjectionMatrix);
2879
- onAfterRender(renderer, scene, camera, geometry, object, group);
2880
- };
2881
- });
3187
+ function saveBlob(blob, filename) {
3188
+ const objectUrl = URL.createObjectURL(blob);
3189
+ const link = document.createElement("a");
3190
+ link.href = objectUrl;
3191
+ link.download = filename;
3192
+ link.style.display = "none";
3193
+ document.body.appendChild(link);
3194
+ link.click();
3195
+ document.body.removeChild(link);
3196
+ URL.revokeObjectURL(objectUrl);
2882
3197
  }
2883
3198
  //#endregion
2884
3199
  //#region src/ui/controls.ts
2885
- var logger$6 = createLogger("controls");
3200
+ var logger$7 = createLogger("controls");
2886
3201
  /** Navigation system. Manages camera controls and zooming. */
2887
3202
  var ControlsSystem = class {
2888
3203
  controller;
@@ -2926,7 +3241,7 @@ var ControlsSystem = class {
2926
3241
  const worldRect = new Rect(this.coordinatesSystem.modelToWorld(rect.min), this.coordinatesSystem.modelToWorld(rect.max));
2927
3242
  const sourceRect = Polygon.fromRect(worldRect).rotate(bearingAngle).bounds;
2928
3243
  if (sourceRect.size.x <= 0 || sourceRect.size.y <= 0) {
2929
- logger$6.warn("zoomTo: sourceRect size is 0");
3244
+ logger$7.warn("zoomTo: sourceRect size is 0");
2930
3245
  return;
2931
3246
  }
2932
3247
  if (paddingPercent) targetRect.expand({
@@ -3225,7 +3540,7 @@ function asEventAPI(system) {
3225
3540
  }
3226
3541
  //#endregion
3227
3542
  //#region src/space/coordinates.ts
3228
- var logger$5 = createLogger("coordinates");
3543
+ var logger$6 = createLogger("coordinates");
3229
3544
  /** System responsible for converting coordinates between different spaces. */
3230
3545
  var CoordinatesSystem = class {
3231
3546
  /** Used as a scratch vector for coordinate space conversions */
@@ -3298,7 +3613,7 @@ var CoordinatesSystem = class {
3298
3613
  canvasToNDC(point, out = new Vector2()) {
3299
3614
  const [w, h] = this.ctx.getDrawingBufferSizePx();
3300
3615
  if (w <= 0 || h <= 0) {
3301
- logger$5.warn("canvasToNDC: renderer size is 0");
3616
+ logger$6.warn("canvasToNDC: renderer size is 0");
3302
3617
  return out.set(0, 0);
3303
3618
  }
3304
3619
  const dpr = this.ctx.three.getPixelRatio();
@@ -3466,7 +3781,7 @@ function createNoopHandler() {
3466
3781
  };
3467
3782
  }
3468
3783
  //#endregion
3469
- //#region ../../node_modules/.pnpm/camera-controls@3.1.1_patch_hash=1d30d1431514ed87b48b2ec3defd4fe64eca4cb9b29373199021281dbc5d7782_three@0.174.0/node_modules/camera-controls/dist/camera-controls.module.js
3784
+ //#region ../../node_modules/.pnpm/camera-controls@3.1.1_patch_hash=1d30d1431514ed87b48b2ec3defd4fe64eca4cb9b29373199021281dbc5d7782_three@0.184.0/node_modules/camera-controls/dist/camera-controls.module.js
3470
3785
  /*!
3471
3786
  * camera-controls
3472
3787
  * https://github.com/yomotsu/camera-controls
@@ -5838,7 +6153,7 @@ var subsetOfTHREE = {
5838
6153
  Plane
5839
6154
  };
5840
6155
  CameraControls.install({ THREE: subsetOfTHREE });
5841
- var logger$4 = createLogger("cameraController");
6156
+ var logger$5 = createLogger("cameraController");
5842
6157
  /**
5843
6158
  * Minimum z-axis separation between overlapping surfaces, in SVG/model units.
5844
6159
  *
@@ -5912,7 +6227,7 @@ var CameraController = class CameraController extends CameraControls {
5912
6227
  radius
5913
6228
  ].map((value) => +value.toFixed(2));
5914
6229
  const clippingPlanes = [this.camera.near, this.camera.far];
5915
- logger$4.debug("camera update %O", {
6230
+ logger$5.debug("camera update %O", {
5916
6231
  position,
5917
6232
  target,
5918
6233
  spherical,
@@ -6567,6 +6882,100 @@ var noopHandlers = {
6567
6882
  pitch: createNoopHandler()
6568
6883
  };
6569
6884
  //#endregion
6885
+ //#region src/util/asserts.ts
6886
+ /** Logger instance for assert warnings */
6887
+ var logger$4 = createLogger("");
6888
+ /**
6889
+ * Asserts the renderer has not been disposed.
6890
+ * @param renderer - Renderer instance to check
6891
+ * @param funcName - Name of the calling function for error messages
6892
+ * @returns false if disposed, true otherwise
6893
+ */
6894
+ function assertNotDisposed(renderer, funcName) {
6895
+ if (renderer.isDisposed) {
6896
+ logger$4.warn(`[${funcName}]: Renderer is used after being disposed. Please create a new instance.`);
6897
+ return false;
6898
+ }
6899
+ return true;
6900
+ }
6901
+ /**
6902
+ * Asserts the renderer has been initialized.
6903
+ * @param renderer - Renderer instance to check
6904
+ * @param funcName - Name of the calling function for error messages
6905
+ * @returns false if not initialized, true otherwise
6906
+ */
6907
+ function assertInitialized(renderer, funcName) {
6908
+ if (!renderer.isInitialized) {
6909
+ logger$4.warn(`${funcName}: Renderer is not initialized. Please call init() before using it.`);
6910
+ return false;
6911
+ }
6912
+ return true;
6913
+ }
6914
+ /**
6915
+ * Asserts the renderer has not been initialized yet.
6916
+ * @param renderer - Renderer instance to check
6917
+ * @param funcName - Name of the calling function for error messages
6918
+ * @returns false if already initialized, true otherwise
6919
+ */
6920
+ function assertNotInitialized(renderer, funcName) {
6921
+ if (renderer.isInitialized) {
6922
+ logger$4.warn(`${funcName}: Renderer is already initialized. Please call init() only once.`);
6923
+ return false;
6924
+ }
6925
+ return true;
6926
+ }
6927
+ /**
6928
+ * Asserts the renderer is not in external mode.
6929
+ * @param renderer - Renderer instance to check
6930
+ * @param funcName - Name of the calling function for error messages
6931
+ * @returns false if in external mode, true otherwise
6932
+ */
6933
+ function assertNotExternalMode(renderer, funcName) {
6934
+ if (renderer.isExternalMode) {
6935
+ logger$4.warn(`${funcName}: This operation is not supported in external mode.`);
6936
+ return false;
6937
+ }
6938
+ return true;
6939
+ }
6940
+ /**
6941
+ * Asserts that number array is a valid 4x4 matrix.
6942
+ * @param matrix number array to check
6943
+ * @param name name of the matrix for error messages
6944
+ * @param log optional logger instance to use for warning messages (defaults to root logger)
6945
+ * @returns true if matrix is valid, false otherwise
6946
+ */
6947
+ function assertValidMatrix(matrix, name, log = logger$4) {
6948
+ if (matrix.length !== 16) {
6949
+ log.warn(`${name}: Matrix must be 16 elements long`);
6950
+ return false;
6951
+ }
6952
+ return true;
6953
+ }
6954
+ /**
6955
+ * Wraps an API object so that every method is guarded by a precondition check.
6956
+ * When the guard (or a per-method override) fails, the corresponding fallback
6957
+ * method is called instead of the real implementation.
6958
+ * Non-function properties are passed through from `impl` (if present) or `fallback`.
6959
+ * @param impl - Real implementation, or null when the backing system doesn't exist
6960
+ * @param fallback - Noop / default implementation used when guard fails or impl is null
6961
+ * @param guard - Default guard applied to every method
6962
+ * @param guards - Optional per-method guard overrides (takes precedence over `guard`)
6963
+ * @returns A new object conforming to T with every method wrapped
6964
+ */
6965
+ function guardAPI(impl, fallback, guard, guards) {
6966
+ const guarded = {};
6967
+ for (const [key, fallbackVal] of Object.entries(fallback)) if (typeof fallbackVal === "function") {
6968
+ const implFn = impl?.[key];
6969
+ const methodGuard = guards?.[key] ?? guard;
6970
+ const fallbackFn = fallbackVal;
6971
+ guarded[key] = (...args) => {
6972
+ if (methodGuard(key) && implFn) return implFn(...args);
6973
+ return fallbackFn(...args);
6974
+ };
6975
+ } else guarded[key] = impl?.[key] ?? fallbackVal;
6976
+ return guarded;
6977
+ }
6978
+ //#endregion
6570
6979
  //#region src/core/updates.ts
6571
6980
  var logger$3 = createLogger("updates");
6572
6981
  /**
@@ -6752,6 +7161,85 @@ var InternalCameraSystem = class {
6752
7161
  }
6753
7162
  };
6754
7163
  //#endregion
7164
+ //#region src/space/external.ts
7165
+ var externalLogger = createLogger("external");
7166
+ /**
7167
+ * Camera system in external mode. Exposes dummy camera instance as a wrapper for external camera transforms,
7168
+ * and estimates zoom factor ndc -> world conversion.
7169
+ */
7170
+ var ExternalCameraSystem = class {
7171
+ /** External camera instance */
7172
+ camera = new Camera();
7173
+ ndcLeft = new Vector2(-1, 0);
7174
+ ndcRight = new Vector2(1, 0);
7175
+ intersectionPointLeft = new Vector3();
7176
+ intersectionPointRight = new Vector3();
7177
+ prevZoomFactor;
7178
+ mainMatrix4 = new Matrix4();
7179
+ mainMatrixInverse = new Matrix4();
7180
+ nearPoint = new Vector3();
7181
+ farPoint = new Vector3();
7182
+ viewDir = new Vector3();
7183
+ target = new Vector3();
7184
+ up = new Vector3();
7185
+ cameraPos = new Vector3();
7186
+ linearSystem = new Matrix3();
7187
+ rhs = new Vector3();
7188
+ /**
7189
+ * @param ctx {@link RendererContext} instance
7190
+ * @param pickingSystem {@link PickingSystem} instance
7191
+ */
7192
+ constructor(ctx, pickingSystem) {
7193
+ this.ctx = ctx;
7194
+ this.pickingSystem = pickingSystem;
7195
+ this.camera.matrixAutoUpdate = false;
7196
+ this.camera.matrixWorldAutoUpdate = false;
7197
+ }
7198
+ /** Current zoom factor estimation */
7199
+ get zoomFactor() {
7200
+ const estimatedZoomFactor = this.estimateZoomFactor();
7201
+ const newZoomFactor = estimatedZoomFactor ?? this.prevZoomFactor ?? 1;
7202
+ if (estimatedZoomFactor) this.prevZoomFactor = estimatedZoomFactor;
7203
+ return newZoomFactor;
7204
+ }
7205
+ /**
7206
+ * Decompose the combined world-to-clip-space matrix into a rigid view matrix and a projection
7207
+ * matrix, then publish all four matrices (and their inverses) on the underlying camera.
7208
+ * @param mainMatrix Combined matrix mapping world space directly to clip space.
7209
+ */
7210
+ setCameraProjection(mainMatrix) {
7211
+ if (!assertValidMatrix(mainMatrix, "setCameraProjection", externalLogger)) return;
7212
+ const M = this.mainMatrix4.fromArray(mainMatrix);
7213
+ const e = M.elements;
7214
+ this.mainMatrixInverse.copy(M).invert();
7215
+ this.linearSystem.set(e[0], e[4], e[8], e[1], e[5], e[9], e[3], e[7], e[11]);
7216
+ this.rhs.set(-e[12], -e[13], -e[15]);
7217
+ this.cameraPos.copy(this.rhs).applyMatrix3(this.linearSystem.invert());
7218
+ this.nearPoint.set(0, 0, 1).applyMatrix4(this.mainMatrixInverse);
7219
+ this.farPoint.set(0, 0, -1).applyMatrix4(this.mainMatrixInverse);
7220
+ this.viewDir.copy(this.farPoint).sub(this.nearPoint).normalize();
7221
+ this.up.set(0, 0, 1);
7222
+ if (Math.abs(this.up.dot(this.viewDir)) > .999) this.up.set(0, 1, 0);
7223
+ this.target.copy(this.cameraPos).add(this.viewDir);
7224
+ const camera = this.camera;
7225
+ camera.matrixWorld.lookAt(this.cameraPos, this.target, this.up);
7226
+ camera.matrixWorld.setPosition(this.cameraPos);
7227
+ camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
7228
+ camera.projectionMatrix.multiplyMatrices(M, camera.matrixWorld);
7229
+ camera.projectionMatrixInverse.copy(camera.projectionMatrix).invert();
7230
+ }
7231
+ estimateZoomFactor() {
7232
+ const [bufferW, bufferH] = this.ctx.getDrawingBufferSizePx();
7233
+ if (bufferW <= 0 || bufferH <= 0) return;
7234
+ const worldPoint1 = this.pickingSystem.intersectPlane(this.ndcLeft, this.camera, this.intersectionPointLeft);
7235
+ const worldPoint2 = this.pickingSystem.intersectPlane(this.ndcRight, this.camera, this.intersectionPointRight);
7236
+ if (!worldPoint1 || !worldPoint2) return;
7237
+ const worldLength = worldPoint2.sub(worldPoint1).length();
7238
+ if (worldLength === 0) return;
7239
+ return bufferW / worldLength;
7240
+ }
7241
+ };
7242
+ //#endregion
6755
7243
  //#region src/space/picking.ts
6756
7244
  /**
6757
7245
  * Picking subsystem.
@@ -6826,7 +7314,8 @@ var SceneSystem = class {
6826
7314
  * @returns Scale factor (model space to world space)
6827
7315
  */
6828
7316
  scaleFactor(sceneId) {
6829
- return this.getSceneStateById(sceneId).worldMatrix.elements[0];
7317
+ const sceneState = this.getSceneStateById(sceneId);
7318
+ return Math.abs(sceneState.worldMatrix.elements[0]);
6830
7319
  }
6831
7320
  /**
6832
7321
  * Register a new scene from it's definition
@@ -7194,7 +7683,7 @@ var Renderer = class {
7194
7683
  this.renderer.autoClear = !this.isExternalMode;
7195
7684
  this.ctx = this.createRendererContext();
7196
7685
  this.eventSystem = new EventSystem();
7197
- this.lightsSystem = new LightsSystem();
7686
+ this.lightsSystem = new LightsSystem(this.ctx);
7198
7687
  this.layerSystem = new LayerSystem(this.ctx);
7199
7688
  this.viewportSystem = new ViewportSystem(this.ctx, this.eventSystem);
7200
7689
  this.updatesSystem = new UpdatesSystem(this.ctx, this.layerSystem);
@@ -7293,6 +7782,18 @@ var Renderer = class {
7293
7782
  return this.disposed;
7294
7783
  }
7295
7784
  /**
7785
+ * Loads a GLB file and returns a Three.js model.
7786
+ * @param models - The models to load.
7787
+ */
7788
+ async loadModel(models) {
7789
+ this.init();
7790
+ const { root, sceneDef } = await loadGLB(models);
7791
+ const scene = this.viewportSystem.initScene(sceneDef);
7792
+ scene.background = new Color(15461355);
7793
+ scene.add(root);
7794
+ this.lightsSystem.initLights(scene);
7795
+ }
7796
+ /**
7296
7797
  * Initializes viewport and scene with the given scene definition.
7297
7798
  * Should be called once on startup. Repeated calls will produce console warnings.
7298
7799
  * @param sceneDef {@link SceneDef} to render
@@ -7361,6 +7862,7 @@ var Renderer = class {
7361
7862
  } else if (sceneState.scene.children.length === 0) {
7362
7863
  const root = this.layerSystem.buildScene(sceneState.sceneDef);
7363
7864
  scene.add(root);
7865
+ this.lightsSystem.initLights(scene);
7364
7866
  }
7365
7867
  }
7366
7868
  const justLoaded = loadedChanged && sceneState.loaded;
@@ -7368,8 +7870,7 @@ var Renderer = class {
7368
7870
  this.viewportSystem.updatePtScale(id);
7369
7871
  const hasDefsUpdated = this.updatesSystem.processPendingUpdates(frustum, 3);
7370
7872
  const forceRedraw = hasControlsUpdated || hasDefsUpdated || justLoaded || this.isExternalMode || this.ui;
7371
- if (this.isExternalMode) patchMatricesInExternalMode(scene);
7372
- this.lightsSystem.updateLights(scene, camera);
7873
+ this.lightsSystem.updateLights(sceneState, camera);
7373
7874
  if (this.needsRedraw || forceRedraw) this.renderer.render(scene, camera);
7374
7875
  if (hasDefsUpdated || this.needsRedraw) this.viewportSystem.invalidateSceneBounds(sceneState);
7375
7876
  }
@@ -7475,4 +7976,4 @@ var Renderer = class {
7475
7976
  }
7476
7977
  };
7477
7978
  //#endregion
7478
- export { Polygon, Rect, Renderer, isImageDef, isImageLayer, isLayerDef, isLayerLayer, isLineDef, isLineLayer, isShapeDef, isShapeLayer, isTextDef, isTextLayer };
7979
+ export { Polygon, Rect, Renderer, glb2gltf, isImageDef, isImageLayer, isLayerDef, isLayerLayer, isLineDef, isLineLayer, isModelDef, isModelLayer, isShapeDef, isShapeLayer, isTextDef, isTextLayer, splitGltf };