@expofp/renderer 3.0.1 → 3.1.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.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.
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, MeshBasicMaterial, LessEqualDepth, Texture, Group, PlaneGeometry, SRGBColorSpace, Vector2, Quaternion, BufferGeometry, LinearSRGBColorSpace, Mesh, 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,47 @@ 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
- };
1104
+ createColorMaterial(params = {}) {
1109
1105
  const material = new MeshBasicMaterial({
1110
1106
  ...sharedParameters,
1111
- color: params.color,
1112
- opacity: params.opacity
1107
+ color: params.color ?? 16777215,
1108
+ opacity: params.opacity ?? 1
1113
1109
  });
1110
+ if (params.is3D) material.depthFunc = LessEqualDepth;
1114
1111
  addDimToMaterial(material);
1112
+ this.addPolygonOffset(material);
1115
1113
  return material;
1116
1114
  }
1117
1115
  /**
1118
1116
  * Creates a texture material.
1119
- * @param map {@link Texture}
1120
- * @param uvOffset whether to enable uv offset with per instance uniforms (for texture atlases)
1117
+ * @param params {@link MaterialTextureParams}
1121
1118
  * @returns MeshBasicMaterial instance
1122
1119
  */
1123
- createTextureMaterial(map, uvOffset = false) {
1124
- const material = new MeshBasicMaterial({ ...sharedParameters, map });
1125
- if (uvOffset) {
1120
+ createTextureMaterial(params) {
1121
+ const material = new MeshBasicMaterial({ ...sharedParameters, map: params.map });
1122
+ if (params.is3D) material.depthFunc = LessEqualDepth;
1123
+ if (params.uvOffset) {
1126
1124
  material.onBeforeCompile = (shader) => {
1127
1125
  shader.vertexShader = shader.vertexShader.replace(
1128
1126
  "#include <uv_vertex>",
1129
1127
  /*glsl*/
1130
1128
  `
1131
1129
  #include <uv_vertex>
1132
- vMapUv = uv * uvOffset.zw + uvOffset.xy;
1130
+ vMapUv = uv * uvOffset.zw + uvOffset.xy;
1133
1131
  `
1134
1132
  );
1135
1133
  };
1136
1134
  }
1137
1135
  addDimToMaterial(material);
1136
+ this.addPolygonOffset(material);
1138
1137
  return material;
1139
1138
  }
1140
1139
  /**
@@ -1158,6 +1157,40 @@ class MaterialSystem {
1158
1157
  }
1159
1158
  return this.backgroundMaterial;
1160
1159
  }
1160
+ /**
1161
+ * Patches a material's onBeforeRender to dynamically set polygonOffset based on the
1162
+ * object's renderOrder. Later layers (higher renderOrder) get a larger negative offset,
1163
+ * giving them a slight depth buffer advantage over earlier layers at the same elevation.
1164
+ * This prevents z-fighting between coplanar 3D content across different layers.
1165
+ *
1166
+ * The units multiplier is derived from {@link CameraControls.maxPolarAngle}:
1167
+ *
1168
+ * multiplier = ⌈tan(maxPolarAngle)⌉
1169
+ *
1170
+ * At steep camera pitch, coplanar surfaces from different draw calls (different
1171
+ * BatchedMesh instances) compute slightly different depth values at the same pixel
1172
+ * due to floating-point interpolation differences in the vertex shader. These errors
1173
+ * are proportional to the depth gradient across the surface, which for a horizontal
1174
+ * plane at pitch angle θ is proportional to tan(θ). The multiplier ensures the
1175
+ * per-layer polygon offset exceeds these errors at the configured maximum pitch.
1176
+ *
1177
+ * For elevated content the effective viewing angle can exceed maxPolarAngle, but this
1178
+ * is self-correcting: the screen-space height of the surface shrinks as cos(angle),
1179
+ * so the total visible z-fighting (pixels × severity ∝ cos × tan = sin) is bounded
1180
+ * and becomes subpixel as the effective angle approaches 90°.
1181
+ *
1182
+ * With maxPolarAngle = 85°: ⌈tan(85°)⌉ = ⌈11.43⌉ = 12.
1183
+ * @param material Material to patch
1184
+ */
1185
+ addPolygonOffset(material) {
1186
+ const onBeforeRender = material.onBeforeRender.bind(material);
1187
+ material.onBeforeRender = (renderer, scene, camera, geometry, object, group) => {
1188
+ onBeforeRender(renderer, scene, camera, geometry, object, group);
1189
+ material.polygonOffset = true;
1190
+ material.polygonOffsetFactor = 0;
1191
+ material.polygonOffsetUnits = -object.renderOrder * POLYGON_OFFSET_MULTIPLIER;
1192
+ };
1193
+ }
1161
1194
  }
1162
1195
  function isShapeDef(def) {
1163
1196
  return def.shape !== void 0;
@@ -1189,36 +1222,14 @@ function isLineLayer(layer) {
1189
1222
  function isLayerLayer(layer) {
1190
1223
  return layer.children[0] && isLayerDef(layer.children[0]);
1191
1224
  }
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
1225
  const INTERACTIVE_LAYER = 1;
1218
- function setInteractive(object, isInteractive) {
1219
- if (isInteractive) object.layers.enable(INTERACTIVE_LAYER);
1226
+ function isInteractive(object) {
1227
+ return object.layers.isEnabled(INTERACTIVE_LAYER);
1228
+ }
1229
+ function setInteractive(object, isInteractive2) {
1230
+ if (isInteractive2) object.layers.enable(INTERACTIVE_LAYER);
1220
1231
  else object.layers.disable(INTERACTIVE_LAYER);
1221
- object.children.forEach((child) => setInteractive(child, isInteractive));
1232
+ object.children.forEach((child) => setInteractive(child, isInteractive2));
1222
1233
  }
1223
1234
  function isVisible(object) {
1224
1235
  if (!object.visible) return false;
@@ -1483,6 +1494,7 @@ class ImageSystem extends RenderableSystem {
1483
1494
  }
1484
1495
  buildLayer(layer) {
1485
1496
  var _a2;
1497
+ const is3D = layer.mode === "3D";
1486
1498
  const group = new Group();
1487
1499
  const images = layer.children;
1488
1500
  const bins = this.packImages(images);
@@ -1497,7 +1509,7 @@ class ImageSystem extends RenderableSystem {
1497
1509
  ).sort((a, b) => a.originalIndex - b.originalIndex);
1498
1510
  const instanceCount = rectsWithDef.length;
1499
1511
  const texture = createAtlas(bin);
1500
- const instanceMaterial = this.materialSystem.createTextureMaterial(texture, true);
1512
+ const instanceMaterial = this.materialSystem.createTextureMaterial({ map: texture, uvOffset: true, is3D });
1501
1513
  const instanceGeometry = new PlaneGeometry();
1502
1514
  const vertexCount = instanceGeometry.attributes["position"].count;
1503
1515
  const indexCount = ((_a2 = instanceGeometry.index) == null ? void 0 : _a2.count) ?? 0;
@@ -1582,7 +1594,7 @@ class ImageSystem extends RenderableSystem {
1582
1594
  const bounds = imageDef.bounds;
1583
1595
  const origin2 = imageDef.origin ?? [0.5, 0.5];
1584
1596
  this.originTranslationMatrix.makeTranslation(0.5 - origin2[0], 0.5 - origin2[1], 0);
1585
- this.globalTranslationMatrix.makeTranslation(bounds.center.x, bounds.center.y, 0);
1597
+ this.globalTranslationMatrix.makeTranslation(bounds.center.x, bounds.center.y, bounds.elevation);
1586
1598
  this.scaleMatrix.makeScale(bounds.size.x, bounds.size.y, 1);
1587
1599
  this.rotationMatrix.makeRotationZ(bounds.rotation);
1588
1600
  const matrix = this.originTranslationMatrix.premultiply(this.scaleMatrix).premultiply(this.rotationMatrix).premultiply(this.globalTranslationMatrix);
@@ -1732,10 +1744,13 @@ class Rect {
1732
1744
  * @param min Top left corner of the rectangle.
1733
1745
  * @param max Bottom right corner of the rectangle.
1734
1746
  * @param rotation Optional rotation of the rectangle. In radians, around center. Positive values rotate clockwise.
1747
+ * @param elevation Optional elevation. Positive values raise geometry toward the viewer. Defaults to 0.
1735
1748
  */
1736
- constructor(min, max, rotation) {
1749
+ constructor(min, max, rotation, elevation) {
1737
1750
  /** Optional rotation of the rectangle. In radians, around center. Positive values rotate clockwise. */
1738
1751
  __publicField(this, "rotation");
1752
+ /** Optional elevation of the rectangle. Positive values raise geometry toward the viewer. Defaults to 0. */
1753
+ __publicField(this, "elevation");
1739
1754
  __publicField(this, "_min");
1740
1755
  __publicField(this, "_max");
1741
1756
  __publicField(this, "_center", new Vector2());
@@ -1743,6 +1758,7 @@ class Rect {
1743
1758
  this._min = createVector2(min);
1744
1759
  this._max = createVector2(max);
1745
1760
  this.rotation = rotation ?? 0;
1761
+ this.elevation = elevation ?? 0;
1746
1762
  this.updateCenterAndSize();
1747
1763
  }
1748
1764
  /** Top left corner of the rectangle. */
@@ -1785,14 +1801,15 @@ class Rect {
1785
1801
  * Creates a rectangle from an SVG rectangle element.
1786
1802
  * @param rect {@link SVGRectElement} or {@link SVGImageElement}
1787
1803
  * @param rotation Optional rotation of the rectangle. In radians, around center. Positive values rotate clockwise.
1804
+ * @param elevation Optional elevation. Positive values raise geometry toward the viewer. Defaults to 0.
1788
1805
  * @returns new {@link Rect} instance
1789
1806
  */
1790
- static fromSvg(rect, rotation) {
1807
+ static fromSvg(rect, rotation, elevation) {
1791
1808
  const x = rect.x.baseVal.value;
1792
1809
  const y = rect.y.baseVal.value;
1793
1810
  const width = rect.width.baseVal.value;
1794
1811
  const height = rect.height.baseVal.value;
1795
- return new Rect([x, y], [x + width, y + height], rotation);
1812
+ return new Rect([x, y], [x + width, y + height], rotation, elevation);
1796
1813
  }
1797
1814
  /**
1798
1815
  * Moves the rectangle by the given offset.
@@ -1848,7 +1865,11 @@ class Polygon {
1848
1865
  get indices() {
1849
1866
  return this._indices;
1850
1867
  }
1851
- /** Bounding rectangle of the polygon. */
1868
+ /**
1869
+ * Bounding rectangle of the polygon.
1870
+ * This is a 2D axis-aligned bounding box computed from the X/Y projection of vertices.
1871
+ * The Z extent of the polygon is not tracked.
1872
+ */
1852
1873
  get bounds() {
1853
1874
  return this._bbox;
1854
1875
  }
@@ -1858,11 +1879,12 @@ class Polygon {
1858
1879
  * @returns new {@link Polygon} instance
1859
1880
  */
1860
1881
  static fromRect(rect) {
1882
+ const z = rect.elevation;
1861
1883
  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 }
1884
+ { x: rect.min.x, y: rect.min.y, z },
1885
+ { x: rect.max.x, y: rect.min.y, z },
1886
+ { x: rect.max.x, y: rect.max.y, z },
1887
+ { x: rect.min.x, y: rect.max.y, z }
1866
1888
  ];
1867
1889
  const indices = [
1868
1890
  [0, 1, 2],
@@ -1914,7 +1936,8 @@ class Polygon {
1914
1936
  }
1915
1937
  /**
1916
1938
  * 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.
1939
+ * @param scaleFactor Can be a single number or a 2D vector. If a single number is provided, both horizontal and
1940
+ * vertical axes will be scaled by the same factor. Z is unchanged in both cases.
1918
1941
  * @param origin Origin of the scaling. If omitted, defaults to the bounding rectangle center.
1919
1942
  * @returns this {@link Polygon} instance
1920
1943
  */
@@ -1942,6 +1965,31 @@ class Polygon {
1942
1965
  }
1943
1966
  }
1944
1967
  const tempVector2 = new Vector2();
1968
+ function groupBy(list, keyGetter) {
1969
+ const map = /* @__PURE__ */ new Map();
1970
+ list.forEach((item) => {
1971
+ const key = keyGetter(item);
1972
+ const collection = map.get(key);
1973
+ if (!collection) {
1974
+ map.set(key, [item]);
1975
+ } else {
1976
+ collection.push(item);
1977
+ }
1978
+ });
1979
+ return map;
1980
+ }
1981
+ function partition(list, pred) {
1982
+ const truthy = [];
1983
+ const falsy = [];
1984
+ for (const item of list) {
1985
+ if (pred(item)) {
1986
+ truthy.push(item);
1987
+ } else {
1988
+ falsy.push(item);
1989
+ }
1990
+ }
1991
+ return [truthy, falsy];
1992
+ }
1945
1993
  function countGeometry(geometry) {
1946
1994
  var _a2;
1947
1995
  if (geometry.index == null) return { vertices: geometry.getAttribute("position").count, indices: 0 };
@@ -1950,6 +1998,18 @@ function countGeometry(geometry) {
1950
1998
  indices: ((_a2 = geometry.index) == null ? void 0 : _a2.count) ?? 0
1951
1999
  };
1952
2000
  }
2001
+ const sphereCenter = new Vector3();
2002
+ function computeBoundingSphere(bounds, origin2, out = new Sphere()) {
2003
+ const ox = (origin2 == null ? void 0 : origin2[0]) ?? 0.5;
2004
+ const oy = (origin2 == null ? void 0 : origin2[1]) ?? 0.5;
2005
+ const w = bounds.size.x;
2006
+ const h = bounds.size.y;
2007
+ const dx = Math.max(ox, 1 - ox) * w;
2008
+ const dy = Math.max(oy, 1 - oy) * h;
2009
+ sphereCenter.set(bounds.center.x, bounds.center.y, bounds.elevation);
2010
+ out.set(sphereCenter, Math.hypot(dx, dy));
2011
+ return out;
2012
+ }
1953
2013
  const logger$a = createLogger("mesh");
1954
2014
  extend([namesPlugin]);
1955
2015
  class MeshSystem extends RenderableSystem {
@@ -1971,6 +2031,7 @@ class MeshSystem extends RenderableSystem {
1971
2031
  this.rectGeometry.deleteAttribute("uv");
1972
2032
  }
1973
2033
  buildLayer(layer) {
2034
+ const is3D = layer.mode === "3D";
1974
2035
  const shapes = layer.children;
1975
2036
  const mapShapeToNormColor = /* @__PURE__ */ new Map();
1976
2037
  for (const shapeDef of shapes) {
@@ -1987,21 +2048,15 @@ class MeshSystem extends RenderableSystem {
1987
2048
  const transparentShapesGrouped = groupBy(transparentShapes, (shapeDef) => mapShapeToNormColor.get(shapeDef).a);
1988
2049
  const group = new Group();
1989
2050
  for (const [opacity, shapes2] of transparentShapesGrouped) {
1990
- const transparentMesh = this.buildBatchedMesh(shapes2, opacity);
2051
+ const transparentMesh = this.buildBatchedMesh(shapes2, is3D, opacity);
1991
2052
  transparentMesh.name = "transparent";
1992
2053
  group.add(transparentMesh);
1993
2054
  }
1994
2055
  if (opaqueShapes.length) {
1995
- const opaqueMesh = this.buildBatchedMesh(opaqueShapes);
2056
+ const opaqueMesh = this.buildBatchedMesh(opaqueShapes, is3D);
1996
2057
  opaqueMesh.name = "opaque";
1997
2058
  group.add(opaqueMesh);
1998
2059
  }
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
2060
  return group;
2006
2061
  }
2007
2062
  updateDefImpl(shapeDef, mesh, instanceIds, firstUpdate) {
@@ -2040,13 +2095,13 @@ class MeshSystem extends RenderableSystem {
2040
2095
  mesh.setColorAt(instanceId, this.color.setRGB(color.r / 255, color.g / 255, color.b / 255, SRGBColorSpace));
2041
2096
  }
2042
2097
  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);
2098
+ this.position.set(shape.center.x, shape.center.y, shape.elevation);
2099
+ this.rotation.setFromAxisAngle(new Vector3(0, 0, 1), shape.rotation);
2045
2100
  this.scale.set(shape.size.x, shape.size.y, 1);
2046
2101
  this.matrix.compose(this.position, this.rotation, this.scale);
2047
2102
  mesh.setMatrixAt(instanceId, this.matrix);
2048
2103
  }
2049
- buildBatchedMesh(shapes, opacity = 1) {
2104
+ buildBatchedMesh(shapes, is3D, opacity = 1) {
2050
2105
  let vertexCount = 0;
2051
2106
  let indexCount = 0;
2052
2107
  let rectAdded = false;
@@ -2065,7 +2120,7 @@ class MeshSystem extends RenderableSystem {
2065
2120
  vertexCount += vertices;
2066
2121
  indexCount += indices;
2067
2122
  }
2068
- const material = this.materialSystem.createColorMaterial({ opacity });
2123
+ const material = this.materialSystem.createColorMaterial({ opacity, is3D });
2069
2124
  const batchedMesh = new BatchedMesh(shapes.length, vertexCount, indexCount, material);
2070
2125
  const rectGeometryId = rectAdded ? batchedMesh.addGeometry(this.rectGeometry) : void 0;
2071
2126
  batchedMesh.setCustomSort((list) => this.sortInstances(batchedMesh, list));
@@ -2201,9 +2256,10 @@ class TextSystem extends RenderableSystem {
2201
2256
  }
2202
2257
  }
2203
2258
  buildBatchedText(layer) {
2259
+ const is3D = layer.mode === "3D";
2204
2260
  const textDefs = layer.children;
2205
2261
  const batchedText = new BatchedText();
2206
- batchedText.material = this.materialSystem.createColorMaterial();
2262
+ batchedText.material = this.materialSystem.createColorMaterial({ is3D });
2207
2263
  const mappingData = [];
2208
2264
  let instanceId = 0;
2209
2265
  for (const textDef of textDefs) {
@@ -2247,7 +2303,7 @@ class TextSystem extends RenderableSystem {
2247
2303
  this.worldPosition.copy(this.localPosition).rotateAround({ x: 0, y: 0 }, textDef.bounds.rotation).add(textDef.bounds.center);
2248
2304
  this.textScale.copy(this.initialTextScale).multiplyScalar(fontSize * dpr);
2249
2305
  text.scale.set(this.textScale.x, this.textScale.y, 1);
2250
- text.position.set(this.worldPosition.x, this.worldPosition.y, 0);
2306
+ text.position.set(this.worldPosition.x, this.worldPosition.y, textDef.bounds.elevation);
2251
2307
  text.rotation.set(0, 0, textDef.bounds.rotation);
2252
2308
  this.calculateClipRect(text, textDef, this.localPosition, this.textScale, this.localToMin, this.localToMax);
2253
2309
  this.localPosition.y += height * dpr;
@@ -2482,22 +2538,24 @@ class LayerSystem {
2482
2538
  * @returns sorted leaf layers for debug logging
2483
2539
  */
2484
2540
  initRenderOrder(rootLayer) {
2485
- const leafLayers = [];
2486
- const stack = [rootLayer];
2541
+ const twoDLayers = [];
2542
+ const threeDLayers = [];
2543
+ const stack = [
2544
+ { layer: rootLayer, inheritedMode: rootLayer.mode ?? "2D" }
2545
+ ];
2487
2546
  while (stack.length) {
2488
- const layer = stack.pop();
2547
+ const { layer, inheritedMode } = stack.pop();
2489
2548
  if (isLayerLayer(layer)) {
2490
- const children = [...layer.children];
2491
- children.reverse();
2492
- for (const child of children) {
2549
+ for (let i = layer.children.length - 1; i >= 0; i--) {
2550
+ const child = layer.children[i];
2493
2551
  this.mapLayerDefToParent.set(child, layer);
2494
- stack.push(child);
2552
+ stack.push({ layer: child, inheritedMode: child.mode ?? inheritedMode });
2495
2553
  }
2496
2554
  } else {
2497
- leafLayers.push(layer);
2555
+ layer.mode ?? (layer.mode = inheritedMode);
2556
+ (layer.mode === "3D" ? threeDLayers : twoDLayers).push(layer);
2498
2557
  }
2499
2558
  }
2500
- const [threeDLayers, twoDLayers] = partition(leafLayers, (layer) => layer.mode === "3d");
2501
2559
  const sorted = [...twoDLayers, ...threeDLayers];
2502
2560
  for (let i = 0; i < sorted.length; i++) {
2503
2561
  this.renderOrderMap.set(sorted[i], i + 1);
@@ -2612,10 +2670,12 @@ class ExternalCameraSystem {
2612
2670
  }
2613
2671
  }
2614
2672
  const originalProjectionMatrix = new Matrix4();
2673
+ const patchedMeshes = /* @__PURE__ */ new WeakSet();
2615
2674
  function patchMatricesInExternalMode(scene) {
2616
2675
  scene.traverse((child) => {
2617
2676
  const mesh = child;
2618
- if (!mesh.isMesh) return;
2677
+ if (!mesh.isMesh || patchedMeshes.has(mesh)) return;
2678
+ patchedMeshes.add(mesh);
2619
2679
  const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
2620
2680
  for (const material of materials) {
2621
2681
  const onBeforeRender = material.onBeforeRender.bind(material);
@@ -3011,12 +3071,15 @@ class CoordinatesSystem {
3011
3071
  return out;
3012
3072
  }
3013
3073
  /**
3014
- * Converts a point from screen coordinates to world space.
3074
+ * Converts a point from NDC to world space by intersecting the ground plane (z=0).
3075
+ * Returns 2D world coordinates (z is always 0). This is appropriate for coordinate
3076
+ * conversions that don't need to account for 3D geometry (e.g. canvasToSvg, camera controls).
3077
+ * For pointer events that should land on 3D geometry, use scene raycasting instead.
3015
3078
  * @param ndcCoords Point in NDC (normalized device coordinates)
3016
3079
  * @param out Optional output vector
3017
- * @returns Point in world space
3080
+ * @returns Point on the ground plane in world space, or undefined if the ray is parallel to the plane
3018
3081
  */
3019
- ndcToWorld(ndcCoords, out = new Vector3()) {
3082
+ ndcToWorldPlane(ndcCoords, out = new Vector3()) {
3020
3083
  return this.pickingSystem.intersectPlane(ndcCoords, this.cameraSystem.camera, out);
3021
3084
  }
3022
3085
  /**
@@ -3046,7 +3109,7 @@ class CoordinatesSystem {
3046
3109
  const vec2 = new Vector2();
3047
3110
  const vec3 = new Vector3();
3048
3111
  const ndcPoint = this.canvasToNDC(point, vec2);
3049
- const worldPoint = this.ndcToWorld(ndcPoint, vec3);
3112
+ const worldPoint = this.ndcToWorldPlane(ndcPoint, vec3);
3050
3113
  if (!worldPoint) return [];
3051
3114
  const result = [];
3052
3115
  for (const sceneState of this.sceneSystem.sceneStates) {
@@ -5665,6 +5728,9 @@ const subsetOfTHREE = {
5665
5728
  };
5666
5729
  CameraControls.install({ THREE: subsetOfTHREE });
5667
5730
  const logger$4 = createLogger("cameraController");
5731
+ const ELEVATION_PRECISION = 0.1;
5732
+ const ELEVATION_HEADROOM_RATIO = 0.98;
5733
+ const DEPTH_BUFFER_LEVELS = 2 ** 24;
5668
5734
  class CameraController extends CameraControls {
5669
5735
  /**
5670
5736
  * @param camera {@link PerspectiveCamera} instance
@@ -5686,6 +5752,7 @@ class CameraController extends CameraControls {
5686
5752
  three: CameraController.ACTION.NONE
5687
5753
  };
5688
5754
  this.touchCancelListener = () => this.cancel();
5755
+ void this.rotatePolarTo(0, false);
5689
5756
  }
5690
5757
  /**
5691
5758
  * Get bearing angle between current camera orientation and true north (in radians).
@@ -5696,15 +5763,50 @@ class CameraController extends CameraControls {
5696
5763
  }
5697
5764
  update(delta) {
5698
5765
  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 });
5766
+ if (needsUpdate) {
5767
+ this.updateNearPlane();
5768
+ if (delta > 0) {
5769
+ const position = this.camera.position.toArray().map((value) => +value.toFixed(2));
5770
+ const target = this._target.toArray().map((value) => +value.toFixed(2));
5771
+ const { theta, phi, radius } = this._spherical;
5772
+ const spherical = [theta * RAD2DEG, phi * RAD2DEG, radius].map((value) => +value.toFixed(2));
5773
+ const clippingPlanes = [this.camera.near, this.camera.far];
5774
+ logger$4.debug("camera update %O", { position, target, spherical, clippingPlanes });
5775
+ }
5705
5776
  }
5706
5777
  return needsUpdate;
5707
5778
  }
5779
+ /**
5780
+ * Updates the near clipping plane to maintain consistent depth buffer precision
5781
+ * at the scene plane regardless of zoom level.
5782
+ *
5783
+ * Two constraints determine the near plane:
5784
+ * - **Headroom**: near = D × (1 − headroom ratio). Dominates at close/default zoom,
5785
+ * keeping headroom at exactly the target ratio. Precision exceeds the target.
5786
+ * - **Precision**: near = D² / (2²⁴ × P). Dominates at far zoom, maintaining target
5787
+ * precision at the cost of reduced headroom.
5788
+ *
5789
+ * The larger of the two is used (precision is never sacrificed for headroom).
5790
+ * The depth precision P is derived from {@link ELEVATION_PRECISION} and the current
5791
+ * {@link CameraControls.maxPolarAngle}, so it adapts when pitch limits change.
5792
+ *
5793
+ * Depth precision at the floor plane (z=0) is the guaranteed minimum. Content above
5794
+ * (closer to camera) has quadratically better precision: P × (d/D)². Content below the
5795
+ * floor degrades quadratically but remains within ~1% for practical depths (< 1% of D).
5796
+ */
5797
+ updateNearPlane() {
5798
+ const camera = this.camera;
5799
+ const D = this.distance;
5800
+ if (D <= 0) return;
5801
+ const cosPitch = Math.max(Math.cos(this.maxPolarAngle), 0.01);
5802
+ const depthPrecision = ELEVATION_PRECISION * cosPitch;
5803
+ const headroomNear = D * (1 - ELEVATION_HEADROOM_RATIO);
5804
+ const precisionNear = D * D / (DEPTH_BUFFER_LEVELS * depthPrecision);
5805
+ const near = Math.max(headroomNear, precisionNear);
5806
+ if (camera.near === near) return;
5807
+ camera.near = near;
5808
+ camera.updateProjectionMatrix();
5809
+ }
5708
5810
  connect(domElement) {
5709
5811
  super.connect(domElement);
5710
5812
  domElement.addEventListener("touchcancel", this.touchCancelListener);
@@ -6074,7 +6176,7 @@ class RollHandler extends Handler {
6074
6176
  }
6075
6177
  setPivot(e) {
6076
6178
  this.coordinatesSystem.canvasToNDC(e.offsetCenter, this.pivotNDC);
6077
- if (!this.coordinatesSystem.ndcToWorld(this.pivotNDC, this.pivotWorld)) return false;
6179
+ if (!this.coordinatesSystem.ndcToWorldPlane(this.pivotNDC, this.pivotWorld)) return false;
6078
6180
  this.controller.getPosition(this.cameraPosition);
6079
6181
  this.controller.getTarget(this.targetWorld);
6080
6182
  this.cameraForward.copy(this.targetWorld).sub(this.cameraPosition);
@@ -6160,7 +6262,7 @@ class InteractionsSystem {
6160
6262
  if (internalCameraSystem) {
6161
6263
  this.eventManager = new EventManager(this.canvas, {
6162
6264
  recognizers: [Rotate, [Pan, { event: "pitch", pointers: 2 }, "rotate"]]
6163
- // FIXME: Double click to zoom
6265
+ // TODO: Double click to zoom
6164
6266
  });
6165
6267
  this.handlers = {
6166
6268
  pan: new PanHandler(this.canvas, this.eventManager, internalCameraSystem, coordinatesSystem),
@@ -6188,8 +6290,6 @@ class InteractionsSystem {
6188
6290
  });
6189
6291
  this.handlers.pan.enable();
6190
6292
  this.handlers.zoom.enable();
6191
- this.handlers.roll.enable();
6192
- this.handlers.pitch.enable();
6193
6293
  }
6194
6294
  }
6195
6295
  /**
@@ -6251,15 +6351,18 @@ class InteractionsSystem {
6251
6351
  if (isDragging || !hasListeners) return;
6252
6352
  const mousePointer = eventToCanvas(event);
6253
6353
  coordinatesSystem.canvasToNDC(mousePointer, this.mousePointerNDC);
6254
- if (!coordinatesSystem.ndcToWorld(this.mousePointerNDC, this.mousePointerWorld)) return;
6255
- const intersectionsByScene = this.viewportAccess.getIntersectedObjectsByScene(this.mousePointerNDC);
6354
+ const raycastResults = this.viewportAccess.raycastByScene(this.mousePointerNDC);
6256
6355
  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);
6356
+ for (const { sceneId, intersections } of raycastResults) {
6357
+ const interactive = intersections.filter((i) => isInteractive(i.object));
6358
+ const closestHit = interactive[0] ?? intersections[0];
6359
+ const worldPoint = closestHit ? this.mousePointerWorld.copy(closestHit.point) : coordinatesSystem.ndcToWorldPlane(this.mousePointerNDC, this.mousePointerWorld);
6360
+ if (!worldPoint) continue;
6361
+ const point = coordinatesSystem.worldToModel(worldPoint, this.mousePointerModel, sceneId);
6362
+ const defs = this.layerSystem.getIntersectedDefs(interactive);
6260
6363
  data.push({ point: { x: point.x, y: point.y }, defs, sceneId });
6261
6364
  }
6262
- this.events.emit(eventType, { event, data });
6365
+ if (data.length > 0) this.events.emit(eventType, { event, data });
6263
6366
  };
6264
6367
  const mousemove = (event) => sharedMouseHandler("mousemove", event);
6265
6368
  const mouseout = (event) => sharedMouseHandler("mouseout", event);
@@ -6295,7 +6398,7 @@ class UpdatesSystem {
6295
6398
  constructor(ctx, layerSystem) {
6296
6399
  __publicField(this, "pendingDefs", /* @__PURE__ */ new Set());
6297
6400
  __publicField(this, "culledDefs", /* @__PURE__ */ new Set());
6298
- __publicField(this, "defBounds", new Box3());
6401
+ __publicField(this, "defSphere", new Sphere());
6299
6402
  __publicField(this, "useUpdateBuffering", true);
6300
6403
  this.ctx = ctx;
6301
6404
  this.layerSystem = layerSystem;
@@ -6336,10 +6439,9 @@ class UpdatesSystem {
6336
6439
  let culled = false;
6337
6440
  this.pendingDefs.delete(def);
6338
6441
  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);
6442
+ const origin2 = isImageDef(def) ? def.origin : void 0;
6443
+ computeBoundingSphere(def.bounds, origin2, this.defSphere);
6444
+ culled = !frustum2.intersectsSphere(this.defSphere);
6343
6445
  }
6344
6446
  if (culled) this.culledDefs.add(def);
6345
6447
  else {
@@ -6384,8 +6486,8 @@ class InternalCameraSystem {
6384
6486
  const h = ctx.getDrawingBufferSizePx()[1];
6385
6487
  this.prevViewportHeightPx = h;
6386
6488
  this.camera = new PerspectiveCamera(this.defaultFov);
6489
+ this.camera.up.set(0, 0, -1);
6387
6490
  this.controller = new CameraController(this.camera);
6388
- void this.controller.rotatePolarTo(0, false);
6389
6491
  }
6390
6492
  /** Current camera zoom factor. */
6391
6493
  get zoomFactor() {
@@ -6418,12 +6520,8 @@ class InternalCameraSystem {
6418
6520
  initCamera(viewbox) {
6419
6521
  this.zoomBounds = [0.1, viewbox.size.x > 1e4 ? 100 : 35];
6420
6522
  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);
6523
+ this.controller.update(0);
6424
6524
  this.camera.updateMatrixWorld();
6425
- this.camera.up.set(0, 0, -1);
6426
- this.controller.updateCameraUp();
6427
6525
  }
6428
6526
  /** Updates the camera when the renderer size changes. */
6429
6527
  updateCamera() {
@@ -6435,8 +6533,7 @@ class InternalCameraSystem {
6435
6533
  const maxDistance = Math.abs(newZoomIdentity / this.zoomBounds[0]);
6436
6534
  const minDistance = Math.abs(newZoomIdentity / this.zoomBounds[1]);
6437
6535
  this.camera.aspect = w / (h || 1);
6438
- this.camera.near = 0.01;
6439
- this.camera.far = Math.max(maxDistance, this.camera.near) * 2;
6536
+ this.camera.far = Math.max(maxDistance, 0.1) * 2;
6440
6537
  this.camera.updateProjectionMatrix();
6441
6538
  this.controller.minDistance = minDistance;
6442
6539
  this.controller.maxDistance = maxDistance;
@@ -6471,21 +6568,21 @@ class InternalCameraSystem {
6471
6568
  }
6472
6569
  }
6473
6570
  class PickingSystem {
6474
- /** */
6475
6571
  constructor() {
6476
6572
  __publicField(this, "raycaster", new Raycaster());
6477
6573
  __publicField(this, "ndcPoint", new Vector2());
6478
6574
  __publicField(this, "viewboxPlane", new Plane(new Vector3(0, 0, 1), 0));
6479
- this.raycaster.layers.set(INTERACTIVE_LAYER);
6480
6575
  }
6481
6576
  /**
6482
- * Gets the objects intersected by the raycaster.
6577
+ * Raycasts the scene and returns all visible intersections sorted by distance.
6578
+ * Does not filter by interaction layer — consumers are responsible for partitioning
6579
+ * interactive vs non-interactive results using {@link isInteractive}.
6483
6580
  * @param ndcCoords raycast point in NDC (normalized device coordinates)
6484
6581
  * @param scene {@link Scene} instance
6485
6582
  * @param camera {@link Camera} instance
6486
- * @returns Array of {@link Intersection} instances
6583
+ * @returns Array of {@link Intersection} instances, nearest first
6487
6584
  */
6488
- getIntersectedObjects(ndcCoords, scene, camera) {
6585
+ raycast(ndcCoords, scene, camera) {
6489
6586
  this.setRaycasterFromCamera(ndcCoords, camera);
6490
6587
  const intersections = this.raycaster.intersectObject(scene, true);
6491
6588
  return intersections.filter((i) => isVisible(i.object));
@@ -6608,7 +6705,7 @@ class SceneSystem {
6608
6705
  const scaleFactor = Math.min(visibleRectSize.width, visibleRectSize.height);
6609
6706
  const { x: centerX, y: centerY } = viewbox.center;
6610
6707
  this.translationMatrix.makeTranslation(-centerX, -centerY, 0);
6611
- this.scaleMatrix.makeScale(scaleFactor, scaleFactor, 1);
6708
+ this.scaleMatrix.makeScale(scaleFactor, scaleFactor, -1);
6612
6709
  targetMatrix.copy(this.translationMatrix).premultiply(this.scaleMatrix);
6613
6710
  const viewportRectCenter = this.tempVector2.copy(viewportRectPx.center);
6614
6711
  const offset = viewportRectCenter.sub({ x: bufferW / 2, y: bufferH / 2 });
@@ -6680,15 +6777,16 @@ class ViewportSystem {
6680
6777
  this.visibleRectCss = rect;
6681
6778
  }
6682
6779
  /**
6683
- * Get the objects intersected by the raycaster grouped by scene.
6780
+ * Raycasts all loaded scenes and returns all visible intersections per scene.
6781
+ * Results are not filtered by interaction layer — consumers partition as needed.
6684
6782
  * @param ndcCoords raycast point in NDC (normalized device coordinates)
6685
- * @returns Array of {@link IntersectionsWithinScene} instances
6783
+ * @returns Array of {@link SceneRaycastResult} instances
6686
6784
  */
6687
- getIntersectedObjectsByScene(ndcCoords) {
6785
+ raycastByScene(ndcCoords) {
6688
6786
  const result = [];
6689
6787
  for (const sceneState of this.sceneSystem.sceneStates) {
6690
6788
  if (!sceneState.loaded) continue;
6691
- const intersections = this.pickingSystem.getIntersectedObjects(ndcCoords, sceneState.scene, this.camera);
6789
+ const intersections = this.pickingSystem.raycast(ndcCoords, sceneState.scene, this.camera);
6692
6790
  result.push({ intersections, sceneId: sceneState.id });
6693
6791
  }
6694
6792
  return result;
@@ -7046,7 +7144,6 @@ class Renderer {
7046
7144
  } else if (sceneState.scene.children.length === 0) {
7047
7145
  const root = this.layerSystem.buildScene(sceneState.sceneDef);
7048
7146
  scene.add(root);
7049
- if (this.isExternalMode) patchMatricesInExternalMode(scene);
7050
7147
  }
7051
7148
  }
7052
7149
  const justLoaded = loadedChanged && sceneState.loaded;
@@ -7054,6 +7151,7 @@ class Renderer {
7054
7151
  this.viewportSystem.updatePtScale(id);
7055
7152
  const hasDefsUpdated = this.updatesSystem.processPendingUpdates(frustum2, 3);
7056
7153
  const forceRedraw = hasControlsUpdated || hasDefsUpdated || justLoaded || this.isExternalMode || this.ui;
7154
+ if (this.isExternalMode) patchMatricesInExternalMode(scene);
7057
7155
  if (this.needsRedraw || forceRedraw) this.renderer.render(scene, camera);
7058
7156
  if (hasDefsUpdated || this.needsRedraw) this.viewportSystem.invalidateSceneBounds(sceneState);
7059
7157
  }
@@ -7090,8 +7188,7 @@ class Renderer {
7090
7188
  }
7091
7189
  initScene(sceneDef, sceneId) {
7092
7190
  const root = this.layerSystem.buildScene(sceneDef);
7093
- const scene = this.viewportSystem.initScene(sceneDef, sceneId).add(root);
7094
- if (this.isExternalMode) patchMatricesInExternalMode(scene);
7191
+ this.viewportSystem.initScene(sceneDef, sceneId).add(root);
7095
7192
  }
7096
7193
  // https://webgl2fundamentals.org/webgl/lessons/webgl-resizing-the-canvas.html
7097
7194
  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.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"