@expofp/renderer 3.0.1 → 3.1.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.d.ts CHANGED
@@ -274,8 +274,14 @@ export declare interface LayerDef extends SharedDef {
274
274
  interactive?: boolean;
275
275
  /** Collection of layer's children. Mixing different types of children is not supported. */
276
276
  children: RenderableDefCollection;
277
- /** Layer's rendering mode, 2d or 3d. Optional, 2d by default. */
278
- mode?: "2d" | "3d";
277
+ /**
278
+ * Layer's rendering mode. Propagates to children: child layers inherit the parent's mode
279
+ * unless they specify their own. Defaults to `"2D"` at the root.
280
+ * Resolved once during scene build and should not be changed afterwards.
281
+ * - `"2D"`: painter's algorithm (AlwaysDepth). Layer order determines overlap.
282
+ * - `"3D"`: depth testing (LessDepth). Closer geometry occludes farther geometry.
283
+ */
284
+ readonly mode?: "2D" | "3D";
279
285
  }
280
286
 
281
287
  /** Line definition */
@@ -338,7 +344,11 @@ export declare class Polygon {
338
344
  get vertices(): readonly Vector3Like[];
339
345
  /** Array of polygon indices. Each index is a triplet of vertex indices forming a triangle. */
340
346
  get indices(): readonly Index[];
341
- /** Bounding rectangle of the polygon. */
347
+ /**
348
+ * Bounding rectangle of the polygon.
349
+ * This is a 2D axis-aligned bounding box computed from the X/Y projection of vertices.
350
+ * The Z extent of the polygon is not tracked.
351
+ */
342
352
  get bounds(): Rect;
343
353
  /**
344
354
  * Converts a {@link Rect} to a {@link Polygon}.
@@ -367,7 +377,8 @@ export declare class Polygon {
367
377
  rotate(rotation: number, center?: Vector2Like): Polygon;
368
378
  /**
369
379
  * Scales the polygon around the given origin.
370
- * @param scaleFactor Can be a single number or a 2D vector. If a single number is provided, both horizontal and vertical axes will be scaled by the same factor.
380
+ * @param scaleFactor Can be a single number or a 2D vector. If a single number is provided, both horizontal and
381
+ * vertical axes will be scaled by the same factor. Z is unchanged in both cases.
371
382
  * @param origin Origin of the scaling. If omitted, defaults to the bounding rectangle center.
372
383
  * @returns this {@link Polygon} instance
373
384
  */
@@ -387,6 +398,8 @@ export declare interface PtScaleEventData {
387
398
  export declare class Rect {
388
399
  /** Optional rotation of the rectangle. In radians, around center. Positive values rotate clockwise. */
389
400
  rotation: number;
401
+ /** Optional elevation of the rectangle. Positive values raise geometry toward the viewer. Defaults to 0. */
402
+ elevation: number;
390
403
  private _min;
391
404
  private _max;
392
405
  private _center;
@@ -395,8 +408,9 @@ export declare class Rect {
395
408
  * @param min Top left corner of the rectangle.
396
409
  * @param max Bottom right corner of the rectangle.
397
410
  * @param rotation Optional rotation of the rectangle. In radians, around center. Positive values rotate clockwise.
411
+ * @param elevation Optional elevation. Positive values raise geometry toward the viewer. Defaults to 0.
398
412
  */
399
- constructor(min: IVector2, max: IVector2, rotation?: number);
413
+ constructor(min: IVector2, max: IVector2, rotation?: number, elevation?: number);
400
414
  /** Top left corner of the rectangle. */
401
415
  get min(): Vector2Like;
402
416
  /** Set top left corner of the rectangle. */
@@ -417,9 +431,10 @@ export declare class Rect {
417
431
  * Creates a rectangle from an SVG rectangle element.
418
432
  * @param rect {@link SVGRectElement} or {@link SVGImageElement}
419
433
  * @param rotation Optional rotation of the rectangle. In radians, around center. Positive values rotate clockwise.
434
+ * @param elevation Optional elevation. Positive values raise geometry toward the viewer. Defaults to 0.
420
435
  * @returns new {@link Rect} instance
421
436
  */
422
- static fromSvg(rect: SVGRectElement | SVGImageElement, rotation?: number): Rect;
437
+ static fromSvg(rect: SVGRectElement | SVGImageElement, rotation?: number, elevation?: number): Rect;
423
438
  /**
424
439
  * Moves the rectangle by the given offset.
425
440
  * @param offset Offset to move the rectangle by.
@@ -455,6 +470,7 @@ export declare class Renderer {
455
470
  private memoryInfoExtension;
456
471
  private memoryInfo?;
457
472
  private eventSystem;
473
+ private lightsSystem;
458
474
  private layerSystem;
459
475
  private viewportSystem;
460
476
  private updatesSystem;
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
  var _a;
5
- import { DataTexture, FloatType, UnsignedIntType, IntType, RGBAFormat, RGBAIntegerFormat, RGFormat, RGIntegerFormat, RedFormat, RedIntegerFormat, BatchedMesh as BatchedMesh$1, BufferAttribute, StreamDrawUsage, Color, Matrix4, Vector3, Frustum, Sphere, Box3, DynamicDrawUsage, Vector4, AlwaysDepth, DoubleSide, MeshBasicMaterial, Texture, Group, PlaneGeometry, SRGBColorSpace, Vector2, Quaternion, Mesh, LessEqualDepth, BufferGeometry, LinearSRGBColorSpace, Camera, Spherical, MathUtils, Plane, Raycaster, PerspectiveCamera, Scene, Clock, WebGLRenderer } from "three";
5
+ import { DataTexture, FloatType, UnsignedIntType, IntType, RGBAFormat, RGBAIntegerFormat, RGFormat, RGIntegerFormat, RedFormat, RedIntegerFormat, BatchedMesh as BatchedMesh$1, BufferAttribute, StreamDrawUsage, Color, Matrix4, Vector3, Frustum, Sphere, Box3, DynamicDrawUsage, Vector4, AlwaysDepth, DoubleSide, MeshPhongMaterial, MeshBasicMaterial, LessEqualDepth, Texture, Group, PlaneGeometry, SRGBColorSpace, Vector2, Quaternion, BufferGeometry, LinearSRGBColorSpace, Mesh, AmbientLight, DirectionalLight, Camera, Spherical, MathUtils, Plane, Raycaster, PerspectiveCamera, Scene, Clock, WebGLRenderer } from "three";
6
6
  import { traverseAncestorsGenerator } from "three/examples/jsm/utils/SceneUtils.js";
7
7
  import createLog from "debug";
8
8
  import { BatchedText as BatchedText$1, Text as Text$1 } from "troika-three-text";
@@ -867,8 +867,6 @@ class Text extends Text$1 {
867
867
  if (!this._batchParent) this.geometry.applyClipRect(uniforms.uTroikaClipRect.value);
868
868
  }
869
869
  uniforms.uTroikaSDFDebug.value = !!this.debugSDF;
870
- material.polygonOffset = !!this.depthOffset;
871
- material.polygonOffsetFactor = material.polygonOffsetUnits = this.depthOffset || 0;
872
870
  const color = isOutline ? this.outlineColor || 0 : this.color;
873
871
  if (color == null) {
874
872
  delete material.color;
@@ -1050,6 +1048,7 @@ const dimColorFrag = (
1050
1048
  return vec4(m * col.a, col.a);
1051
1049
  }`
1052
1050
  );
1051
+ const POLYGON_OFFSET_MULTIPLIER = 12;
1053
1052
  const sharedParameters = {
1054
1053
  side: DoubleSide,
1055
1054
  transparent: true,
@@ -1094,47 +1093,48 @@ class MaterialSystem {
1094
1093
  }
1095
1094
  };
1096
1095
  addDimToMaterial(material);
1096
+ this.addPolygonOffset(material);
1097
1097
  return material;
1098
1098
  }
1099
1099
  /**
1100
1100
  * Creates a color material.
1101
- * @param partialParams {@link MaterialColorParams}
1101
+ * @param params {@link MaterialColorParams}
1102
1102
  * @returns MeshBasicMaterial instance
1103
1103
  */
1104
- createColorMaterial(partialParams = {}) {
1105
- const params = {
1106
- color: partialParams.color ?? 16777215,
1107
- opacity: partialParams.opacity ?? 1
1108
- };
1109
- const material = new MeshBasicMaterial({
1104
+ createColorMaterial(params = {}) {
1105
+ const materialConstructor = params.lightingEnable ? MeshPhongMaterial : MeshBasicMaterial;
1106
+ const material = new materialConstructor({
1110
1107
  ...sharedParameters,
1111
- color: params.color,
1112
- opacity: params.opacity
1108
+ color: params.color ?? 16777215,
1109
+ opacity: params.opacity ?? 1
1113
1110
  });
1111
+ if (params.depthEnable) material.depthFunc = LessEqualDepth;
1114
1112
  addDimToMaterial(material);
1113
+ this.addPolygonOffset(material);
1115
1114
  return material;
1116
1115
  }
1117
1116
  /**
1118
1117
  * Creates a texture material.
1119
- * @param map {@link Texture}
1120
- * @param uvOffset whether to enable uv offset with per instance uniforms (for texture atlases)
1118
+ * @param params {@link MaterialTextureParams}
1121
1119
  * @returns MeshBasicMaterial instance
1122
1120
  */
1123
- createTextureMaterial(map, uvOffset = false) {
1124
- const material = new MeshBasicMaterial({ ...sharedParameters, map });
1125
- if (uvOffset) {
1121
+ createTextureMaterial(params) {
1122
+ const material = new MeshBasicMaterial({ ...sharedParameters, map: params.map });
1123
+ if (params.depthEnable) material.depthFunc = LessEqualDepth;
1124
+ if (params.uvOffset) {
1126
1125
  material.onBeforeCompile = (shader) => {
1127
1126
  shader.vertexShader = shader.vertexShader.replace(
1128
1127
  "#include <uv_vertex>",
1129
1128
  /*glsl*/
1130
1129
  `
1131
1130
  #include <uv_vertex>
1132
- vMapUv = uv * uvOffset.zw + uvOffset.xy;
1131
+ vMapUv = uv * uvOffset.zw + uvOffset.xy;
1133
1132
  `
1134
1133
  );
1135
1134
  };
1136
1135
  }
1137
1136
  addDimToMaterial(material);
1137
+ this.addPolygonOffset(material);
1138
1138
  return material;
1139
1139
  }
1140
1140
  /**
@@ -1158,6 +1158,40 @@ class MaterialSystem {
1158
1158
  }
1159
1159
  return this.backgroundMaterial;
1160
1160
  }
1161
+ /**
1162
+ * Patches a material's onBeforeRender to dynamically set polygonOffset based on the
1163
+ * object's renderOrder. Later layers (higher renderOrder) get a larger negative offset,
1164
+ * giving them a slight depth buffer advantage over earlier layers at the same elevation.
1165
+ * This prevents z-fighting between coplanar 3D content across different layers.
1166
+ *
1167
+ * The units multiplier is derived from {@link CameraControls.maxPolarAngle}:
1168
+ *
1169
+ * multiplier = ⌈tan(maxPolarAngle)⌉
1170
+ *
1171
+ * At steep camera pitch, coplanar surfaces from different draw calls (different
1172
+ * BatchedMesh instances) compute slightly different depth values at the same pixel
1173
+ * due to floating-point interpolation differences in the vertex shader. These errors
1174
+ * are proportional to the depth gradient across the surface, which for a horizontal
1175
+ * plane at pitch angle θ is proportional to tan(θ). The multiplier ensures the
1176
+ * per-layer polygon offset exceeds these errors at the configured maximum pitch.
1177
+ *
1178
+ * For elevated content the effective viewing angle can exceed maxPolarAngle, but this
1179
+ * is self-correcting: the screen-space height of the surface shrinks as cos(angle),
1180
+ * so the total visible z-fighting (pixels × severity ∝ cos × tan = sin) is bounded
1181
+ * and becomes subpixel as the effective angle approaches 90°.
1182
+ *
1183
+ * With maxPolarAngle = 85°: ⌈tan(85°)⌉ = ⌈11.43⌉ = 12.
1184
+ * @param material Material to patch
1185
+ */
1186
+ addPolygonOffset(material) {
1187
+ const onBeforeRender = material.onBeforeRender.bind(material);
1188
+ material.onBeforeRender = (renderer, scene, camera, geometry, object, group) => {
1189
+ onBeforeRender(renderer, scene, camera, geometry, object, group);
1190
+ material.polygonOffset = true;
1191
+ material.polygonOffsetFactor = 0;
1192
+ material.polygonOffsetUnits = -object.renderOrder * POLYGON_OFFSET_MULTIPLIER;
1193
+ };
1194
+ }
1161
1195
  }
1162
1196
  function isShapeDef(def) {
1163
1197
  return def.shape !== void 0;
@@ -1189,36 +1223,14 @@ function isLineLayer(layer) {
1189
1223
  function isLayerLayer(layer) {
1190
1224
  return layer.children[0] && isLayerDef(layer.children[0]);
1191
1225
  }
1192
- function groupBy(list, keyGetter) {
1193
- const map = /* @__PURE__ */ new Map();
1194
- list.forEach((item) => {
1195
- const key = keyGetter(item);
1196
- const collection = map.get(key);
1197
- if (!collection) {
1198
- map.set(key, [item]);
1199
- } else {
1200
- collection.push(item);
1201
- }
1202
- });
1203
- return map;
1204
- }
1205
- function partition(list, pred) {
1206
- const truthy = [];
1207
- const falsy = [];
1208
- for (const item of list) {
1209
- if (pred(item)) {
1210
- truthy.push(item);
1211
- } else {
1212
- falsy.push(item);
1213
- }
1214
- }
1215
- return [truthy, falsy];
1216
- }
1217
1226
  const INTERACTIVE_LAYER = 1;
1218
- function setInteractive(object, isInteractive) {
1219
- if (isInteractive) object.layers.enable(INTERACTIVE_LAYER);
1227
+ function isInteractive(object) {
1228
+ return object.layers.isEnabled(INTERACTIVE_LAYER);
1229
+ }
1230
+ function setInteractive(object, isInteractive2) {
1231
+ if (isInteractive2) object.layers.enable(INTERACTIVE_LAYER);
1220
1232
  else object.layers.disable(INTERACTIVE_LAYER);
1221
- object.children.forEach((child) => setInteractive(child, isInteractive));
1233
+ object.children.forEach((child) => setInteractive(child, isInteractive2));
1222
1234
  }
1223
1235
  function isVisible(object) {
1224
1236
  if (!object.visible) return false;
@@ -1483,6 +1495,7 @@ class ImageSystem extends RenderableSystem {
1483
1495
  }
1484
1496
  buildLayer(layer) {
1485
1497
  var _a2;
1498
+ const is3D = layer.mode === "3D";
1486
1499
  const group = new Group();
1487
1500
  const images = layer.children;
1488
1501
  const bins = this.packImages(images);
@@ -1497,7 +1510,11 @@ class ImageSystem extends RenderableSystem {
1497
1510
  ).sort((a, b) => a.originalIndex - b.originalIndex);
1498
1511
  const instanceCount = rectsWithDef.length;
1499
1512
  const texture = createAtlas(bin);
1500
- const instanceMaterial = this.materialSystem.createTextureMaterial(texture, true);
1513
+ const instanceMaterial = this.materialSystem.createTextureMaterial({
1514
+ map: texture,
1515
+ uvOffset: true,
1516
+ depthEnable: is3D
1517
+ });
1501
1518
  const instanceGeometry = new PlaneGeometry();
1502
1519
  const vertexCount = instanceGeometry.attributes["position"].count;
1503
1520
  const indexCount = ((_a2 = instanceGeometry.index) == null ? void 0 : _a2.count) ?? 0;
@@ -1582,7 +1599,7 @@ class ImageSystem extends RenderableSystem {
1582
1599
  const bounds = imageDef.bounds;
1583
1600
  const origin2 = imageDef.origin ?? [0.5, 0.5];
1584
1601
  this.originTranslationMatrix.makeTranslation(0.5 - origin2[0], 0.5 - origin2[1], 0);
1585
- this.globalTranslationMatrix.makeTranslation(bounds.center.x, bounds.center.y, 0);
1602
+ this.globalTranslationMatrix.makeTranslation(bounds.center.x, bounds.center.y, bounds.elevation);
1586
1603
  this.scaleMatrix.makeScale(bounds.size.x, bounds.size.y, 1);
1587
1604
  this.rotationMatrix.makeRotationZ(bounds.rotation);
1588
1605
  const matrix = this.originTranslationMatrix.premultiply(this.scaleMatrix).premultiply(this.rotationMatrix).premultiply(this.globalTranslationMatrix);
@@ -1732,10 +1749,13 @@ class Rect {
1732
1749
  * @param min Top left corner of the rectangle.
1733
1750
  * @param max Bottom right corner of the rectangle.
1734
1751
  * @param rotation Optional rotation of the rectangle. In radians, around center. Positive values rotate clockwise.
1752
+ * @param elevation Optional elevation. Positive values raise geometry toward the viewer. Defaults to 0.
1735
1753
  */
1736
- constructor(min, max, rotation) {
1754
+ constructor(min, max, rotation, elevation) {
1737
1755
  /** Optional rotation of the rectangle. In radians, around center. Positive values rotate clockwise. */
1738
1756
  __publicField(this, "rotation");
1757
+ /** Optional elevation of the rectangle. Positive values raise geometry toward the viewer. Defaults to 0. */
1758
+ __publicField(this, "elevation");
1739
1759
  __publicField(this, "_min");
1740
1760
  __publicField(this, "_max");
1741
1761
  __publicField(this, "_center", new Vector2());
@@ -1743,6 +1763,7 @@ class Rect {
1743
1763
  this._min = createVector2(min);
1744
1764
  this._max = createVector2(max);
1745
1765
  this.rotation = rotation ?? 0;
1766
+ this.elevation = elevation ?? 0;
1746
1767
  this.updateCenterAndSize();
1747
1768
  }
1748
1769
  /** Top left corner of the rectangle. */
@@ -1785,14 +1806,15 @@ class Rect {
1785
1806
  * Creates a rectangle from an SVG rectangle element.
1786
1807
  * @param rect {@link SVGRectElement} or {@link SVGImageElement}
1787
1808
  * @param rotation Optional rotation of the rectangle. In radians, around center. Positive values rotate clockwise.
1809
+ * @param elevation Optional elevation. Positive values raise geometry toward the viewer. Defaults to 0.
1788
1810
  * @returns new {@link Rect} instance
1789
1811
  */
1790
- static fromSvg(rect, rotation) {
1812
+ static fromSvg(rect, rotation, elevation) {
1791
1813
  const x = rect.x.baseVal.value;
1792
1814
  const y = rect.y.baseVal.value;
1793
1815
  const width = rect.width.baseVal.value;
1794
1816
  const height = rect.height.baseVal.value;
1795
- return new Rect([x, y], [x + width, y + height], rotation);
1817
+ return new Rect([x, y], [x + width, y + height], rotation, elevation);
1796
1818
  }
1797
1819
  /**
1798
1820
  * Moves the rectangle by the given offset.
@@ -1848,7 +1870,11 @@ class Polygon {
1848
1870
  get indices() {
1849
1871
  return this._indices;
1850
1872
  }
1851
- /** Bounding rectangle of the polygon. */
1873
+ /**
1874
+ * Bounding rectangle of the polygon.
1875
+ * This is a 2D axis-aligned bounding box computed from the X/Y projection of vertices.
1876
+ * The Z extent of the polygon is not tracked.
1877
+ */
1852
1878
  get bounds() {
1853
1879
  return this._bbox;
1854
1880
  }
@@ -1858,11 +1884,12 @@ class Polygon {
1858
1884
  * @returns new {@link Polygon} instance
1859
1885
  */
1860
1886
  static fromRect(rect) {
1887
+ const z = rect.elevation;
1861
1888
  const vertices = [
1862
- { x: rect.min.x, y: rect.min.y },
1863
- { x: rect.max.x, y: rect.min.y },
1864
- { x: rect.max.x, y: rect.max.y },
1865
- { x: rect.min.x, y: rect.max.y }
1889
+ { x: rect.min.x, y: rect.min.y, z },
1890
+ { x: rect.max.x, y: rect.min.y, z },
1891
+ { x: rect.max.x, y: rect.max.y, z },
1892
+ { x: rect.min.x, y: rect.max.y, z }
1866
1893
  ];
1867
1894
  const indices = [
1868
1895
  [0, 1, 2],
@@ -1914,7 +1941,8 @@ class Polygon {
1914
1941
  }
1915
1942
  /**
1916
1943
  * Scales the polygon around the given origin.
1917
- * @param scaleFactor Can be a single number or a 2D vector. If a single number is provided, both horizontal and vertical axes will be scaled by the same factor.
1944
+ * @param scaleFactor Can be a single number or a 2D vector. If a single number is provided, both horizontal and
1945
+ * vertical axes will be scaled by the same factor. Z is unchanged in both cases.
1918
1946
  * @param origin Origin of the scaling. If omitted, defaults to the bounding rectangle center.
1919
1947
  * @returns this {@link Polygon} instance
1920
1948
  */
@@ -1942,6 +1970,31 @@ class Polygon {
1942
1970
  }
1943
1971
  }
1944
1972
  const tempVector2 = new Vector2();
1973
+ function groupBy(list, keyGetter) {
1974
+ const map = /* @__PURE__ */ new Map();
1975
+ list.forEach((item) => {
1976
+ const key = keyGetter(item);
1977
+ const collection = map.get(key);
1978
+ if (!collection) {
1979
+ map.set(key, [item]);
1980
+ } else {
1981
+ collection.push(item);
1982
+ }
1983
+ });
1984
+ return map;
1985
+ }
1986
+ function partition(list, pred) {
1987
+ const truthy = [];
1988
+ const falsy = [];
1989
+ for (const item of list) {
1990
+ if (pred(item)) {
1991
+ truthy.push(item);
1992
+ } else {
1993
+ falsy.push(item);
1994
+ }
1995
+ }
1996
+ return [truthy, falsy];
1997
+ }
1945
1998
  function countGeometry(geometry) {
1946
1999
  var _a2;
1947
2000
  if (geometry.index == null) return { vertices: geometry.getAttribute("position").count, indices: 0 };
@@ -1950,6 +2003,18 @@ function countGeometry(geometry) {
1950
2003
  indices: ((_a2 = geometry.index) == null ? void 0 : _a2.count) ?? 0
1951
2004
  };
1952
2005
  }
2006
+ const sphereCenter = new Vector3();
2007
+ function computeBoundingSphere(bounds, origin2, out = new Sphere()) {
2008
+ const ox = (origin2 == null ? void 0 : origin2[0]) ?? 0.5;
2009
+ const oy = (origin2 == null ? void 0 : origin2[1]) ?? 0.5;
2010
+ const w = bounds.size.x;
2011
+ const h = bounds.size.y;
2012
+ const dx = Math.max(ox, 1 - ox) * w;
2013
+ const dy = Math.max(oy, 1 - oy) * h;
2014
+ sphereCenter.set(bounds.center.x, bounds.center.y, bounds.elevation);
2015
+ out.set(sphereCenter, Math.hypot(dx, dy));
2016
+ return out;
2017
+ }
1953
2018
  const logger$a = createLogger("mesh");
1954
2019
  extend([namesPlugin]);
1955
2020
  class MeshSystem extends RenderableSystem {
@@ -1964,13 +2029,13 @@ class MeshSystem extends RenderableSystem {
1964
2029
  __publicField(this, "rotation", new Quaternion());
1965
2030
  __publicField(this, "scale", new Vector3());
1966
2031
  __publicField(this, "matrix", new Matrix4());
1967
- __publicField(this, "rectGeometry", new PlaneGeometry(1, 1));
2032
+ __publicField(this, "rectGeometry", new PlaneGeometry(1, 1).deleteAttribute("uv").deleteAttribute("normal"));
2033
+ __publicField(this, "rectGeometry3D", new PlaneGeometry(1, 1).deleteAttribute("uv").toNonIndexed());
1968
2034
  __publicField(this, "mapInstanceIdToShapeType", /* @__PURE__ */ new Map());
1969
2035
  this.materialSystem = materialSystem;
1970
- this.rectGeometry.deleteAttribute("normal");
1971
- this.rectGeometry.deleteAttribute("uv");
1972
2036
  }
1973
2037
  buildLayer(layer) {
2038
+ const is3D = layer.mode === "3D";
1974
2039
  const shapes = layer.children;
1975
2040
  const mapShapeToNormColor = /* @__PURE__ */ new Map();
1976
2041
  for (const shapeDef of shapes) {
@@ -1987,21 +2052,15 @@ class MeshSystem extends RenderableSystem {
1987
2052
  const transparentShapesGrouped = groupBy(transparentShapes, (shapeDef) => mapShapeToNormColor.get(shapeDef).a);
1988
2053
  const group = new Group();
1989
2054
  for (const [opacity, shapes2] of transparentShapesGrouped) {
1990
- const transparentMesh = this.buildBatchedMesh(shapes2, opacity);
2055
+ const transparentMesh = this.buildBatchedMesh(shapes2, is3D, opacity);
1991
2056
  transparentMesh.name = "transparent";
1992
2057
  group.add(transparentMesh);
1993
2058
  }
1994
2059
  if (opaqueShapes.length) {
1995
- const opaqueMesh = this.buildBatchedMesh(opaqueShapes);
2060
+ const opaqueMesh = this.buildBatchedMesh(opaqueShapes, is3D);
1996
2061
  opaqueMesh.name = "opaque";
1997
2062
  group.add(opaqueMesh);
1998
2063
  }
1999
- if (layer.mode === "3d") {
2000
- group.children.filter((child) => child instanceof Mesh).forEach((mesh) => {
2001
- const material = mesh.material;
2002
- material.depthFunc = LessEqualDepth;
2003
- });
2004
- }
2005
2064
  return group;
2006
2065
  }
2007
2066
  updateDefImpl(shapeDef, mesh, instanceIds, firstUpdate) {
@@ -2026,7 +2085,9 @@ class MeshSystem extends RenderableSystem {
2026
2085
  logger$a.warn("Polygon geometry changing not supported %O", shapeDef);
2027
2086
  return;
2028
2087
  }
2029
- mesh.setGeometryAt(geometryId, this.buildPolygonGeometry(shape));
2088
+ const geometry = this.buildPolygonGeometry(shape);
2089
+ if (mesh.geometry.hasAttribute("normal")) geometry.computeVertexNormals();
2090
+ mesh.setGeometryAt(geometryId, geometry);
2030
2091
  } else if (isRect) {
2031
2092
  this.updateRect(shape, mesh, instanceId);
2032
2093
  }
@@ -2040,34 +2101,39 @@ class MeshSystem extends RenderableSystem {
2040
2101
  mesh.setColorAt(instanceId, this.color.setRGB(color.r / 255, color.g / 255, color.b / 255, SRGBColorSpace));
2041
2102
  }
2042
2103
  updateRect(shape, mesh, instanceId) {
2043
- this.position.set(shape.center.x, shape.center.y, 0);
2044
- this.rotation.setFromAxisAngle(new Vector3(0, 0, 1), shape.rotation ?? 0);
2104
+ this.position.set(shape.center.x, shape.center.y, shape.elevation);
2105
+ this.rotation.setFromAxisAngle(new Vector3(0, 0, 1), shape.rotation);
2045
2106
  this.scale.set(shape.size.x, shape.size.y, 1);
2046
2107
  this.matrix.compose(this.position, this.rotation, this.scale);
2047
2108
  mesh.setMatrixAt(instanceId, this.matrix);
2048
2109
  }
2049
- buildBatchedMesh(shapes, opacity = 1) {
2110
+ buildBatchedMesh(shapes, is3D, opacity = 1) {
2050
2111
  let vertexCount = 0;
2051
2112
  let indexCount = 0;
2052
2113
  let rectAdded = false;
2053
2114
  const shapeDefToGeometry = /* @__PURE__ */ new Map();
2115
+ const rectGeom = is3D ? this.rectGeometry3D : this.rectGeometry;
2054
2116
  for (const shapeDef of shapes) {
2055
2117
  let vertices = 0;
2056
2118
  let indices = 0;
2057
2119
  if (shapeDef.shape instanceof Rect && !rectAdded) {
2058
2120
  rectAdded = true;
2059
- ({ vertices, indices } = countGeometry(this.rectGeometry));
2121
+ ({ vertices, indices } = countGeometry(rectGeom));
2060
2122
  } else if (shapeDef.shape instanceof Polygon) {
2061
- const geometry = this.buildPolygonGeometry(shapeDef.shape);
2123
+ let geometry = this.buildPolygonGeometry(shapeDef.shape);
2124
+ if (is3D) {
2125
+ geometry = geometry.toNonIndexed();
2126
+ geometry.computeVertexNormals();
2127
+ }
2062
2128
  shapeDefToGeometry.set(shapeDef, geometry);
2063
2129
  ({ vertices, indices } = countGeometry(geometry));
2064
2130
  }
2065
2131
  vertexCount += vertices;
2066
2132
  indexCount += indices;
2067
2133
  }
2068
- const material = this.materialSystem.createColorMaterial({ opacity });
2134
+ const material = this.materialSystem.createColorMaterial({ opacity, depthEnable: is3D, lightingEnable: is3D });
2069
2135
  const batchedMesh = new BatchedMesh(shapes.length, vertexCount, indexCount, material);
2070
- const rectGeometryId = rectAdded ? batchedMesh.addGeometry(this.rectGeometry) : void 0;
2136
+ const rectGeometryId = rectAdded ? batchedMesh.addGeometry(rectGeom) : void 0;
2071
2137
  batchedMesh.setCustomSort((list) => this.sortInstances(batchedMesh, list));
2072
2138
  for (const shapeDef of shapes) {
2073
2139
  let instanceId;
@@ -2201,9 +2267,10 @@ class TextSystem extends RenderableSystem {
2201
2267
  }
2202
2268
  }
2203
2269
  buildBatchedText(layer) {
2270
+ const is3D = layer.mode === "3D";
2204
2271
  const textDefs = layer.children;
2205
2272
  const batchedText = new BatchedText();
2206
- batchedText.material = this.materialSystem.createColorMaterial();
2273
+ batchedText.material = this.materialSystem.createColorMaterial({ depthEnable: is3D });
2207
2274
  const mappingData = [];
2208
2275
  let instanceId = 0;
2209
2276
  for (const textDef of textDefs) {
@@ -2247,7 +2314,7 @@ class TextSystem extends RenderableSystem {
2247
2314
  this.worldPosition.copy(this.localPosition).rotateAround({ x: 0, y: 0 }, textDef.bounds.rotation).add(textDef.bounds.center);
2248
2315
  this.textScale.copy(this.initialTextScale).multiplyScalar(fontSize * dpr);
2249
2316
  text.scale.set(this.textScale.x, this.textScale.y, 1);
2250
- text.position.set(this.worldPosition.x, this.worldPosition.y, 0);
2317
+ text.position.set(this.worldPosition.x, this.worldPosition.y, textDef.bounds.elevation);
2251
2318
  text.rotation.set(0, 0, textDef.bounds.rotation);
2252
2319
  this.calculateClipRect(text, textDef, this.localPosition, this.textScale, this.localToMin, this.localToMax);
2253
2320
  this.localPosition.y += height * dpr;
@@ -2482,22 +2549,24 @@ class LayerSystem {
2482
2549
  * @returns sorted leaf layers for debug logging
2483
2550
  */
2484
2551
  initRenderOrder(rootLayer) {
2485
- const leafLayers = [];
2486
- const stack = [rootLayer];
2552
+ const twoDLayers = [];
2553
+ const threeDLayers = [];
2554
+ const stack = [
2555
+ { layer: rootLayer, inheritedMode: rootLayer.mode ?? "2D" }
2556
+ ];
2487
2557
  while (stack.length) {
2488
- const layer = stack.pop();
2558
+ const { layer, inheritedMode } = stack.pop();
2489
2559
  if (isLayerLayer(layer)) {
2490
- const children = [...layer.children];
2491
- children.reverse();
2492
- for (const child of children) {
2560
+ for (let i = layer.children.length - 1; i >= 0; i--) {
2561
+ const child = layer.children[i];
2493
2562
  this.mapLayerDefToParent.set(child, layer);
2494
- stack.push(child);
2563
+ stack.push({ layer: child, inheritedMode: child.mode ?? inheritedMode });
2495
2564
  }
2496
2565
  } else {
2497
- leafLayers.push(layer);
2566
+ layer.mode ?? (layer.mode = inheritedMode);
2567
+ (layer.mode === "3D" ? threeDLayers : twoDLayers).push(layer);
2498
2568
  }
2499
2569
  }
2500
- const [threeDLayers, twoDLayers] = partition(leafLayers, (layer) => layer.mode === "3d");
2501
2570
  const sorted = [...twoDLayers, ...threeDLayers];
2502
2571
  for (let i = 0; i < sorted.length; i++) {
2503
2572
  this.renderOrderMap.set(sorted[i], i + 1);
@@ -2514,6 +2583,36 @@ class LayerSystem {
2514
2583
  return fullName;
2515
2584
  }
2516
2585
  }
2586
+ class LightsSystem {
2587
+ constructor() {
2588
+ __publicField(this, "color", 16777215);
2589
+ __publicField(this, "intensity", 0.5);
2590
+ __publicField(this, "mapSceneToLight", /* @__PURE__ */ new Map());
2591
+ }
2592
+ /**
2593
+ * Initializes a directional light for the given scene.
2594
+ * @param scene {@link Scene} to add the light to
2595
+ */
2596
+ initLights(scene) {
2597
+ const ambientLight = new AmbientLight(16777215, this.intensity);
2598
+ scene.add(ambientLight);
2599
+ const directionalLight = new DirectionalLight(this.color, this.intensity * Math.PI * 2);
2600
+ scene.add(directionalLight);
2601
+ scene.add(directionalLight.target);
2602
+ this.mapSceneToLight.set(scene, directionalLight);
2603
+ }
2604
+ /**
2605
+ * Updates the light direction to match the camera's viewing direction.
2606
+ * @param scene {@link Scene} containing the light
2607
+ * @param camera {@link Camera} to derive the light direction from
2608
+ */
2609
+ updateLights(scene, camera) {
2610
+ const light = this.mapSceneToLight.get(scene);
2611
+ if (!light) return;
2612
+ const e = scene.matrixWorld.elements;
2613
+ camera.getWorldDirection(light.position).multiply({ x: Math.sign(e[0]), y: Math.sign(e[5]), z: Math.sign(e[10]) }).normalize().negate();
2614
+ }
2615
+ }
2517
2616
  const logger$7 = createLogger("");
2518
2617
  function assertNotDisposed(renderer, funcName) {
2519
2618
  if (renderer.isDisposed) {
@@ -2612,10 +2711,12 @@ class ExternalCameraSystem {
2612
2711
  }
2613
2712
  }
2614
2713
  const originalProjectionMatrix = new Matrix4();
2714
+ const patchedMeshes = /* @__PURE__ */ new WeakSet();
2615
2715
  function patchMatricesInExternalMode(scene) {
2616
2716
  scene.traverse((child) => {
2617
2717
  const mesh = child;
2618
- if (!mesh.isMesh) return;
2718
+ if (!mesh.isMesh || patchedMeshes.has(mesh)) return;
2719
+ patchedMeshes.add(mesh);
2619
2720
  const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
2620
2721
  for (const material of materials) {
2621
2722
  const onBeforeRender = material.onBeforeRender.bind(material);
@@ -3011,12 +3112,15 @@ class CoordinatesSystem {
3011
3112
  return out;
3012
3113
  }
3013
3114
  /**
3014
- * Converts a point from screen coordinates to world space.
3115
+ * Converts a point from NDC to world space by intersecting the ground plane (z=0).
3116
+ * Returns 2D world coordinates (z is always 0). This is appropriate for coordinate
3117
+ * conversions that don't need to account for 3D geometry (e.g. canvasToSvg, camera controls).
3118
+ * For pointer events that should land on 3D geometry, use scene raycasting instead.
3015
3119
  * @param ndcCoords Point in NDC (normalized device coordinates)
3016
3120
  * @param out Optional output vector
3017
- * @returns Point in world space
3121
+ * @returns Point on the ground plane in world space, or undefined if the ray is parallel to the plane
3018
3122
  */
3019
- ndcToWorld(ndcCoords, out = new Vector3()) {
3123
+ ndcToWorldPlane(ndcCoords, out = new Vector3()) {
3020
3124
  return this.pickingSystem.intersectPlane(ndcCoords, this.cameraSystem.camera, out);
3021
3125
  }
3022
3126
  /**
@@ -3046,7 +3150,7 @@ class CoordinatesSystem {
3046
3150
  const vec2 = new Vector2();
3047
3151
  const vec3 = new Vector3();
3048
3152
  const ndcPoint = this.canvasToNDC(point, vec2);
3049
- const worldPoint = this.ndcToWorld(ndcPoint, vec3);
3153
+ const worldPoint = this.ndcToWorldPlane(ndcPoint, vec3);
3050
3154
  if (!worldPoint) return [];
3051
3155
  const result = [];
3052
3156
  for (const sceneState of this.sceneSystem.sceneStates) {
@@ -5665,6 +5769,9 @@ const subsetOfTHREE = {
5665
5769
  };
5666
5770
  CameraControls.install({ THREE: subsetOfTHREE });
5667
5771
  const logger$4 = createLogger("cameraController");
5772
+ const ELEVATION_PRECISION = 0.1;
5773
+ const ELEVATION_HEADROOM_RATIO = 0.98;
5774
+ const DEPTH_BUFFER_LEVELS = 2 ** 24;
5668
5775
  class CameraController extends CameraControls {
5669
5776
  /**
5670
5777
  * @param camera {@link PerspectiveCamera} instance
@@ -5686,6 +5793,7 @@ class CameraController extends CameraControls {
5686
5793
  three: CameraController.ACTION.NONE
5687
5794
  };
5688
5795
  this.touchCancelListener = () => this.cancel();
5796
+ void this.rotatePolarTo(0, false);
5689
5797
  }
5690
5798
  /**
5691
5799
  * Get bearing angle between current camera orientation and true north (in radians).
@@ -5696,15 +5804,50 @@ class CameraController extends CameraControls {
5696
5804
  }
5697
5805
  update(delta) {
5698
5806
  const needsUpdate = super.update(delta);
5699
- if (needsUpdate && delta > 0) {
5700
- const position = this.camera.position.toArray().map((value) => +value.toFixed(2));
5701
- const target = this._target.toArray().map((value) => +value.toFixed(2));
5702
- const { theta, phi, radius } = this._spherical;
5703
- const spherical = [theta * RAD2DEG, phi * RAD2DEG, radius].map((value) => +value.toFixed(2));
5704
- logger$4.debug("camera update %O", { position, target, spherical });
5807
+ if (needsUpdate) {
5808
+ this.updateNearPlane();
5809
+ if (delta > 0) {
5810
+ const position = this.camera.position.toArray().map((value) => +value.toFixed(2));
5811
+ const target = this._target.toArray().map((value) => +value.toFixed(2));
5812
+ const { theta, phi, radius } = this._spherical;
5813
+ const spherical = [theta * RAD2DEG, phi * RAD2DEG, radius].map((value) => +value.toFixed(2));
5814
+ const clippingPlanes = [this.camera.near, this.camera.far];
5815
+ logger$4.debug("camera update %O", { position, target, spherical, clippingPlanes });
5816
+ }
5705
5817
  }
5706
5818
  return needsUpdate;
5707
5819
  }
5820
+ /**
5821
+ * Updates the near clipping plane to maintain consistent depth buffer precision
5822
+ * at the scene plane regardless of zoom level.
5823
+ *
5824
+ * Two constraints determine the near plane:
5825
+ * - **Headroom**: near = D × (1 − headroom ratio). Dominates at close/default zoom,
5826
+ * keeping headroom at exactly the target ratio. Precision exceeds the target.
5827
+ * - **Precision**: near = D² / (2²⁴ × P). Dominates at far zoom, maintaining target
5828
+ * precision at the cost of reduced headroom.
5829
+ *
5830
+ * The larger of the two is used (precision is never sacrificed for headroom).
5831
+ * The depth precision P is derived from {@link ELEVATION_PRECISION} and the current
5832
+ * {@link CameraControls.maxPolarAngle}, so it adapts when pitch limits change.
5833
+ *
5834
+ * Depth precision at the floor plane (z=0) is the guaranteed minimum. Content above
5835
+ * (closer to camera) has quadratically better precision: P × (d/D)². Content below the
5836
+ * floor degrades quadratically but remains within ~1% for practical depths (< 1% of D).
5837
+ */
5838
+ updateNearPlane() {
5839
+ const camera = this.camera;
5840
+ const D = this.distance;
5841
+ if (D <= 0) return;
5842
+ const cosPitch = Math.max(Math.cos(this.maxPolarAngle), 0.01);
5843
+ const depthPrecision = ELEVATION_PRECISION * cosPitch;
5844
+ const headroomNear = D * (1 - ELEVATION_HEADROOM_RATIO);
5845
+ const precisionNear = D * D / (DEPTH_BUFFER_LEVELS * depthPrecision);
5846
+ const near = Math.max(headroomNear, precisionNear);
5847
+ if (camera.near === near) return;
5848
+ camera.near = near;
5849
+ camera.updateProjectionMatrix();
5850
+ }
5708
5851
  connect(domElement) {
5709
5852
  super.connect(domElement);
5710
5853
  domElement.addEventListener("touchcancel", this.touchCancelListener);
@@ -6074,7 +6217,7 @@ class RollHandler extends Handler {
6074
6217
  }
6075
6218
  setPivot(e) {
6076
6219
  this.coordinatesSystem.canvasToNDC(e.offsetCenter, this.pivotNDC);
6077
- if (!this.coordinatesSystem.ndcToWorld(this.pivotNDC, this.pivotWorld)) return false;
6220
+ if (!this.coordinatesSystem.ndcToWorldPlane(this.pivotNDC, this.pivotWorld)) return false;
6078
6221
  this.controller.getPosition(this.cameraPosition);
6079
6222
  this.controller.getTarget(this.targetWorld);
6080
6223
  this.cameraForward.copy(this.targetWorld).sub(this.cameraPosition);
@@ -6160,7 +6303,7 @@ class InteractionsSystem {
6160
6303
  if (internalCameraSystem) {
6161
6304
  this.eventManager = new EventManager(this.canvas, {
6162
6305
  recognizers: [Rotate, [Pan, { event: "pitch", pointers: 2 }, "rotate"]]
6163
- // FIXME: Double click to zoom
6306
+ // TODO: Double click to zoom
6164
6307
  });
6165
6308
  this.handlers = {
6166
6309
  pan: new PanHandler(this.canvas, this.eventManager, internalCameraSystem, coordinatesSystem),
@@ -6188,8 +6331,6 @@ class InteractionsSystem {
6188
6331
  });
6189
6332
  this.handlers.pan.enable();
6190
6333
  this.handlers.zoom.enable();
6191
- this.handlers.roll.enable();
6192
- this.handlers.pitch.enable();
6193
6334
  }
6194
6335
  }
6195
6336
  /**
@@ -6251,15 +6392,18 @@ class InteractionsSystem {
6251
6392
  if (isDragging || !hasListeners) return;
6252
6393
  const mousePointer = eventToCanvas(event);
6253
6394
  coordinatesSystem.canvasToNDC(mousePointer, this.mousePointerNDC);
6254
- if (!coordinatesSystem.ndcToWorld(this.mousePointerNDC, this.mousePointerWorld)) return;
6255
- const intersectionsByScene = this.viewportAccess.getIntersectedObjectsByScene(this.mousePointerNDC);
6395
+ const raycastResults = this.viewportAccess.raycastByScene(this.mousePointerNDC);
6256
6396
  const data = [];
6257
- for (const { sceneId, intersections } of intersectionsByScene) {
6258
- const point = coordinatesSystem.worldToModel(this.mousePointerWorld, this.mousePointerModel, sceneId);
6259
- const defs = this.layerSystem.getIntersectedDefs(intersections);
6397
+ for (const { sceneId, intersections } of raycastResults) {
6398
+ const interactive = intersections.filter((i) => isInteractive(i.object));
6399
+ const closestHit = interactive[0] ?? intersections[0];
6400
+ const worldPoint = closestHit ? this.mousePointerWorld.copy(closestHit.point) : coordinatesSystem.ndcToWorldPlane(this.mousePointerNDC, this.mousePointerWorld);
6401
+ if (!worldPoint) continue;
6402
+ const point = coordinatesSystem.worldToModel(worldPoint, this.mousePointerModel, sceneId);
6403
+ const defs = this.layerSystem.getIntersectedDefs(interactive);
6260
6404
  data.push({ point: { x: point.x, y: point.y }, defs, sceneId });
6261
6405
  }
6262
- this.events.emit(eventType, { event, data });
6406
+ if (data.length > 0) this.events.emit(eventType, { event, data });
6263
6407
  };
6264
6408
  const mousemove = (event) => sharedMouseHandler("mousemove", event);
6265
6409
  const mouseout = (event) => sharedMouseHandler("mouseout", event);
@@ -6295,7 +6439,7 @@ class UpdatesSystem {
6295
6439
  constructor(ctx, layerSystem) {
6296
6440
  __publicField(this, "pendingDefs", /* @__PURE__ */ new Set());
6297
6441
  __publicField(this, "culledDefs", /* @__PURE__ */ new Set());
6298
- __publicField(this, "defBounds", new Box3());
6442
+ __publicField(this, "defSphere", new Sphere());
6299
6443
  __publicField(this, "useUpdateBuffering", true);
6300
6444
  this.ctx = ctx;
6301
6445
  this.layerSystem = layerSystem;
@@ -6336,10 +6480,9 @@ class UpdatesSystem {
6336
6480
  let culled = false;
6337
6481
  this.pendingDefs.delete(def);
6338
6482
  if (isTextDef(def) || isImageDef(def)) {
6339
- const bounds = def.bounds;
6340
- this.defBounds.min.set(bounds.min.x, bounds.min.y, 0);
6341
- this.defBounds.max.set(bounds.max.x, bounds.max.y, 0);
6342
- culled = !frustum2.intersectsBox(this.defBounds);
6483
+ const origin2 = isImageDef(def) ? def.origin : void 0;
6484
+ computeBoundingSphere(def.bounds, origin2, this.defSphere);
6485
+ culled = !frustum2.intersectsSphere(this.defSphere);
6343
6486
  }
6344
6487
  if (culled) this.culledDefs.add(def);
6345
6488
  else {
@@ -6384,8 +6527,8 @@ class InternalCameraSystem {
6384
6527
  const h = ctx.getDrawingBufferSizePx()[1];
6385
6528
  this.prevViewportHeightPx = h;
6386
6529
  this.camera = new PerspectiveCamera(this.defaultFov);
6530
+ this.camera.up.set(0, 0, -1);
6387
6531
  this.controller = new CameraController(this.camera);
6388
- void this.controller.rotatePolarTo(0, false);
6389
6532
  }
6390
6533
  /** Current camera zoom factor. */
6391
6534
  get zoomFactor() {
@@ -6418,12 +6561,8 @@ class InternalCameraSystem {
6418
6561
  initCamera(viewbox) {
6419
6562
  this.zoomBounds = [0.1, viewbox.size.x > 1e4 ? 100 : 35];
6420
6563
  this.updateCamera();
6421
- this.camera.up.set(0, -1, 0);
6422
- this.camera.position.set(0, 0, -this.zoomIdentityDistance);
6423
- this.camera.lookAt(0, 0, 0);
6564
+ this.controller.update(0);
6424
6565
  this.camera.updateMatrixWorld();
6425
- this.camera.up.set(0, 0, -1);
6426
- this.controller.updateCameraUp();
6427
6566
  }
6428
6567
  /** Updates the camera when the renderer size changes. */
6429
6568
  updateCamera() {
@@ -6435,8 +6574,7 @@ class InternalCameraSystem {
6435
6574
  const maxDistance = Math.abs(newZoomIdentity / this.zoomBounds[0]);
6436
6575
  const minDistance = Math.abs(newZoomIdentity / this.zoomBounds[1]);
6437
6576
  this.camera.aspect = w / (h || 1);
6438
- this.camera.near = 0.01;
6439
- this.camera.far = Math.max(maxDistance, this.camera.near) * 2;
6577
+ this.camera.far = Math.max(maxDistance, 0.1) * 2;
6440
6578
  this.camera.updateProjectionMatrix();
6441
6579
  this.controller.minDistance = minDistance;
6442
6580
  this.controller.maxDistance = maxDistance;
@@ -6471,21 +6609,21 @@ class InternalCameraSystem {
6471
6609
  }
6472
6610
  }
6473
6611
  class PickingSystem {
6474
- /** */
6475
6612
  constructor() {
6476
6613
  __publicField(this, "raycaster", new Raycaster());
6477
6614
  __publicField(this, "ndcPoint", new Vector2());
6478
6615
  __publicField(this, "viewboxPlane", new Plane(new Vector3(0, 0, 1), 0));
6479
- this.raycaster.layers.set(INTERACTIVE_LAYER);
6480
6616
  }
6481
6617
  /**
6482
- * Gets the objects intersected by the raycaster.
6618
+ * Raycasts the scene and returns all visible intersections sorted by distance.
6619
+ * Does not filter by interaction layer — consumers are responsible for partitioning
6620
+ * interactive vs non-interactive results using {@link isInteractive}.
6483
6621
  * @param ndcCoords raycast point in NDC (normalized device coordinates)
6484
6622
  * @param scene {@link Scene} instance
6485
6623
  * @param camera {@link Camera} instance
6486
- * @returns Array of {@link Intersection} instances
6624
+ * @returns Array of {@link Intersection} instances, nearest first
6487
6625
  */
6488
- getIntersectedObjects(ndcCoords, scene, camera) {
6626
+ raycast(ndcCoords, scene, camera) {
6489
6627
  this.setRaycasterFromCamera(ndcCoords, camera);
6490
6628
  const intersections = this.raycaster.intersectObject(scene, true);
6491
6629
  return intersections.filter((i) => isVisible(i.object));
@@ -6608,7 +6746,7 @@ class SceneSystem {
6608
6746
  const scaleFactor = Math.min(visibleRectSize.width, visibleRectSize.height);
6609
6747
  const { x: centerX, y: centerY } = viewbox.center;
6610
6748
  this.translationMatrix.makeTranslation(-centerX, -centerY, 0);
6611
- this.scaleMatrix.makeScale(scaleFactor, scaleFactor, 1);
6749
+ this.scaleMatrix.makeScale(scaleFactor, scaleFactor, -scaleFactor);
6612
6750
  targetMatrix.copy(this.translationMatrix).premultiply(this.scaleMatrix);
6613
6751
  const viewportRectCenter = this.tempVector2.copy(viewportRectPx.center);
6614
6752
  const offset = viewportRectCenter.sub({ x: bufferW / 2, y: bufferH / 2 });
@@ -6680,15 +6818,16 @@ class ViewportSystem {
6680
6818
  this.visibleRectCss = rect;
6681
6819
  }
6682
6820
  /**
6683
- * Get the objects intersected by the raycaster grouped by scene.
6821
+ * Raycasts all loaded scenes and returns all visible intersections per scene.
6822
+ * Results are not filtered by interaction layer — consumers partition as needed.
6684
6823
  * @param ndcCoords raycast point in NDC (normalized device coordinates)
6685
- * @returns Array of {@link IntersectionsWithinScene} instances
6824
+ * @returns Array of {@link SceneRaycastResult} instances
6686
6825
  */
6687
- getIntersectedObjectsByScene(ndcCoords) {
6826
+ raycastByScene(ndcCoords) {
6688
6827
  const result = [];
6689
6828
  for (const sceneState of this.sceneSystem.sceneStates) {
6690
6829
  if (!sceneState.loaded) continue;
6691
- const intersections = this.pickingSystem.getIntersectedObjects(ndcCoords, sceneState.scene, this.camera);
6830
+ const intersections = this.pickingSystem.raycast(ndcCoords, sceneState.scene, this.camera);
6692
6831
  result.push({ intersections, sceneId: sceneState.id });
6693
6832
  }
6694
6833
  return result;
@@ -6850,6 +6989,7 @@ class Renderer {
6850
6989
  __publicField(this, "memoryInfoExtension", null);
6851
6990
  __publicField(this, "memoryInfo");
6852
6991
  __publicField(this, "eventSystem");
6992
+ __publicField(this, "lightsSystem");
6853
6993
  __publicField(this, "layerSystem");
6854
6994
  __publicField(this, "viewportSystem");
6855
6995
  __publicField(this, "updatesSystem");
@@ -6868,7 +7008,8 @@ class Renderer {
6868
7008
  const rendererOptions = {
6869
7009
  antialias: true,
6870
7010
  context: gl,
6871
- canvas: this.canvas
7011
+ canvas: this.canvas,
7012
+ precision: "highp"
6872
7013
  };
6873
7014
  this.clock = new Clock();
6874
7015
  this.renderer = new WebGLRenderer(rendererOptions);
@@ -6877,6 +7018,7 @@ class Renderer {
6877
7018
  this.renderer.autoClear = !this.isExternalMode;
6878
7019
  this.ctx = this.createRendererContext();
6879
7020
  this.eventSystem = new EventSystem();
7021
+ this.lightsSystem = new LightsSystem();
6880
7022
  this.layerSystem = new LayerSystem(this.ctx);
6881
7023
  this.viewportSystem = new ViewportSystem(this.ctx, this.eventSystem);
6882
7024
  this.updatesSystem = new UpdatesSystem(this.ctx, this.layerSystem);
@@ -7046,7 +7188,6 @@ class Renderer {
7046
7188
  } else if (sceneState.scene.children.length === 0) {
7047
7189
  const root = this.layerSystem.buildScene(sceneState.sceneDef);
7048
7190
  scene.add(root);
7049
- if (this.isExternalMode) patchMatricesInExternalMode(scene);
7050
7191
  }
7051
7192
  }
7052
7193
  const justLoaded = loadedChanged && sceneState.loaded;
@@ -7054,6 +7195,8 @@ class Renderer {
7054
7195
  this.viewportSystem.updatePtScale(id);
7055
7196
  const hasDefsUpdated = this.updatesSystem.processPendingUpdates(frustum2, 3);
7056
7197
  const forceRedraw = hasControlsUpdated || hasDefsUpdated || justLoaded || this.isExternalMode || this.ui;
7198
+ if (this.isExternalMode) patchMatricesInExternalMode(scene);
7199
+ this.lightsSystem.updateLights(scene, camera);
7057
7200
  if (this.needsRedraw || forceRedraw) this.renderer.render(scene, camera);
7058
7201
  if (hasDefsUpdated || this.needsRedraw) this.viewportSystem.invalidateSceneBounds(sceneState);
7059
7202
  }
@@ -7090,8 +7233,9 @@ class Renderer {
7090
7233
  }
7091
7234
  initScene(sceneDef, sceneId) {
7092
7235
  const root = this.layerSystem.buildScene(sceneDef);
7093
- const scene = this.viewportSystem.initScene(sceneDef, sceneId).add(root);
7094
- if (this.isExternalMode) patchMatricesInExternalMode(scene);
7236
+ const scene = this.viewportSystem.initScene(sceneDef, sceneId);
7237
+ scene.add(root);
7238
+ this.lightsSystem.initLights(scene);
7095
7239
  }
7096
7240
  // https://webgl2fundamentals.org/webgl/lessons/webgl-resizing-the-canvas.html
7097
7241
  resizeCanvasToDisplaySize() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expofp/renderer",
3
- "version": "3.0.1",
3
+ "version": "3.1.1",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"