@expofp/renderer 3.1.6 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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);
@@ -1067,7 +1070,6 @@ var dimColorFrag = `
1067
1070
  var POLYGON_OFFSET_MULTIPLIER = 12;
1068
1071
  var sharedParameters = {
1069
1072
  side: DoubleSide,
1070
- transparent: true,
1071
1073
  forceSinglePass: true,
1072
1074
  depthFunc: AlwaysDepth
1073
1075
  };
@@ -1075,6 +1077,7 @@ var sharedParameters = {
1075
1077
  var MaterialSystem = class {
1076
1078
  backgroundMaterial;
1077
1079
  viewport = new Vector4();
1080
+ lightingMaterials = [];
1078
1081
  /**
1079
1082
  * Creates a line material.
1080
1083
  * @param params {@link LineMaterialParameters}
@@ -1103,8 +1106,7 @@ var MaterialSystem = class {
1103
1106
  uniforms["resolution"].value.set(this.viewport.z, this.viewport.w);
1104
1107
  }
1105
1108
  };
1106
- addDimToMaterial(material);
1107
- this.addPolygonOffset(material);
1109
+ this.patchMaterial(material);
1108
1110
  return material;
1109
1111
  }
1110
1112
  /**
@@ -1113,14 +1115,21 @@ var MaterialSystem = class {
1113
1115
  * @returns MeshBasicMaterial instance
1114
1116
  */
1115
1117
  createColorMaterial(params = {}) {
1116
- const material = new (params.lightingEnable ? MeshPhongMaterial : MeshBasicMaterial)({
1118
+ const materialConstructor = params.lightingEnable ? MeshPhysicalMaterial : MeshBasicMaterial;
1119
+ const color = params.color ?? 16777215;
1120
+ const opacity = params.opacity ?? 1;
1121
+ const transparent = opacity < 1;
1122
+ const material = new materialConstructor({
1117
1123
  ...sharedParameters,
1118
- color: params.color ?? 16777215,
1119
- opacity: params.opacity ?? 1
1124
+ color,
1125
+ opacity,
1126
+ transparent
1120
1127
  });
1121
- if (params.depthEnable) material.depthFunc = LessEqualDepth;
1122
- addDimToMaterial(material);
1123
- this.addPolygonOffset(material);
1128
+ if (transparent) {
1129
+ material.depthWrite = false;
1130
+ material.depthFunc = LessDepth;
1131
+ } else if (params.depthEnable) material.depthFunc = LessEqualDepth;
1132
+ this.patchMaterial(material, { lightingEnable: params.lightingEnable });
1124
1133
  return material;
1125
1134
  }
1126
1135
  /**
@@ -1131,18 +1140,37 @@ var MaterialSystem = class {
1131
1140
  createTextureMaterial(params) {
1132
1141
  const material = new MeshBasicMaterial({
1133
1142
  ...sharedParameters,
1134
- map: params.map
1143
+ map: params.map,
1144
+ transparent: true,
1145
+ depthFunc: LessDepth
1135
1146
  });
1136
- if (params.depthEnable) material.depthFunc = LessEqualDepth;
1137
1147
  if (params.uvOffset) material.onBeforeCompile = (shader) => {
1138
1148
  shader.vertexShader = shader.vertexShader.replace("#include <uv_vertex>", `
1139
1149
  #include <uv_vertex>
1140
1150
  vMapUv = uv * uvOffset.zw + uvOffset.xy;
1141
1151
  `);
1142
1152
  };
1153
+ this.patchMaterial(material);
1154
+ return material;
1155
+ }
1156
+ /**
1157
+ * Apply the engine's cross-cutting material patches: dim shader hooks, polygon offset,
1158
+ * and (optional) registration with the lighting system. Used internally by every
1159
+ * factory in this class and exposed publicly so externally-built materials (e.g. PBR
1160
+ * materials cloned from GLTF source) can opt in to the same behavior.
1161
+ * @param material material to patch
1162
+ * @param opts patch options
1163
+ * @param opts.lightingEnable when true and the material is a MeshPhysicalMaterial,
1164
+ * configures it for the project's lighting pipeline and registers it
1165
+ */
1166
+ patchMaterial(material, opts = {}) {
1143
1167
  addDimToMaterial(material);
1144
1168
  this.addPolygonOffset(material);
1145
- return material;
1169
+ if (opts.lightingEnable && material instanceof MeshPhysicalMaterial) {
1170
+ material.ior = 1;
1171
+ material.specularIntensity = 0;
1172
+ this.lightingMaterials.push(material);
1173
+ }
1146
1174
  }
1147
1175
  /**
1148
1176
  * Creates a background material. Used for the background layer to support dimming the background.
@@ -1213,6 +1241,9 @@ function isTextDef(def) {
1213
1241
  function isLineDef(def) {
1214
1242
  return def.points !== void 0;
1215
1243
  }
1244
+ function isModelDef(def) {
1245
+ return def.url !== void 0;
1246
+ }
1216
1247
  function isLayerDef(def) {
1217
1248
  return def.children !== void 0;
1218
1249
  }
@@ -1228,6 +1259,9 @@ function isTextLayer(layer) {
1228
1259
  function isLineLayer(layer) {
1229
1260
  return layer.children[0] && isLineDef(layer.children[0]);
1230
1261
  }
1262
+ function isModelLayer(layer) {
1263
+ return layer.children[0] && isModelDef(layer.children[0]);
1264
+ }
1231
1265
  function isLayerLayer(layer) {
1232
1266
  return layer.children[0] && isLayerDef(layer.children[0]);
1233
1267
  }
@@ -1341,13 +1375,14 @@ var RenderableSystem = class {
1341
1375
  * @param firstUpdate whether this is the first update for this def. Set to true when the def is first added to the scene.
1342
1376
  */
1343
1377
  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);
1378
+ const mappings = this.getObjectInstanceByDef(def);
1379
+ if (!mappings) return;
1380
+ for (const { object: mesh, instanceIds } of mappings) {
1381
+ for (const instanceId of instanceIds) mesh.setVisibleAt(instanceId, !def.hidden);
1382
+ if (def.hidden && !firstUpdate) continue;
1383
+ for (const instanceId of instanceIds) toggleInstanceDim(mesh, instanceId, def.dim);
1384
+ this.updateDefImpl(def, mesh, instanceIds, firstUpdate);
1385
+ }
1351
1386
  }
1352
1387
  /**
1353
1388
  * Update an existing collection with a new layer definition.
@@ -1418,7 +1453,8 @@ var RenderableSystem = class {
1418
1453
  this.logger.debug(`Tried to register def with empty instanceIds %O`, def);
1419
1454
  return;
1420
1455
  }
1421
- this.mapDefToObject.set(def, {
1456
+ if (!this.mapDefToObject.has(def)) this.mapDefToObject.set(def, []);
1457
+ this.mapDefToObject.get(def).push({
1422
1458
  object,
1423
1459
  instanceIds: ids
1424
1460
  });
@@ -1427,29 +1463,57 @@ var RenderableSystem = class {
1427
1463
  this.updateDef(def, true);
1428
1464
  }
1429
1465
  /**
1430
- * Unregister a def and its associated object mapping.
1466
+ * Register a single instance mapping between a def and an object/instance, appending
1467
+ * one entry per call. Use when a single def maps to multiple instances across one or
1468
+ * more containers (e.g. a model spread across several BatchedMesh batches). Pushes
1469
+ * once per instance into the object→defs map so `getDefsByObject(obj)[batchId]` stays
1470
+ * aligned with sequential `BatchedMesh.addInstance` ids.
1471
+ * @param def def to register
1472
+ * @param object container the def's instance lives in
1473
+ * @param instanceId single instance id in the container
1474
+ */
1475
+ registerDefInstance(def, object, instanceId) {
1476
+ if (!this.mapDefToObject.has(def)) this.mapDefToObject.set(def, []);
1477
+ this.mapDefToObject.get(def).push({
1478
+ object,
1479
+ instanceIds: [instanceId]
1480
+ });
1481
+ if (!this.mapObjectToDefs.has(object)) this.mapObjectToDefs.set(object, []);
1482
+ this.mapObjectToDefs.get(object).push(def);
1483
+ this.updateDef(def, true);
1484
+ }
1485
+ /**
1486
+ * Unregister a def and its associated object mappings.
1431
1487
  * @param def def to unregister
1432
1488
  */
1433
1489
  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);
1490
+ const mappings = this.mapDefToObject.get(def);
1491
+ if (mappings) {
1492
+ for (const { object } of mappings) {
1493
+ const defs = this.mapObjectToDefs.get(object);
1494
+ if (defs) {
1495
+ this.mapObjectToDefs.set(object, defs.filter((d) => d !== def));
1496
+ if (this.mapObjectToDefs.get(object).length === 0) this.mapObjectToDefs.delete(object);
1497
+ }
1441
1498
  }
1442
1499
  this.mapDefToObject.delete(def);
1443
1500
  }
1444
1501
  }
1445
1502
  /**
1446
- * Unregister all defs associated with an object.
1503
+ * Unregister all defs associated with an object. Only removes the entries that point
1504
+ * at this object — defs with mappings to other containers keep those entries intact.
1447
1505
  * @param object object to unregister
1448
1506
  */
1449
1507
  unregisterObject(object) {
1450
1508
  const defs = this.mapObjectToDefs.get(object);
1451
1509
  if (defs) {
1452
- for (const def of defs) this.mapDefToObject.delete(def);
1510
+ for (const def of defs) {
1511
+ const mappings = this.mapDefToObject.get(def);
1512
+ if (!mappings) continue;
1513
+ const remaining = mappings.filter((m) => m.object !== object);
1514
+ if (remaining.length === 0) this.mapDefToObject.delete(def);
1515
+ else this.mapDefToObject.set(def, remaining);
1516
+ }
1453
1517
  this.mapObjectToDefs.delete(object);
1454
1518
  }
1455
1519
  }
@@ -1461,17 +1525,18 @@ var RenderableSystem = class {
1461
1525
  this.mapObjectToDefs.clear();
1462
1526
  }
1463
1527
  /**
1464
- * Lookup object/instance by def.
1528
+ * Lookup object/instance mappings by def. Returns an array because a single def can
1529
+ * map to multiple containers (e.g. a model spread across batches keyed by material).
1465
1530
  * @param def def to lookup
1466
- * @returns object and instance ids
1531
+ * @returns array of object/instance-id mappings, or undefined if none
1467
1532
  */
1468
1533
  getObjectInstanceByDef(def) {
1469
- const mapping = this.mapDefToObject.get(def);
1470
- if (!mapping) {
1534
+ const mappings = this.mapDefToObject.get(def);
1535
+ if (!mappings || mappings.length === 0) {
1471
1536
  this.logger.debug(`No object mapping found for def %O`, def);
1472
1537
  return;
1473
1538
  }
1474
- return mapping;
1539
+ return mappings;
1475
1540
  }
1476
1541
  /**
1477
1542
  * Lookup defs by object.
@@ -1491,7 +1556,7 @@ var RenderableSystem = class {
1491
1556
  };
1492
1557
  //#endregion
1493
1558
  //#region src/geometry/image.ts
1494
- var logger$12 = createLogger("image");
1559
+ var logger$14 = createLogger("image");
1495
1560
  /**
1496
1561
  * A system that handles the rendering of image defs.
1497
1562
  */
@@ -1509,10 +1574,10 @@ var ImageSystem = class extends RenderableSystem {
1509
1574
  * @param materialSystem {@link MaterialSystem}
1510
1575
  */
1511
1576
  constructor(ctx, materialSystem) {
1512
- super("image", ctx, logger$12);
1577
+ super("image", ctx, logger$14);
1513
1578
  this.materialSystem = materialSystem;
1514
1579
  const atlasTextureSize = ctx.three.capabilities.maxTextureSize;
1515
- logger$12.debug(`Max texture size: ${atlasTextureSize}`);
1580
+ logger$14.debug(`Max texture size: ${atlasTextureSize}`);
1516
1581
  this.packer = new MaxRectsPacker(atlasTextureSize, atlasTextureSize, 1, { pot: false });
1517
1582
  }
1518
1583
  dispose() {
@@ -1583,7 +1648,7 @@ var ImageSystem = class extends RenderableSystem {
1583
1648
  oldTexture?.dispose();
1584
1649
  mesh.visible = true;
1585
1650
  data.appliedScale = targetScale;
1586
- logger$12.debug(`Rendered atlas for ${mesh.name || mesh.parent?.name} at scale ${targetScale.toFixed(3)}`);
1651
+ logger$14.debug(`Rendered atlas for ${mesh.name || mesh.parent?.name} at scale ${targetScale.toFixed(3)}`);
1587
1652
  }
1588
1653
  }
1589
1654
  disposeObject(object) {
@@ -1603,7 +1668,7 @@ var ImageSystem = class extends RenderableSystem {
1603
1668
  }
1604
1669
  computeResizeFactor() {
1605
1670
  if (!this.memoryLimitMb) {
1606
- logger$12.debug("Memory limit is not set, atlases will not be scaled");
1671
+ logger$14.debug("Memory limit is not set, atlases will not be scaled");
1607
1672
  return 1;
1608
1673
  }
1609
1674
  let totalResizable = 0;
@@ -1614,17 +1679,17 @@ var ImageSystem = class extends RenderableSystem {
1614
1679
  else totalNonResizable += bytes;
1615
1680
  }
1616
1681
  if (totalResizable === 0) {
1617
- logger$12.debug("No resizable atlases, atlases will not be scaled");
1682
+ logger$14.debug("No resizable atlases, atlases will not be scaled");
1618
1683
  return 1;
1619
1684
  }
1620
1685
  const budget = this.memoryLimitMb * 1024 * 1024 - totalNonResizable;
1621
1686
  if (budget <= 0) {
1622
- logger$12.debug("Memory limit is too low, unable to resize textures.");
1687
+ logger$14.debug("Memory limit is too low, unable to resize textures.");
1623
1688
  return 1;
1624
1689
  }
1625
1690
  const factor = Math.sqrt(budget / totalResizable);
1626
1691
  if (factor >= 1) return 1;
1627
- logger$12.debug(`Resize factor: ${factor.toFixed(3)} (budget ${budget} bytes, resizable ${totalResizable} bytes)`);
1692
+ logger$14.debug(`Resize factor: ${factor.toFixed(3)} (budget ${budget} bytes, resizable ${totalResizable} bytes)`);
1628
1693
  return factor;
1629
1694
  }
1630
1695
  packImages(images) {
@@ -1646,8 +1711,8 @@ var ImageSystem = class extends RenderableSystem {
1646
1711
  const boundsHeight = image.bounds.size.y;
1647
1712
  const ratio = sourceArea / (boundsWidth * boundsHeight);
1648
1713
  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.`);
1714
+ logger$14.debug(`Image bounds: ${boundsWidth}x${boundsHeight}`, `Image source: ${sourceWidth}x${sourceHeight}`);
1715
+ logger$14.warn(`Image bounds area is ${ratio.toFixed(2)} times smaller than the image.`);
1651
1716
  }
1652
1717
  const rect = new Rectangle(image.source.width, image.source.height);
1653
1718
  rect.data = [imageWithIndex];
@@ -1656,7 +1721,7 @@ var ImageSystem = class extends RenderableSystem {
1656
1721
  } else existingRect.data.push(imageWithIndex);
1657
1722
  }
1658
1723
  this.packer.addArray(rectangles);
1659
- this.packer.bins.forEach((bin) => logger$12.debug(`Bin: ${bin.width}x${bin.height}, ${bin.rects.length} rectangles`));
1724
+ this.packer.bins.forEach((bin) => logger$14.debug(`Bin: ${bin.width}x${bin.height}, ${bin.rects.length} rectangles`));
1660
1725
  return this.packer.bins;
1661
1726
  }
1662
1727
  };
@@ -1668,7 +1733,7 @@ function createAtlas(bin, scale) {
1668
1733
  const ctx = canvas.getContext("2d");
1669
1734
  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
1735
  const t1 = performance.now();
1671
- logger$12.debug(`Create atlas (${canvas.width}x${canvas.height}) took ${(t1 - t0).toFixed(2)} milliseconds.`);
1736
+ logger$14.debug(`Create atlas (${canvas.width}x${canvas.height}) took ${(t1 - t0).toFixed(2)} milliseconds.`);
1672
1737
  return createTexture(canvas);
1673
1738
  }
1674
1739
  function createPlaceholderTexture() {
@@ -1687,7 +1752,7 @@ function getAtlasSizeBytes(width, height) {
1687
1752
  }
1688
1753
  //#endregion
1689
1754
  //#region src/geometry/line.ts
1690
- var logger$11 = createLogger("line");
1755
+ var logger$13 = createLogger("line");
1691
1756
  /**
1692
1757
  * A system that handles the rendering of line defs.
1693
1758
  */
@@ -1698,7 +1763,7 @@ var LineSystem = class extends RenderableSystem {
1698
1763
  * @param materialSystem {@link MaterialSystem}
1699
1764
  */
1700
1765
  constructor(ctx, materialSystem) {
1701
- super("line", ctx, logger$11);
1766
+ super("line", ctx, logger$13);
1702
1767
  this.materialSystem = materialSystem;
1703
1768
  }
1704
1769
  buildLayer(layer) {
@@ -2106,7 +2171,7 @@ function computeBoundingSphere(bounds, origin, out = new Sphere()) {
2106
2171
  }
2107
2172
  //#endregion
2108
2173
  //#region src/geometry/mesh.ts
2109
- var logger$10 = createLogger("mesh");
2174
+ var logger$12 = createLogger("mesh");
2110
2175
  extend([namesPlugin]);
2111
2176
  /**
2112
2177
  * A system that handles the rendering of shape defs.
@@ -2125,7 +2190,7 @@ var MeshSystem = class extends RenderableSystem {
2125
2190
  * @param materialSystem {@link MaterialSystem}
2126
2191
  */
2127
2192
  constructor(ctx, materialSystem) {
2128
- super("mesh", ctx, logger$10);
2193
+ super("mesh", ctx, logger$12);
2129
2194
  this.materialSystem = materialSystem;
2130
2195
  }
2131
2196
  buildLayer(layer) {
@@ -2164,24 +2229,25 @@ var MeshSystem = class extends RenderableSystem {
2164
2229
  const isPolygon = shape instanceof Polygon;
2165
2230
  const isRect = shape instanceof Rect;
2166
2231
  if (expectedShapeType === "polygon" && !isPolygon || expectedShapeType === "rect" && !isRect) {
2167
- logger$10.warn("Shape type changing not supported %O", shapeDef);
2232
+ logger$12.warn("Shape type changing not supported %O", shapeDef);
2168
2233
  return;
2169
2234
  }
2170
2235
  if (isPolygon) {
2171
2236
  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);
2237
+ let newGeometry = this.buildPolygonGeometry(shape);
2238
+ if (mesh.userData["is3D"]) newGeometry = processGeometryFor3D(newGeometry);
2239
+ const { vertices, indices } = countGeometry(newGeometry);
2240
+ if (vertices != geometryRange?.reservedVertexCount || indices != Math.max(geometryRange.reservedIndexCount, 0)) {
2241
+ logger$12.warn("Polygon geometry changing not supported %O", shapeDef);
2174
2242
  return;
2175
2243
  }
2176
- const geometry = this.buildPolygonGeometry(shape);
2177
- if (mesh.geometry.hasAttribute("normal")) geometry.computeVertexNormals();
2178
- mesh.setGeometryAt(geometryId, geometry);
2244
+ mesh.setGeometryAt(geometryId, newGeometry);
2179
2245
  } else if (isRect) this.updateRect(shape, mesh, instanceId);
2180
2246
  }
2181
2247
  updateColor(shapeDef, mesh, instanceId) {
2182
2248
  const color = this.normalizeColor(shapeDef.color);
2183
2249
  if (!color) {
2184
- logger$10.warn(`Invalid color: ${shapeDef.color} %O`, shapeDef);
2250
+ logger$12.warn(`Invalid color: ${shapeDef.color} %O`, shapeDef);
2185
2251
  return;
2186
2252
  }
2187
2253
  mesh.setColorAt(instanceId, this.color.setRGB(color.r / 255, color.g / 255, color.b / 255, SRGBColorSpace));
@@ -2207,10 +2273,7 @@ var MeshSystem = class extends RenderableSystem {
2207
2273
  ({vertices, indices} = countGeometry(rectGeom));
2208
2274
  } else if (shapeDef.shape instanceof Polygon) {
2209
2275
  let geometry = this.buildPolygonGeometry(shapeDef.shape);
2210
- if (is3D) {
2211
- geometry = geometry.toNonIndexed();
2212
- geometry.computeVertexNormals();
2213
- }
2276
+ if (is3D) geometry = processGeometryFor3D(geometry);
2214
2277
  shapeDefToGeometry.set(shapeDef, geometry);
2215
2278
  ({vertices, indices} = countGeometry(geometry));
2216
2279
  }
@@ -2224,7 +2287,8 @@ var MeshSystem = class extends RenderableSystem {
2224
2287
  });
2225
2288
  const batchedMesh = new BatchedMesh$1(shapes.length, vertexCount, indexCount, material);
2226
2289
  const rectGeometryId = rectAdded ? batchedMesh.addGeometry(rectGeom) : void 0;
2227
- batchedMesh.setCustomSort((list) => this.sortInstances(batchedMesh, list));
2290
+ batchedMesh.setCustomSort((list) => this.sortInstances(batchedMesh, is3D, opacity < 1, list));
2291
+ if (is3D) batchedMesh.userData["is3D"] = true;
2228
2292
  for (const shapeDef of shapes) {
2229
2293
  let instanceId;
2230
2294
  let type;
@@ -2254,18 +2318,316 @@ var MeshSystem = class extends RenderableSystem {
2254
2318
  buildPolygonGeometry(polygon) {
2255
2319
  return new BufferGeometry().setFromPoints(polygon.vertices).setIndex(polygon.indices.flat());
2256
2320
  }
2257
- sortInstances(mesh, list) {
2321
+ sortInstances(mesh, is3D, isTransparent, list) {
2258
2322
  const shapeDefs = this.getDefsByObject(mesh);
2259
2323
  list.sort((a, b) => {
2260
2324
  const aDef = shapeDefs[a.index];
2261
2325
  const bDef = shapeDefs[b.index];
2262
- return (aDef.dim === false ? 1 : 0) - (bDef.dim === false ? 1 : 0);
2326
+ const aDim = aDef.dim === false ? 1 : 0;
2327
+ const bDim = bDef.dim === false ? 1 : 0;
2328
+ if (aDim !== bDim) return aDim - bDim;
2329
+ if (!is3D) return 0;
2330
+ return isTransparent ? sortTransparent(a, b) : sortOpaque(a, b);
2263
2331
  });
2264
2332
  }
2265
2333
  };
2334
+ function sortOpaque(a, b) {
2335
+ return a.z - b.z;
2336
+ }
2337
+ function sortTransparent(a, b) {
2338
+ return b.z - a.z;
2339
+ }
2340
+ function processGeometryFor3D(geometry) {
2341
+ let processedGeometry = geometry;
2342
+ processedGeometry = processedGeometry.toNonIndexed();
2343
+ processedGeometry.computeVertexNormals();
2344
+ processedGeometry = BufferGeometryUtils.toCreasedNormals(processedGeometry, Math.PI / 6);
2345
+ return processedGeometry;
2346
+ }
2347
+ //#endregion
2348
+ //#region src/geometry/model.ts
2349
+ var logger$11 = createLogger("model");
2350
+ /**
2351
+ * Converts the GLB Y-up coordinate system to the engine's Z-up convention.
2352
+ * Matches the rotation/scale applied at the scene root in {@link import("../loaders/glb.ts")}.
2353
+ */
2354
+ var Y_UP_TO_Z_UP = new Matrix4().makeRotationX(-Math.PI / 2).multiply(new Matrix4().makeScale(1, -1, 1));
2355
+ /**
2356
+ * A system that handles the rendering of external 3D model defs (e.g. GLB/glTF).
2357
+ * Loads models asynchronously, builds one {@link BatchedMesh} per (ModelDef, source
2358
+ * material) pair so each material clone can carry its def's opacity directly, and
2359
+ * routes shading through {@link MaterialSystem.patchMaterial} for dim + polygon offset.
2360
+ */
2361
+ var ModelSystem = class extends RenderableSystem {
2362
+ loader = new GLTFLoader();
2363
+ cachePromise = /* @__PURE__ */ new Map();
2364
+ pendingUpdates = /* @__PURE__ */ new Map();
2365
+ layerAborted = /* @__PURE__ */ new WeakSet();
2366
+ isDisposed = false;
2367
+ /**
2368
+ * @param ctx {@link RendererContext}
2369
+ * @param materialSystem {@link MaterialSystem}
2370
+ */
2371
+ constructor(ctx, materialSystem) {
2372
+ super("model", ctx, logger$11);
2373
+ this.materialSystem = materialSystem;
2374
+ const dracoLoader = new DRACOLoader();
2375
+ dracoLoader.setDecoderPath("https://www.gstatic.com/draco/versioned/decoders/1.5.7/");
2376
+ this.loader.setDRACOLoader(dracoLoader);
2377
+ this.loader.setMeshoptDecoder(MeshoptDecoder);
2378
+ }
2379
+ buildLayer(layer) {
2380
+ const group = new Group();
2381
+ if (layer.children.length === 0) return group;
2382
+ this.loadAndBuild(group, layer.children);
2383
+ return group;
2384
+ }
2385
+ updateDef(def, firstUpdate = false) {
2386
+ if (!this.getObjectInstanceByDef(def)) {
2387
+ this.pendingUpdates.set(def, def);
2388
+ return;
2389
+ }
2390
+ super.updateDef(def, firstUpdate);
2391
+ }
2392
+ disposeLayer(group) {
2393
+ this.layerAborted.add(group);
2394
+ super.disposeLayer(group);
2395
+ }
2396
+ dispose() {
2397
+ this.isDisposed = true;
2398
+ super.dispose();
2399
+ for (const cachePromise of this.cachePromise.values()) cachePromise.then((cache) => disposeGltfCache(cache)).catch(() => void 0);
2400
+ this.cachePromise.clear();
2401
+ this.pendingUpdates.clear();
2402
+ }
2403
+ disposeObject(object) {
2404
+ disposeObject(object, { textures: false });
2405
+ }
2406
+ updateDefImpl(def, mesh, instanceIds) {
2407
+ const extents = mesh.userData["modelExtents"];
2408
+ if (extents) {
2409
+ const matrix = composeInstanceMatrix(def, extents);
2410
+ for (const instanceId of instanceIds) mesh.setMatrixAt(instanceId, matrix);
2411
+ }
2412
+ const material = mesh.material;
2413
+ const opacity = def.opacity ?? 1;
2414
+ material.opacity = opacity;
2415
+ const srcTransparent = material.userData["srcTransparent"] === true;
2416
+ material.transparent = opacity < 1 || srcTransparent;
2417
+ material.depthWrite = !material.transparent;
2418
+ }
2419
+ async loadAndBuild(group, models) {
2420
+ let caches;
2421
+ try {
2422
+ caches = await Promise.all(models.map((def) => this.loadCached(def.url)));
2423
+ } catch (err) {
2424
+ logger$11.warn("Failed to load model batch %O", err);
2425
+ return;
2426
+ }
2427
+ if (this.isDisposed || this.layerAborted.has(group)) return;
2428
+ const batches = [];
2429
+ for (let i = 0; i < models.length; i++) {
2430
+ const cache = caches[i];
2431
+ if (!cache || cache.entries.length === 0) continue;
2432
+ batches.push(...this.buildBatchesForDef(models[i], cache));
2433
+ }
2434
+ if (this.isDisposed || this.layerAborted.has(group)) {
2435
+ for (const batch of batches) {
2436
+ this.disposeObject(batch);
2437
+ this.unregisterObject(batch);
2438
+ }
2439
+ return;
2440
+ }
2441
+ group.add(...batches);
2442
+ for (const batch of batches) batch.renderOrder = group.renderOrder;
2443
+ for (const [def, latest] of this.pendingUpdates) if (this.getObjectInstanceByDef(def)) {
2444
+ this.pendingUpdates.delete(def);
2445
+ this.updateDef(latest);
2446
+ }
2447
+ }
2448
+ buildBatchesForDef(def, cache) {
2449
+ const buckets = /* @__PURE__ */ new Map();
2450
+ for (const entry of cache.entries) {
2451
+ const key = `${entry.sourceMaterial.uuid}|${attributeSignature(entry.geometry)}`;
2452
+ let bucket = buckets.get(key);
2453
+ if (!bucket) {
2454
+ bucket = {
2455
+ sourceMaterial: entry.sourceMaterial,
2456
+ geometries: []
2457
+ };
2458
+ buckets.set(key, bucket);
2459
+ }
2460
+ bucket.geometries.push(entry.geometry);
2461
+ }
2462
+ const batches = [];
2463
+ for (const { sourceMaterial, geometries } of buckets.values()) {
2464
+ const material = this.createPatchedMaterial(sourceMaterial);
2465
+ let totalVertices = 0;
2466
+ let totalIndices = 0;
2467
+ for (const geometry of geometries) {
2468
+ totalVertices += geometry.attributes["position"].count;
2469
+ totalIndices += geometry.index?.count ?? 0;
2470
+ }
2471
+ const batch = new BatchedMesh$1(geometries.length, totalVertices, totalIndices, material);
2472
+ batch.userData["modelExtents"] = cache.extents;
2473
+ for (const geometry of geometries) {
2474
+ const geometryId = batch.addGeometry(geometry);
2475
+ const instanceId = batch.addInstance(geometryId);
2476
+ this.registerDefInstance(def, batch, instanceId);
2477
+ }
2478
+ batches.push(batch);
2479
+ }
2480
+ return batches;
2481
+ }
2482
+ createPatchedMaterial(source) {
2483
+ const physical = new MeshPhysicalMaterial();
2484
+ if (source instanceof MeshStandardMaterial || source instanceof MeshPhysicalMaterial) {
2485
+ physical.color.copy(source.color);
2486
+ physical.map = source.map;
2487
+ physical.normalMap = source.normalMap;
2488
+ physical.normalScale.copy(source.normalScale);
2489
+ physical.roughness = source.roughness;
2490
+ physical.roughnessMap = source.roughnessMap;
2491
+ physical.metalness = source.metalness;
2492
+ physical.metalnessMap = source.metalnessMap;
2493
+ physical.emissive.copy(source.emissive);
2494
+ physical.emissiveIntensity = source.emissiveIntensity;
2495
+ physical.emissiveMap = source.emissiveMap;
2496
+ physical.aoMap = source.aoMap;
2497
+ physical.aoMapIntensity = source.aoMapIntensity;
2498
+ physical.alphaMap = source.alphaMap;
2499
+ physical.alphaTest = source.alphaTest;
2500
+ }
2501
+ physical.side = source.side;
2502
+ physical.transparent = source.transparent;
2503
+ physical.opacity = source.opacity;
2504
+ physical.depthWrite = !source.transparent;
2505
+ physical.depthFunc = source.transparent ? LessDepth : LessEqualDepth;
2506
+ physical.userData["srcTransparent"] = source.transparent;
2507
+ this.materialSystem.patchMaterial(physical, { lightingEnable: true });
2508
+ return physical;
2509
+ }
2510
+ async loadCached(url) {
2511
+ let promise = this.cachePromise.get(url);
2512
+ if (!promise) {
2513
+ promise = this.parseGltf(url);
2514
+ this.cachePromise.set(url, promise);
2515
+ }
2516
+ try {
2517
+ return await promise;
2518
+ } catch (err) {
2519
+ logger$11.warn("Failed to load model %s: %O", url, err);
2520
+ this.cachePromise.delete(url);
2521
+ return;
2522
+ }
2523
+ }
2524
+ async parseGltf(url) {
2525
+ const entries = collectGltfEntries(await this.loader.loadAsync(url));
2526
+ const extents = new Box3();
2527
+ for (const { geometry } of entries) {
2528
+ if (!geometry.boundingBox) geometry.computeBoundingBox();
2529
+ extents.union(geometry.boundingBox);
2530
+ }
2531
+ return {
2532
+ extents,
2533
+ entries
2534
+ };
2535
+ }
2536
+ };
2537
+ /**
2538
+ * Traverses a loaded GLTF scene and collects baked geometries paired with their
2539
+ * source materials. Geometries are cloned, then transformed by
2540
+ * {@link Y_UP_TO_Z_UP} times the node's world matrix so vertices land in engine
2541
+ * space relative to the GLB root.
2542
+ * @param gltf parsed GLTF
2543
+ * @returns array of `{geometry, sourceMaterial}` entries, one per Mesh node
2544
+ */
2545
+ function collectGltfEntries(gltf) {
2546
+ const entries = [];
2547
+ gltf.scene.updateMatrixWorld(true);
2548
+ const tmp = new Matrix4();
2549
+ gltf.scene.traverse((node) => {
2550
+ if (!(node instanceof Mesh)) return;
2551
+ const mesh = node;
2552
+ if (!mesh.geometry || !mesh.material) return;
2553
+ const sourceMaterial = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
2554
+ 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);
2555
+ const baked = mesh.geometry.clone();
2556
+ tmp.multiplyMatrices(Y_UP_TO_Z_UP, mesh.matrixWorld);
2557
+ baked.applyMatrix4(tmp);
2558
+ baked.computeBoundingBox();
2559
+ baked.computeBoundingSphere();
2560
+ entries.push({
2561
+ geometry: baked,
2562
+ sourceMaterial
2563
+ });
2564
+ });
2565
+ return entries;
2566
+ }
2567
+ /**
2568
+ * Disposes a cached GLB's baked geometries and the unique textures referenced by its
2569
+ * source materials. Source textures are shared across batches, so this is the only
2570
+ * place they should be released.
2571
+ * @param cache cache entry to dispose
2572
+ */
2573
+ function disposeGltfCache(cache) {
2574
+ const seen = /* @__PURE__ */ new Set();
2575
+ const textureProps = [
2576
+ "map",
2577
+ "normalMap",
2578
+ "roughnessMap",
2579
+ "metalnessMap",
2580
+ "emissiveMap",
2581
+ "aoMap",
2582
+ "alphaMap"
2583
+ ];
2584
+ for (const { sourceMaterial } of cache.entries) {
2585
+ const matRecord = sourceMaterial;
2586
+ for (const prop of textureProps) {
2587
+ const tex = matRecord[prop];
2588
+ if (tex && tex instanceof Texture && !seen.has(tex)) {
2589
+ seen.add(tex);
2590
+ tex.dispose();
2591
+ }
2592
+ }
2593
+ }
2594
+ for (const { geometry } of cache.entries) geometry.dispose();
2595
+ }
2596
+ /**
2597
+ * Stable identifier for a geometry's attribute set. Used to sub-bucket geometries that
2598
+ * share a source material but have different attribute names (e.g. with vs without uv),
2599
+ * since {@link BatchedMesh.addGeometry} requires homogeneous attributes per batch.
2600
+ * @param geometry geometry to summarize
2601
+ * @returns comma-separated sorted attribute names
2602
+ */
2603
+ function attributeSignature(geometry) {
2604
+ return Object.keys(geometry.attributes).sort().join(",");
2605
+ }
2606
+ /**
2607
+ * Composes a per-instance world matrix from a ModelDef's bounds and the loaded GLB's
2608
+ * engine-space extents: scale uniformly to fit bounds.size in XY, rotate around Z by
2609
+ * bounds.rotation, and translate so the model sits at bounds.center with its base at
2610
+ * bounds.elevation.
2611
+ * @param def model definition
2612
+ * @param extents engine-space bounding box of the loaded GLB
2613
+ * @returns world matrix to set on a BatchedMesh instance
2614
+ */
2615
+ function composeInstanceMatrix(def, extents) {
2616
+ const size = extents.getSize(tempSize);
2617
+ const center = extents.getCenter(tempCenter);
2618
+ const sx = size.x > 0 ? def.bounds.size.x / size.x : 1;
2619
+ const sy = size.y > 0 ? def.bounds.size.y / size.y : 1;
2620
+ const scale = Math.min(sx, sy);
2621
+ 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));
2622
+ }
2623
+ var tempSize = new Vector3();
2624
+ var tempCenter = new Vector3();
2625
+ var tempScale = new Matrix4();
2626
+ var tempRotation = new Matrix4();
2627
+ var tempTranslation = new Matrix4();
2266
2628
  //#endregion
2267
2629
  //#region src/geometry/text.ts
2268
- var logger$9 = createLogger("text");
2630
+ var logger$10 = createLogger("text");
2269
2631
  /**
2270
2632
  * A system that handles the rendering of text defs.
2271
2633
  */
@@ -2286,7 +2648,7 @@ var TextSystem = class extends RenderableSystem {
2286
2648
  * @param materialSystem {@link MaterialSystem}
2287
2649
  */
2288
2650
  constructor(ctx, materialSystem) {
2289
- super("text", ctx, logger$9);
2651
+ super("text", ctx, logger$10);
2290
2652
  this.materialSystem = materialSystem;
2291
2653
  }
2292
2654
  dispose() {
@@ -2364,6 +2726,9 @@ var TextSystem = class extends RenderableSystem {
2364
2726
  const textDefs = layer.children;
2365
2727
  const batchedText = new BatchedText$1();
2366
2728
  batchedText.material = this.materialSystem.createColorMaterial({ depthEnable: is3D });
2729
+ batchedText.material.transparent = false;
2730
+ batchedText.material.blending = CustomBlending;
2731
+ batchedText.material.blendSrcAlpha = OneFactor;
2367
2732
  const mappingData = [];
2368
2733
  let instanceId = 0;
2369
2734
  for (const textDef of textDefs) {
@@ -2458,7 +2823,7 @@ function setAnchorsAndAlignment(text, alignment) {
2458
2823
  }
2459
2824
  //#endregion
2460
2825
  //#region src/geometry/layer.ts
2461
- var logger$8 = createLogger("layer");
2826
+ var logger$9 = createLogger("layer");
2462
2827
  /**
2463
2828
  * A system that handles the rendering of layer defs and scene graph building.
2464
2829
  */
@@ -2469,6 +2834,7 @@ var LayerSystem = class {
2469
2834
  imageSystem;
2470
2835
  textSystem;
2471
2836
  lineSystem;
2837
+ modelSystem;
2472
2838
  systems;
2473
2839
  mapLayerDefsToObjects = /* @__PURE__ */ new Map();
2474
2840
  mapLayerDefToParent = /* @__PURE__ */ new Map();
@@ -2483,11 +2849,13 @@ var LayerSystem = class {
2483
2849
  this.imageSystem = new ImageSystem(this.ctx, this.materialSystem);
2484
2850
  this.textSystem = new TextSystem(this.ctx, this.materialSystem);
2485
2851
  this.lineSystem = new LineSystem(this.ctx, this.materialSystem);
2852
+ this.modelSystem = new ModelSystem(this.ctx, this.materialSystem);
2486
2853
  this.systems = [
2487
2854
  this.meshSystem,
2488
2855
  this.imageSystem,
2489
2856
  this.textSystem,
2490
- this.lineSystem
2857
+ this.lineSystem,
2858
+ this.modelSystem
2491
2859
  ];
2492
2860
  }
2493
2861
  /**
@@ -2513,14 +2881,14 @@ var LayerSystem = class {
2513
2881
  */
2514
2882
  buildScene(sceneDef) {
2515
2883
  const renderOrder = this.initRenderOrder(sceneDef.rootLayer).map((layer) => this.getFullLayerName(layer));
2516
- logger$8.debug("Render order %O", renderOrder);
2884
+ logger$9.debug("Render order %O", renderOrder);
2517
2885
  const rootGroup = new Group();
2518
2886
  rootGroup.name = sceneDef.rootLayer.name;
2519
2887
  this.mapLayerDefsToObjects.set(sceneDef.rootLayer, rootGroup);
2520
2888
  if (sceneDef.background) rootGroup.add(this.createBackgroundLayer(sceneDef.background));
2521
2889
  for (const child of sceneDef.rootLayer.children) rootGroup.add(this.buildLayer(child));
2522
2890
  this.updateLayer(sceneDef.rootLayer, false);
2523
- printTree(rootGroup, logger$8.debug);
2891
+ printTree(rootGroup, logger$9.debug);
2524
2892
  if (sceneDef.memoryLimit) this.imageSystem.memoryLimitMb = sceneDef.memoryLimit;
2525
2893
  this.imageSystem.renderAtlases();
2526
2894
  return rootGroup;
@@ -2553,6 +2921,7 @@ var LayerSystem = class {
2553
2921
  else if (isImageDef(def)) this.imageSystem.updateDef(def);
2554
2922
  else if (isTextDef(def)) this.textSystem.updateDef(def);
2555
2923
  else if (isLineDef(def)) this.lineSystem.updateDef(def);
2924
+ else if (isModelDef(def)) this.modelSystem.updateDef(def);
2556
2925
  else this.updateLayer(def, true);
2557
2926
  }
2558
2927
  disposeLayerTree(layerDef) {
@@ -2563,6 +2932,7 @@ var LayerSystem = class {
2563
2932
  else if (isImageLayer(layerDef)) this.imageSystem.disposeLayer(layerObject);
2564
2933
  else if (isTextLayer(layerDef)) this.textSystem.disposeLayer(layerObject);
2565
2934
  else if (isLineLayer(layerDef)) this.lineSystem.disposeLayer(layerObject);
2935
+ else if (isModelLayer(layerDef)) this.modelSystem.disposeLayer(layerObject);
2566
2936
  this.mapLayerDefsToObjects.delete(layerDef);
2567
2937
  this.mapLayerDefToParent.delete(layerDef);
2568
2938
  this.renderOrderMap.delete(layerDef);
@@ -2576,6 +2946,7 @@ var LayerSystem = class {
2576
2946
  else if (isLineLayer(layerDef)) this.lineSystem.updateLayer(group, layerDef);
2577
2947
  else if (isTextLayer(layerDef)) this.textSystem.updateLayer(group, layerDef);
2578
2948
  else if (isShapeLayer(layerDef)) this.meshSystem.updateLayer(group, layerDef);
2949
+ else if (isModelLayer(layerDef)) this.modelSystem.updateLayer(group, layerDef);
2579
2950
  this.setLayerName(group, group.name);
2580
2951
  }
2581
2952
  layerObject.visible = !layerDef.hidden && layerDef.children.length > 0;
@@ -2585,12 +2956,13 @@ var LayerSystem = class {
2585
2956
  }
2586
2957
  buildLayer(layerDef, parentPrefix = "") {
2587
2958
  const layerFullName = parentPrefix + layerDef.name;
2588
- logger$8.debug(`Building layer ${layerFullName}...`);
2959
+ logger$9.debug(`Building layer ${layerFullName}...`);
2589
2960
  let layerObject;
2590
2961
  if (isShapeLayer(layerDef)) layerObject = this.meshSystem.buildLayer(layerDef);
2591
2962
  else if (isImageLayer(layerDef)) layerObject = this.imageSystem.buildLayer(layerDef);
2592
2963
  else if (isTextLayer(layerDef)) layerObject = this.textSystem.buildLayer(layerDef);
2593
2964
  else if (isLineLayer(layerDef)) layerObject = this.lineSystem.buildLayer(layerDef);
2965
+ else if (isModelLayer(layerDef)) layerObject = this.modelSystem.buildLayer(layerDef);
2594
2966
  else {
2595
2967
  layerObject = new Group();
2596
2968
  layerDef.children.map((layer) => this.buildLayer(layer, parentPrefix + `${layerDef.name}:`)).forEach((g) => layerObject.add(g));
@@ -2610,11 +2982,8 @@ var LayerSystem = class {
2610
2982
  setRenderOrder(object, layer) {
2611
2983
  const renderOrder = this.renderOrderMap.get(layer);
2612
2984
  if (renderOrder === void 0) return;
2613
- if (object.isGroup) {
2614
- object.children.forEach((child) => this.setRenderOrder(child, layer));
2615
- return;
2616
- }
2617
2985
  object.renderOrder = renderOrder;
2986
+ if (object.isGroup) object.children.forEach((child) => this.setRenderOrder(child, layer));
2618
2987
  }
2619
2988
  createBackgroundLayer(color) {
2620
2989
  const backgroundMesh = new Mesh(new PlaneGeometry(2, 2), this.materialSystem.createBackgroundMaterial(color));
@@ -2670,219 +3039,154 @@ var LayerSystem = class {
2670
3039
  //#region src/geometry/lights.ts
2671
3040
  /** System for managing directional lights in 3D scenes. */
2672
3041
  var LightsSystem = class {
2673
- color = 16777215;
2674
- intensity = .5;
3042
+ skyColor = 16777215;
3043
+ groundColor = 0;
3044
+ intensity = Math.PI;
2675
3045
  mapSceneToLight = /* @__PURE__ */ new Map();
3046
+ pmremGenerator;
3047
+ /**
3048
+ * @param ctx {@link RendererContext} instance
3049
+ */
3050
+ constructor(ctx) {
3051
+ this.ctx = ctx;
3052
+ this.pmremGenerator = new PMREMGenerator(this.ctx.three);
3053
+ }
2676
3054
  /**
2677
3055
  * Initializes a directional light for the given scene.
2678
3056
  * @param scene {@link Scene} to add the light to
2679
3057
  */
2680
3058
  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);
3059
+ const hemisphereLightIntensity = this.intensity * .25;
3060
+ const hemisphereLight = new HemisphereLight(this.skyColor, this.groundColor, hemisphereLightIntensity);
3061
+ const matrixWorldInverse = new Matrix4().copy(scene.matrixWorld).invert();
3062
+ const position = new Vector3(0, 0, -1).applyMatrix4(matrixWorldInverse);
3063
+ hemisphereLight.position.copy(position);
3064
+ scene.add(hemisphereLight);
3065
+ const directionalLightIntensity = this.intensity * .5;
3066
+ const directionalLight = new DirectionalLight(this.skyColor, directionalLightIntensity);
2684
3067
  scene.add(directionalLight);
2685
3068
  scene.add(directionalLight.target);
2686
3069
  this.mapSceneToLight.set(scene, directionalLight);
3070
+ scene.environment = this.pmremGenerator.fromScene(new ColorEnvironment(), .04).texture;
3071
+ scene.environmentIntensity = .25;
2687
3072
  }
2688
3073
  /**
2689
3074
  * Updates the light direction to match the camera's viewing direction.
2690
- * @param scene {@link Scene} containing the light
3075
+ * @param sceneState {@link SceneState} containing the light
2691
3076
  * @param camera {@link Camera} to derive the light direction from
2692
3077
  */
2693
- updateLights(scene, camera) {
2694
- const light = this.mapSceneToLight.get(scene);
3078
+ updateLights(sceneState, camera) {
3079
+ const light = this.mapSceneToLight.get(sceneState.scene);
2695
3080
  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();
3081
+ camera.getWorldDirection(light.position).transformDirection(sceneState.worldMatrixInverse).negate();
2702
3082
  }
2703
3083
  };
2704
3084
  //#endregion
2705
- //#region src/util/asserts.ts
2706
- /** Logger instance for assert warnings */
2707
- var logger$7 = createLogger("");
3085
+ //#region src/loaders/glb.ts
3086
+ var logger$8 = createLogger("glb");
2708
3087
  /**
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
3088
+ * Loads a GLB file and returns a Three.js model.
3089
+ * @param models - The models to load.
3090
+ * @returns An object containing parsed Three.js objects.
2713
3091
  */
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;
3092
+ async function loadGLB(models) {
3093
+ const loader = new GLTFLoader();
3094
+ const dracoLoader = new DRACOLoader();
3095
+ dracoLoader.setDecoderPath("https://www.gstatic.com/draco/versioned/decoders/1.5.7/");
3096
+ const root = new Group();
3097
+ for (const model of models) {
3098
+ if (model.isMeshopt) loader.setMeshoptDecoder(MeshoptDecoder);
3099
+ if (model.isDraco) loader.setDRACOLoader(dracoLoader);
3100
+ const loadedModel = await loader.loadAsync(model.url);
3101
+ logger$8.info("model loaded", loadedModel);
3102
+ root.add(...loadedModel.scene.children);
3103
+ loader.setMeshoptDecoder(null);
3104
+ loader.setDRACOLoader(null);
3105
+ }
3106
+ const bounds = new Box3();
3107
+ root.traverse((node) => {
3108
+ if (node.userData && Object.keys(node.userData).filter((key) => key !== "name").length > 0) logger$8.info(`node ${node.name} userData:`, node.userData);
3109
+ if (node instanceof Mesh && node.geometry) {
3110
+ const materialJSON = node.material.toJSON();
3111
+ logger$8.info(`node ${node.name} material %O:`, materialJSON);
3112
+ const geometry = node.geometry;
3113
+ if (!geometry.boundingBox) geometry.computeBoundingBox();
3114
+ const box = geometry.boundingBox.clone();
3115
+ box.applyMatrix4(node.matrixWorld);
3116
+ bounds.union(box);
3117
+ }
3118
+ });
3119
+ logger$8.info("model bounds", bounds);
3120
+ root.rotation.x = -Math.PI / 2;
3121
+ root.scale.set(1, -1, 1);
3122
+ return {
3123
+ sceneDef: {
3124
+ rootLayer: {
3125
+ children: [],
3126
+ name: "root"
3127
+ },
3128
+ viewbox: new Rect([bounds.min.x, bounds.min.z], [bounds.max.x, bounds.max.z])
3129
+ },
3130
+ root
3131
+ };
2733
3132
  }
2734
3133
  /**
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
3134
+ * Loads a GLB file, converts it to a Three.js {@link Scene}, exports the scene as a GLTF
3135
+ * (JSON) file, and triggers a browser download of the result.
3136
+ * @param url - The URL of the GLB file.
3137
+ * @param isMeshopt - Whether the model file uses meshopt compression.
2739
3138
  */
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;
3139
+ async function glb2gltf(url, isMeshopt) {
3140
+ const loader = new GLTFLoader();
3141
+ if (isMeshopt) loader.setMeshoptDecoder(MeshoptDecoder);
3142
+ const model = await loader.loadAsync(url);
3143
+ logger$8.info("glb loaded for export", model);
3144
+ const scene = new Scene();
3145
+ for (const child of model.scene.children) scene.add(child.clone());
3146
+ const gltf = await new GLTFExporter().parseAsync(scene);
3147
+ logger$8.info("scene exported as gltf");
3148
+ const filename = `${deriveBasename(url)}.gltf`;
3149
+ saveBlob(new Blob([JSON.stringify(gltf, null, 2)], { type: "application/json" }), filename);
2746
3150
  }
2747
3151
  /**
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
3152
+ * Loads a GLTF/GLB file and splits its top-level children into two GLB files based on
3153
+ * whether each child's name starts with `BasisTransform`. The matching children are saved
3154
+ * with a `-decor` suffix; the rest are saved with a `-base` suffix. Both files are
3155
+ * triggered as browser downloads. The output is intentionally uncompressed — run a
3156
+ * meshopt/quantize pass offline (e.g. via `gltf-transform`) afterwards.
3157
+ * @param url - The URL of the GLTF/GLB file.
2752
3158
  */
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;
3159
+ async function splitGltf(url) {
3160
+ const model = await new GLTFLoader().loadAsync(url);
3161
+ logger$8.info("gltf loaded for split", model);
3162
+ const baseScene = new Scene();
3163
+ const decorScene = new Scene();
3164
+ for (const child of model.scene.children) (child.name.startsWith("BasisTransform") ? decorScene : baseScene).add(child.clone());
3165
+ logger$8.info("split: base=%d decor=%d", baseScene.children.length, decorScene.children.length);
3166
+ const exporter = new GLTFExporter();
3167
+ const baseGlb = await exporter.parseAsync(baseScene, { binary: true });
3168
+ const decorGlb = await exporter.parseAsync(decorScene, { binary: true });
3169
+ const stem = deriveBasename(url);
3170
+ saveBlob(new Blob([baseGlb], { type: "model/gltf-binary" }), `${stem}-base.glb`);
3171
+ saveBlob(new Blob([decorGlb], { type: "model/gltf-binary" }), `${stem}-decor.glb`);
2759
3172
  }
2760
- /**
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
2766
- */
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;
3173
+ function deriveBasename(url) {
3174
+ return (url.split("?")[0].split("#")[0].split("/").pop() ?? "model").replace(/\.(glb|gltf)$/i, "");
2773
3175
  }
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;
2797
- }
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
- });
3176
+ function saveBlob(blob, filename) {
3177
+ const objectUrl = URL.createObjectURL(blob);
3178
+ const link = document.createElement("a");
3179
+ link.href = objectUrl;
3180
+ link.download = filename;
3181
+ link.style.display = "none";
3182
+ document.body.appendChild(link);
3183
+ link.click();
3184
+ document.body.removeChild(link);
3185
+ URL.revokeObjectURL(objectUrl);
2882
3186
  }
2883
3187
  //#endregion
2884
3188
  //#region src/ui/controls.ts
2885
- var logger$6 = createLogger("controls");
3189
+ var logger$7 = createLogger("controls");
2886
3190
  /** Navigation system. Manages camera controls and zooming. */
2887
3191
  var ControlsSystem = class {
2888
3192
  controller;
@@ -2926,7 +3230,7 @@ var ControlsSystem = class {
2926
3230
  const worldRect = new Rect(this.coordinatesSystem.modelToWorld(rect.min), this.coordinatesSystem.modelToWorld(rect.max));
2927
3231
  const sourceRect = Polygon.fromRect(worldRect).rotate(bearingAngle).bounds;
2928
3232
  if (sourceRect.size.x <= 0 || sourceRect.size.y <= 0) {
2929
- logger$6.warn("zoomTo: sourceRect size is 0");
3233
+ logger$7.warn("zoomTo: sourceRect size is 0");
2930
3234
  return;
2931
3235
  }
2932
3236
  if (paddingPercent) targetRect.expand({
@@ -3225,7 +3529,7 @@ function asEventAPI(system) {
3225
3529
  }
3226
3530
  //#endregion
3227
3531
  //#region src/space/coordinates.ts
3228
- var logger$5 = createLogger("coordinates");
3532
+ var logger$6 = createLogger("coordinates");
3229
3533
  /** System responsible for converting coordinates between different spaces. */
3230
3534
  var CoordinatesSystem = class {
3231
3535
  /** Used as a scratch vector for coordinate space conversions */
@@ -3298,7 +3602,7 @@ var CoordinatesSystem = class {
3298
3602
  canvasToNDC(point, out = new Vector2()) {
3299
3603
  const [w, h] = this.ctx.getDrawingBufferSizePx();
3300
3604
  if (w <= 0 || h <= 0) {
3301
- logger$5.warn("canvasToNDC: renderer size is 0");
3605
+ logger$6.warn("canvasToNDC: renderer size is 0");
3302
3606
  return out.set(0, 0);
3303
3607
  }
3304
3608
  const dpr = this.ctx.three.getPixelRatio();
@@ -3466,7 +3770,7 @@ function createNoopHandler() {
3466
3770
  };
3467
3771
  }
3468
3772
  //#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
3773
+ //#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
3774
  /*!
3471
3775
  * camera-controls
3472
3776
  * https://github.com/yomotsu/camera-controls
@@ -5838,7 +6142,7 @@ var subsetOfTHREE = {
5838
6142
  Plane
5839
6143
  };
5840
6144
  CameraControls.install({ THREE: subsetOfTHREE });
5841
- var logger$4 = createLogger("cameraController");
6145
+ var logger$5 = createLogger("cameraController");
5842
6146
  /**
5843
6147
  * Minimum z-axis separation between overlapping surfaces, in SVG/model units.
5844
6148
  *
@@ -5912,7 +6216,7 @@ var CameraController = class CameraController extends CameraControls {
5912
6216
  radius
5913
6217
  ].map((value) => +value.toFixed(2));
5914
6218
  const clippingPlanes = [this.camera.near, this.camera.far];
5915
- logger$4.debug("camera update %O", {
6219
+ logger$5.debug("camera update %O", {
5916
6220
  position,
5917
6221
  target,
5918
6222
  spherical,
@@ -6567,6 +6871,100 @@ var noopHandlers = {
6567
6871
  pitch: createNoopHandler()
6568
6872
  };
6569
6873
  //#endregion
6874
+ //#region src/util/asserts.ts
6875
+ /** Logger instance for assert warnings */
6876
+ var logger$4 = createLogger("");
6877
+ /**
6878
+ * Asserts the renderer has not been disposed.
6879
+ * @param renderer - Renderer instance to check
6880
+ * @param funcName - Name of the calling function for error messages
6881
+ * @returns false if disposed, true otherwise
6882
+ */
6883
+ function assertNotDisposed(renderer, funcName) {
6884
+ if (renderer.isDisposed) {
6885
+ logger$4.warn(`[${funcName}]: Renderer is used after being disposed. Please create a new instance.`);
6886
+ return false;
6887
+ }
6888
+ return true;
6889
+ }
6890
+ /**
6891
+ * Asserts the renderer has been initialized.
6892
+ * @param renderer - Renderer instance to check
6893
+ * @param funcName - Name of the calling function for error messages
6894
+ * @returns false if not initialized, true otherwise
6895
+ */
6896
+ function assertInitialized(renderer, funcName) {
6897
+ if (!renderer.isInitialized) {
6898
+ logger$4.warn(`${funcName}: Renderer is not initialized. Please call init() before using it.`);
6899
+ return false;
6900
+ }
6901
+ return true;
6902
+ }
6903
+ /**
6904
+ * Asserts the renderer has not been initialized yet.
6905
+ * @param renderer - Renderer instance to check
6906
+ * @param funcName - Name of the calling function for error messages
6907
+ * @returns false if already initialized, true otherwise
6908
+ */
6909
+ function assertNotInitialized(renderer, funcName) {
6910
+ if (renderer.isInitialized) {
6911
+ logger$4.warn(`${funcName}: Renderer is already initialized. Please call init() only once.`);
6912
+ return false;
6913
+ }
6914
+ return true;
6915
+ }
6916
+ /**
6917
+ * Asserts the renderer is not in external mode.
6918
+ * @param renderer - Renderer instance to check
6919
+ * @param funcName - Name of the calling function for error messages
6920
+ * @returns false if in external mode, true otherwise
6921
+ */
6922
+ function assertNotExternalMode(renderer, funcName) {
6923
+ if (renderer.isExternalMode) {
6924
+ logger$4.warn(`${funcName}: This operation is not supported in external mode.`);
6925
+ return false;
6926
+ }
6927
+ return true;
6928
+ }
6929
+ /**
6930
+ * Asserts that number array is a valid 4x4 matrix.
6931
+ * @param matrix number array to check
6932
+ * @param name name of the matrix for error messages
6933
+ * @param log optional logger instance to use for warning messages (defaults to root logger)
6934
+ * @returns true if matrix is valid, false otherwise
6935
+ */
6936
+ function assertValidMatrix(matrix, name, log = logger$4) {
6937
+ if (matrix.length !== 16) {
6938
+ log.warn(`${name}: Matrix must be 16 elements long`);
6939
+ return false;
6940
+ }
6941
+ return true;
6942
+ }
6943
+ /**
6944
+ * Wraps an API object so that every method is guarded by a precondition check.
6945
+ * When the guard (or a per-method override) fails, the corresponding fallback
6946
+ * method is called instead of the real implementation.
6947
+ * Non-function properties are passed through from `impl` (if present) or `fallback`.
6948
+ * @param impl - Real implementation, or null when the backing system doesn't exist
6949
+ * @param fallback - Noop / default implementation used when guard fails or impl is null
6950
+ * @param guard - Default guard applied to every method
6951
+ * @param guards - Optional per-method guard overrides (takes precedence over `guard`)
6952
+ * @returns A new object conforming to T with every method wrapped
6953
+ */
6954
+ function guardAPI(impl, fallback, guard, guards) {
6955
+ const guarded = {};
6956
+ for (const [key, fallbackVal] of Object.entries(fallback)) if (typeof fallbackVal === "function") {
6957
+ const implFn = impl?.[key];
6958
+ const methodGuard = guards?.[key] ?? guard;
6959
+ const fallbackFn = fallbackVal;
6960
+ guarded[key] = (...args) => {
6961
+ if (methodGuard(key) && implFn) return implFn(...args);
6962
+ return fallbackFn(...args);
6963
+ };
6964
+ } else guarded[key] = impl?.[key] ?? fallbackVal;
6965
+ return guarded;
6966
+ }
6967
+ //#endregion
6570
6968
  //#region src/core/updates.ts
6571
6969
  var logger$3 = createLogger("updates");
6572
6970
  /**
@@ -6752,6 +7150,85 @@ var InternalCameraSystem = class {
6752
7150
  }
6753
7151
  };
6754
7152
  //#endregion
7153
+ //#region src/space/external.ts
7154
+ var externalLogger = createLogger("external");
7155
+ /**
7156
+ * Camera system in external mode. Exposes dummy camera instance as a wrapper for external camera transforms,
7157
+ * and estimates zoom factor ndc -> world conversion.
7158
+ */
7159
+ var ExternalCameraSystem = class {
7160
+ /** External camera instance */
7161
+ camera = new Camera();
7162
+ ndcLeft = new Vector2(-1, 0);
7163
+ ndcRight = new Vector2(1, 0);
7164
+ intersectionPointLeft = new Vector3();
7165
+ intersectionPointRight = new Vector3();
7166
+ prevZoomFactor;
7167
+ mainMatrix4 = new Matrix4();
7168
+ mainMatrixInverse = new Matrix4();
7169
+ nearPoint = new Vector3();
7170
+ farPoint = new Vector3();
7171
+ viewDir = new Vector3();
7172
+ target = new Vector3();
7173
+ up = new Vector3();
7174
+ cameraPos = new Vector3();
7175
+ linearSystem = new Matrix3();
7176
+ rhs = new Vector3();
7177
+ /**
7178
+ * @param ctx {@link RendererContext} instance
7179
+ * @param pickingSystem {@link PickingSystem} instance
7180
+ */
7181
+ constructor(ctx, pickingSystem) {
7182
+ this.ctx = ctx;
7183
+ this.pickingSystem = pickingSystem;
7184
+ this.camera.matrixAutoUpdate = false;
7185
+ this.camera.matrixWorldAutoUpdate = false;
7186
+ }
7187
+ /** Current zoom factor estimation */
7188
+ get zoomFactor() {
7189
+ const estimatedZoomFactor = this.estimateZoomFactor();
7190
+ const newZoomFactor = estimatedZoomFactor ?? this.prevZoomFactor ?? 1;
7191
+ if (estimatedZoomFactor) this.prevZoomFactor = estimatedZoomFactor;
7192
+ return newZoomFactor;
7193
+ }
7194
+ /**
7195
+ * Decompose the combined world-to-clip-space matrix into a rigid view matrix and a projection
7196
+ * matrix, then publish all four matrices (and their inverses) on the underlying camera.
7197
+ * @param mainMatrix Combined matrix mapping world space directly to clip space.
7198
+ */
7199
+ setCameraProjection(mainMatrix) {
7200
+ if (!assertValidMatrix(mainMatrix, "setCameraProjection", externalLogger)) return;
7201
+ const M = this.mainMatrix4.fromArray(mainMatrix);
7202
+ const e = M.elements;
7203
+ this.mainMatrixInverse.copy(M).invert();
7204
+ this.linearSystem.set(e[0], e[4], e[8], e[1], e[5], e[9], e[3], e[7], e[11]);
7205
+ this.rhs.set(-e[12], -e[13], -e[15]);
7206
+ this.cameraPos.copy(this.rhs).applyMatrix3(this.linearSystem.invert());
7207
+ this.nearPoint.set(0, 0, 1).applyMatrix4(this.mainMatrixInverse);
7208
+ this.farPoint.set(0, 0, -1).applyMatrix4(this.mainMatrixInverse);
7209
+ this.viewDir.copy(this.farPoint).sub(this.nearPoint).normalize();
7210
+ this.up.set(0, 0, 1);
7211
+ if (Math.abs(this.up.dot(this.viewDir)) > .999) this.up.set(0, 1, 0);
7212
+ this.target.copy(this.cameraPos).add(this.viewDir);
7213
+ const camera = this.camera;
7214
+ camera.matrixWorld.lookAt(this.cameraPos, this.target, this.up);
7215
+ camera.matrixWorld.setPosition(this.cameraPos);
7216
+ camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
7217
+ camera.projectionMatrix.multiplyMatrices(M, camera.matrixWorld);
7218
+ camera.projectionMatrixInverse.copy(camera.projectionMatrix).invert();
7219
+ }
7220
+ estimateZoomFactor() {
7221
+ const [bufferW, bufferH] = this.ctx.getDrawingBufferSizePx();
7222
+ if (bufferW <= 0 || bufferH <= 0) return;
7223
+ const worldPoint1 = this.pickingSystem.intersectPlane(this.ndcLeft, this.camera, this.intersectionPointLeft);
7224
+ const worldPoint2 = this.pickingSystem.intersectPlane(this.ndcRight, this.camera, this.intersectionPointRight);
7225
+ if (!worldPoint1 || !worldPoint2) return;
7226
+ const worldLength = worldPoint2.sub(worldPoint1).length();
7227
+ if (worldLength === 0) return;
7228
+ return bufferW / worldLength;
7229
+ }
7230
+ };
7231
+ //#endregion
6755
7232
  //#region src/space/picking.ts
6756
7233
  /**
6757
7234
  * Picking subsystem.
@@ -6826,7 +7303,8 @@ var SceneSystem = class {
6826
7303
  * @returns Scale factor (model space to world space)
6827
7304
  */
6828
7305
  scaleFactor(sceneId) {
6829
- return this.getSceneStateById(sceneId).worldMatrix.elements[0];
7306
+ const sceneState = this.getSceneStateById(sceneId);
7307
+ return Math.abs(sceneState.worldMatrix.elements[0]);
6830
7308
  }
6831
7309
  /**
6832
7310
  * Register a new scene from it's definition
@@ -7194,7 +7672,7 @@ var Renderer = class {
7194
7672
  this.renderer.autoClear = !this.isExternalMode;
7195
7673
  this.ctx = this.createRendererContext();
7196
7674
  this.eventSystem = new EventSystem();
7197
- this.lightsSystem = new LightsSystem();
7675
+ this.lightsSystem = new LightsSystem(this.ctx);
7198
7676
  this.layerSystem = new LayerSystem(this.ctx);
7199
7677
  this.viewportSystem = new ViewportSystem(this.ctx, this.eventSystem);
7200
7678
  this.updatesSystem = new UpdatesSystem(this.ctx, this.layerSystem);
@@ -7293,6 +7771,18 @@ var Renderer = class {
7293
7771
  return this.disposed;
7294
7772
  }
7295
7773
  /**
7774
+ * Loads a GLB file and returns a Three.js model.
7775
+ * @param models - The models to load.
7776
+ */
7777
+ async loadModel(models) {
7778
+ this.init();
7779
+ const { root, sceneDef } = await loadGLB(models);
7780
+ const scene = this.viewportSystem.initScene(sceneDef);
7781
+ scene.background = new Color(15461355);
7782
+ scene.add(root);
7783
+ this.lightsSystem.initLights(scene);
7784
+ }
7785
+ /**
7296
7786
  * Initializes viewport and scene with the given scene definition.
7297
7787
  * Should be called once on startup. Repeated calls will produce console warnings.
7298
7788
  * @param sceneDef {@link SceneDef} to render
@@ -7361,6 +7851,7 @@ var Renderer = class {
7361
7851
  } else if (sceneState.scene.children.length === 0) {
7362
7852
  const root = this.layerSystem.buildScene(sceneState.sceneDef);
7363
7853
  scene.add(root);
7854
+ this.lightsSystem.initLights(scene);
7364
7855
  }
7365
7856
  }
7366
7857
  const justLoaded = loadedChanged && sceneState.loaded;
@@ -7368,8 +7859,7 @@ var Renderer = class {
7368
7859
  this.viewportSystem.updatePtScale(id);
7369
7860
  const hasDefsUpdated = this.updatesSystem.processPendingUpdates(frustum, 3);
7370
7861
  const forceRedraw = hasControlsUpdated || hasDefsUpdated || justLoaded || this.isExternalMode || this.ui;
7371
- if (this.isExternalMode) patchMatricesInExternalMode(scene);
7372
- this.lightsSystem.updateLights(scene, camera);
7862
+ this.lightsSystem.updateLights(sceneState, camera);
7373
7863
  if (this.needsRedraw || forceRedraw) this.renderer.render(scene, camera);
7374
7864
  if (hasDefsUpdated || this.needsRedraw) this.viewportSystem.invalidateSceneBounds(sceneState);
7375
7865
  }
@@ -7475,4 +7965,4 @@ var Renderer = class {
7475
7965
  }
7476
7966
  };
7477
7967
  //#endregion
7478
- export { Polygon, Rect, Renderer, isImageDef, isImageLayer, isLayerDef, isLayerLayer, isLineDef, isLineLayer, isShapeDef, isShapeLayer, isTextDef, isTextLayer };
7968
+ export { Polygon, Rect, Renderer, glb2gltf, isImageDef, isImageLayer, isLayerDef, isLayerLayer, isLineDef, isLineLayer, isModelDef, isModelLayer, isShapeDef, isShapeLayer, isTextDef, isTextLayer, splitGltf };