@expofp/renderer 2.2.1 → 2.3.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
@@ -1,8 +1,6 @@
1
1
  import { default as default_2 } from 'stats-gl';
2
- import { Vector2 } from 'three';
3
2
  import { Vector2Like } from 'three';
4
3
  import { Vector2Tuple } from 'three';
5
- import { Vector3 } from 'three';
6
4
  import { Vector3Like } from 'three';
7
5
  import { Vector3Tuple } from 'three';
8
6
  import { WebGLRenderer } from 'three';
@@ -132,20 +130,6 @@ export declare interface ControlsAPI {
132
130
  getCameraState: () => CameraState;
133
131
  }
134
132
 
135
- /**
136
- * Converts multiple vector2 representations to a {@link Vector2} instance.
137
- * @param vector2 Vector2 representation as a tuple/POJO
138
- * @returns Vector2 instance
139
- */
140
- export declare function createVector2(vector2: IVector2): Vector2;
141
-
142
- /**
143
- * Converts multiple vector3 representations to a {@link Vector3} instance.
144
- * @param vector3 Vector3 representation as a tuple/POJO
145
- * @returns Vector3 instance
146
- */
147
- export declare function createVector3(vector3: IVector3 | IVector2): Vector3;
148
-
149
133
  /**
150
134
  * Mapping of engine-wide events to their payloads
151
135
  */
@@ -691,6 +675,16 @@ export declare interface ViewportAPI {
691
675
  * @param dynamicTransformMatrix dynamic transform matrix (changes every frame)
692
676
  */
693
677
  setDynamicTransform: (dynamicTransformMatrix: number[]) => void;
678
+ /**
679
+ * Set the maximum zoom factor. Default is 35.
680
+ * @param maxZoom Maximum zoom factor
681
+ */
682
+ setMaxZoom: (maxZoom: number) => void;
683
+ /**
684
+ * Set the minimum zoom factor. Default is 0.1.
685
+ * @param minZoom Minimum zoom factor
686
+ */
687
+ setMinZoom: (minZoom: number) => void;
694
688
  }
695
689
 
696
690
  /** Options for the {@link ControlsAPI.zoomTo} method. */
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, Vector4, AlwaysDepth, DoubleSide, MeshBasicMaterial, Texture, Group, PlaneGeometry, SRGBColorSpace, Vector2, Quaternion, BufferGeometry, Mesh, LessEqualDepth, LinearSRGBColorSpace, Plane, Raycaster, Sphere, Box3, Spherical, PerspectiveCamera, Camera, Scene, MathUtils, 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, Texture, Group, PlaneGeometry, SRGBColorSpace, Vector2, Quaternion, BufferGeometry, Mesh, LessEqualDepth, LinearSRGBColorSpace, Plane, Raycaster, Spherical, PerspectiveCamera, Camera, Scene, MathUtils, 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";
@@ -540,20 +540,57 @@ class BatchedMesh extends BatchedMesh$1 {
540
540
  /** Whether to use WebGL_multi_draw extension or less performant fallback */
541
541
  __publicField(BatchedMesh, "useMultiDraw", true);
542
542
  const floatsPerMember = 32;
543
- const tempColor = new Color();
544
543
  const defaultStrokeColor = 8421504;
544
+ const defaultOrient = "+x+y";
545
+ const glyphBoundsAttrName = "aTroikaGlyphBounds";
546
+ const glyphIndexAttrName = "aTroikaGlyphIndex";
547
+ const memberIndexAttrName = "aTroikaTextBatchMemberIndex";
548
+ const syncStartEvent = { type: "syncstart" };
549
+ const syncCompleteEvent = { type: "synccomplete" };
550
+ const SYNCABLE_PROPS = [
551
+ "font",
552
+ "fontSize",
553
+ "fontStyle",
554
+ "fontWeight",
555
+ "lang",
556
+ "letterSpacing",
557
+ "lineHeight",
558
+ "maxWidth",
559
+ "overflowWrap",
560
+ "text",
561
+ "direction",
562
+ "textAlign",
563
+ "textIndent",
564
+ "whiteSpace",
565
+ "anchorX",
566
+ "anchorY",
567
+ "colorRanges",
568
+ "sdfGlyphSize"
569
+ ];
570
+ const tempColor = new Color();
545
571
  const tempMat4 = new Matrix4();
546
572
  const tempVec3a = new Vector3();
547
573
  const tempVec3b = new Vector3();
548
574
  const origin = new Vector3();
549
- const defaultOrient = "+x+y";
575
+ const frustum = new Frustum();
576
+ const sphere = new Sphere();
577
+ const box = new Box3();
550
578
  class BatchedText extends BatchedText$1 {
551
579
  // eslint-disable-next-line jsdoc/require-jsdoc
552
580
  constructor() {
553
581
  super();
554
- __publicField(this, "mapInstanceIdToText", /* @__PURE__ */ new Map());
582
+ __publicField(this, "_needsRepack", false);
555
583
  __publicField(this, "textArray", []);
556
584
  __publicField(this, "textureNeedsUpdate", false);
585
+ __publicField(this, "boundsNeedsUpdate", false);
586
+ __publicField(this, "batchNeedsSync", true);
587
+ this.addEventListener("synccomplete", () => {
588
+ this.boundsNeedsUpdate = true;
589
+ });
590
+ }
591
+ /** Mark the batch as needing a sync pass. */
592
+ requestSync() {
593
+ this.batchNeedsSync = true;
557
594
  }
558
595
  /** Number of texts in the batch */
559
596
  get size() {
@@ -569,7 +606,13 @@ class BatchedText extends BatchedText$1 {
569
606
  * @returns Text object
570
607
  */
571
608
  getText(instanceId) {
572
- return this.mapInstanceIdToText.get(instanceId);
609
+ return this.textArray[instanceId];
610
+ }
611
+ /**
612
+ * Mark this BatchedText as needing bounds update. This should be called when changing text transforms.
613
+ */
614
+ invalidateBounds() {
615
+ this.boundsNeedsUpdate = true;
573
616
  }
574
617
  /**
575
618
  * Set the visibility of the {@link Text} object by instance id.
@@ -583,17 +626,112 @@ class BatchedText extends BatchedText$1 {
583
626
  }
584
627
  addText(text, instanceId) {
585
628
  super.addText(text);
586
- if (instanceId !== void 0) {
587
- this.mapInstanceIdToText.set(instanceId, text);
588
- }
589
- this.textArray.push(text);
629
+ text._batchParent = this;
630
+ if (instanceId !== void 0) this.textArray[instanceId] = text;
631
+ this.batchNeedsSync = true;
632
+ this.boundsNeedsUpdate = true;
633
+ }
634
+ removeText(text) {
635
+ super.removeText(text);
636
+ if (text._batchParent === this) text._batchParent = void 0;
637
+ this.textArray.splice(this.textArray.indexOf(text), 1);
638
+ this.batchNeedsSync = true;
639
+ this.boundsNeedsUpdate = true;
590
640
  }
591
641
  dispose() {
592
642
  super.dispose();
643
+ this.textArray.length = 0;
593
644
  this.dispatchEvent({ type: "dispose" });
594
645
  }
595
- // TODO: Check performance
596
- _prepareForRender(material) {
646
+ updateBounds() {
647
+ if (this.boundsNeedsUpdate) {
648
+ const bbox = this.geometry.boundingBox.makeEmpty();
649
+ for (const text of this.textArray) {
650
+ if (!text.visible) continue;
651
+ if (text.matrixAutoUpdate) text.updateMatrix();
652
+ box.copy(text.geometry.boundingBox).applyMatrix4(text.matrix);
653
+ bbox.union(box);
654
+ }
655
+ bbox.getBoundingSphere(this.geometry.boundingSphere);
656
+ this.boundsNeedsUpdate = false;
657
+ }
658
+ }
659
+ onBeforeRender(renderer, scene, camera, geometry, material) {
660
+ if (this.batchNeedsSync) this.sync();
661
+ if ("isTroikaTextMaterial" in material && material.isTroikaTextMaterial) {
662
+ this.prepareForRender(material, scene, camera);
663
+ }
664
+ }
665
+ /**
666
+ * Sync members and repack the batched geometry if needed.
667
+ * @param callback Optional callback invoked after sync completes.
668
+ */
669
+ sync(callback) {
670
+ if (!this.batchNeedsSync) return;
671
+ this.batchNeedsSync = false;
672
+ let needsRepack = this._needsRepack;
673
+ this._needsRepack = false;
674
+ const pendingSyncs = [];
675
+ this._members.forEach((packingInfo, text) => {
676
+ const needsMemberSync = text._needsSync;
677
+ if (packingInfo.dirty || needsMemberSync) {
678
+ packingInfo.dirty = false;
679
+ needsRepack = true;
680
+ if (needsMemberSync) pendingSyncs.push(new Promise((resolve) => text.sync(resolve)));
681
+ }
682
+ });
683
+ if (!needsRepack) return;
684
+ this.dispatchEvent(syncStartEvent);
685
+ const repack = () => {
686
+ this.repackBatchedGeometry();
687
+ this.dispatchEvent(syncCompleteEvent);
688
+ if (callback) callback();
689
+ };
690
+ if (pendingSyncs.length) {
691
+ void Promise.all(pendingSyncs).then(repack);
692
+ } else {
693
+ repack();
694
+ }
695
+ }
696
+ repackBatchedGeometry() {
697
+ var _a2, _b, _c;
698
+ const geometry = this.geometry;
699
+ const batchedAttributes = geometry.attributes;
700
+ let memberIndexes = ((_a2 = batchedAttributes[memberIndexAttrName]) == null ? void 0 : _a2.array) ?? new Uint16Array(0);
701
+ let batchedGlyphIndexes = ((_b = batchedAttributes[glyphIndexAttrName]) == null ? void 0 : _b.array) ?? new Float32Array(0);
702
+ let batchedGlyphBounds = ((_c = batchedAttributes[glyphBoundsAttrName]) == null ? void 0 : _c.array) ?? new Float32Array(0);
703
+ let totalGlyphCount = 0;
704
+ this._members.forEach((_, { textRenderInfo }) => {
705
+ if (textRenderInfo) {
706
+ totalGlyphCount += textRenderInfo.glyphAtlasIndices.length;
707
+ this._textRenderInfo = textRenderInfo;
708
+ }
709
+ });
710
+ if (totalGlyphCount !== memberIndexes.length) {
711
+ memberIndexes = cloneAndResize(memberIndexes, totalGlyphCount);
712
+ batchedGlyphIndexes = cloneAndResize(batchedGlyphIndexes, totalGlyphCount);
713
+ batchedGlyphBounds = cloneAndResize(batchedGlyphBounds, totalGlyphCount * 4);
714
+ }
715
+ let memberIndex = 0;
716
+ let glyphIndex = 0;
717
+ this._members.forEach((packingInfo, { textRenderInfo }) => {
718
+ if (textRenderInfo) {
719
+ const glyphCount = textRenderInfo.glyphAtlasIndices.length;
720
+ memberIndexes.fill(memberIndex, glyphIndex, glyphIndex + glyphCount);
721
+ batchedGlyphIndexes.set(textRenderInfo.glyphAtlasIndices, glyphIndex);
722
+ batchedGlyphBounds.set(textRenderInfo.glyphBounds, glyphIndex * 4);
723
+ glyphIndex += glyphCount;
724
+ packingInfo.index = memberIndex++;
725
+ }
726
+ });
727
+ geometry.updateAttributeData(memberIndexAttrName, memberIndexes, 1);
728
+ geometry.getAttribute(memberIndexAttrName).setUsage(DynamicDrawUsage);
729
+ geometry.updateAttributeData(glyphIndexAttrName, batchedGlyphIndexes, 1);
730
+ geometry.updateAttributeData(glyphBoundsAttrName, batchedGlyphBounds, 4);
731
+ this.boundsNeedsUpdate = true;
732
+ this.updateBounds();
733
+ }
734
+ prepareForRender(material, scene, camera) {
597
735
  var _a2;
598
736
  const isOutline = material.isTextOutlineMaterial;
599
737
  material.uniforms.uTroikaIsOutline.value = isOutline;
@@ -612,12 +750,15 @@ class BatchedText extends BatchedText$1 {
612
750
  }
613
751
  const texData = texture.image.data;
614
752
  this.textureNeedsUpdate = false;
753
+ tempMat4.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse).multiply(scene.matrixWorld);
754
+ frustum.setFromProjectionMatrix(tempMat4, camera.coordinateSystem);
615
755
  for (const text of this.textArray) {
616
756
  const index = ((_a2 = this._members.get(text)) == null ? void 0 : _a2.index) ?? -1;
617
757
  const textRenderInfo = text.textRenderInfo;
618
758
  if (index < 0 || !textRenderInfo) continue;
619
759
  const startIndex = index * floatsPerMember;
620
- if (!text.visible) {
760
+ const isHidden = !text.visible || !frustum.intersectsSphere(sphere.copy(text.geometry.boundingSphere).applyMatrix4(text.matrix));
761
+ if (isHidden) {
621
762
  for (let i = 0; i < 16; i++) {
622
763
  this.setTexData(startIndex + i, 0, texData);
623
764
  }
@@ -675,6 +816,11 @@ class BatchedText extends BatchedText$1 {
675
816
  }
676
817
  }
677
818
  class Text extends Text$1 {
819
+ constructor() {
820
+ super(...arguments);
821
+ /** Parent batch for sync invalidation. */
822
+ __publicField(this, "_batchParent");
823
+ }
678
824
  _prepareForRender(material) {
679
825
  const isOutline = material.isTextOutlineMaterial;
680
826
  const uniforms = material.uniforms;
@@ -732,7 +878,7 @@ class Text extends Text$1 {
732
878
  blockBounds[3] + pad
733
879
  );
734
880
  }
735
- this.geometry.applyClipRect(uniforms.uTroikaClipRect.value);
881
+ if (!this._batchParent) this.geometry.applyClipRect(uniforms.uTroikaClipRect.value);
736
882
  }
737
883
  uniforms.uTroikaSDFDebug.value = !!this.debugSDF;
738
884
  material.polygonOffset = !!this.depthOffset;
@@ -764,6 +910,29 @@ class Text extends Text$1 {
764
910
  }
765
911
  }
766
912
  }
913
+ SYNCABLE_PROPS.forEach((prop) => {
914
+ const privateKey = `_private_${prop}`;
915
+ Object.defineProperty(Text.prototype, prop, {
916
+ get() {
917
+ return this[privateKey];
918
+ },
919
+ set(value) {
920
+ var _a2;
921
+ const target = this;
922
+ if (value !== target[privateKey]) {
923
+ target[privateKey] = value;
924
+ target._needsSync = true;
925
+ (_a2 = target._batchParent) == null ? void 0 : _a2.requestSync();
926
+ }
927
+ }
928
+ });
929
+ });
930
+ function cloneAndResize(source, newLength) {
931
+ const constructor = source.constructor;
932
+ const copy = new constructor(newLength);
933
+ copy.set(source.subarray(0, newLength));
934
+ return copy;
935
+ }
767
936
  function setDimming(root, dim) {
768
937
  root.userData["uDim"] = dim === void 0 ? void 0 : +dim;
769
938
  }
@@ -2001,19 +2170,30 @@ class TextSystem extends RenderableSystem {
2001
2170
  group.add(newBatchedText);
2002
2171
  }
2003
2172
  updateDefImpl(textDef, mesh, instanceIds) {
2004
- for (const [i, line] of textDef.lines.entries()) {
2173
+ const lines = [];
2174
+ for (let i = 0; i < textDef.lines.length; i++) {
2175
+ const line = textDef.lines[i];
2005
2176
  const text = mesh.getText(instanceIds[i]);
2006
- if (text) {
2007
- text.text = line.text;
2008
- text.color = this.textColor.set(line.color).getHex(LinearSRGBColorSpace);
2009
- text.font = line.fontUrl;
2010
- if (line.stroke) {
2011
- text.outlineColor = line.stroke.color;
2012
- text.outlineWidth = line.stroke.width / line.fontSize;
2013
- }
2177
+ if (line.fontSize === 0) {
2178
+ text.visible = false;
2179
+ continue;
2014
2180
  }
2181
+ text.visible = true;
2182
+ text.text = line.text;
2183
+ text.color = this.textColor.set(line.color).getHex(LinearSRGBColorSpace);
2184
+ text.font = line.fontUrl;
2185
+ if (line.stroke) {
2186
+ text.outlineColor = line.stroke.color;
2187
+ text.outlineWidth = line.fontSize == 0 ? 0 : line.stroke.width / line.fontSize;
2188
+ }
2189
+ const fontSize = line.fontSize;
2190
+ const height = line.text.split("\n").length * text.lineHeight * fontSize;
2191
+ lines.push({ text, fontSize, height });
2192
+ }
2193
+ if (lines.length > 0) {
2194
+ if (textDef.alignment.vertical === "bottom") lines.reverse();
2195
+ this.updateScale(textDef, mesh, lines);
2015
2196
  }
2016
- this.updateScale(textDef);
2017
2197
  }
2018
2198
  buildBatchedText(layer) {
2019
2199
  const textDefs = layer.children;
@@ -2028,6 +2208,7 @@ class TextSystem extends RenderableSystem {
2028
2208
  text.fontSize = 1;
2029
2209
  text.lineHeight = 1.1;
2030
2210
  text.whiteSpace = "nowrap";
2211
+ text.clipRect = [0, 0, 0, 0];
2031
2212
  batchedText.addText(text, instanceId);
2032
2213
  instanceIds.push(instanceId);
2033
2214
  instanceId++;
@@ -2047,9 +2228,8 @@ class TextSystem extends RenderableSystem {
2047
2228
  return batchedText;
2048
2229
  }
2049
2230
  // TODO: Simplify
2050
- updateScale(textDef) {
2231
+ updateScale(textDef, mesh, lines) {
2051
2232
  const dpr = this.renderer.context.getPixelRatio();
2052
- const lines = this.getTextLines(textDef);
2053
2233
  this.calculateStartInBoundsPosition(
2054
2234
  textDef,
2055
2235
  lines,
@@ -2057,41 +2237,18 @@ class TextSystem extends RenderableSystem {
2057
2237
  this.alignmentOffset,
2058
2238
  this.localPosition
2059
2239
  );
2060
- for (const { text, fontSize, height, alignment } of lines) {
2061
- if (!fontSize || !height) {
2062
- text.visible = false;
2063
- continue;
2064
- }
2065
- text.visible = true;
2066
- setAnchorsAndAlignment(text, alignment);
2240
+ for (const { text, fontSize, height } of lines) {
2241
+ setAnchorsAndAlignment(text, textDef.alignment);
2067
2242
  this.worldPosition.copy(this.localPosition).rotateAround({ x: 0, y: 0 }, textDef.bounds.rotation).add(textDef.bounds.center);
2068
2243
  this.textScale.copy(this.initialTextScale).multiplyScalar(fontSize * dpr);
2069
2244
  text.scale.set(this.textScale.x, this.textScale.y, 1);
2070
2245
  text.position.set(this.worldPosition.x, this.worldPosition.y, 0);
2071
2246
  text.rotation.set(0, 0, textDef.bounds.rotation);
2072
- text.clipRect = this.calculateClipRect(
2073
- textDef,
2074
- this.localPosition,
2075
- this.textScale,
2076
- this.localToMin,
2077
- this.localToMax
2078
- );
2247
+ this.calculateClipRect(text, textDef, this.localPosition, this.textScale, this.localToMin, this.localToMax);
2079
2248
  this.localPosition.y += height * dpr;
2249
+ mesh.invalidateBounds();
2080
2250
  }
2081
2251
  }
2082
- getTextLines(textDef) {
2083
- const { object: mesh, instanceIds } = this.getObjectInstanceByDef(textDef);
2084
- const alignment = textDef.alignment;
2085
- const lines = instanceIds.map((instanceId, i) => {
2086
- const text = mesh.getText(instanceId);
2087
- const line = textDef.lines[i];
2088
- const fontSize = line.fontSize;
2089
- const height = fontSize ? text.text.split("\n").length * text.lineHeight * fontSize : 0;
2090
- return { text, fontSize, height, alignment };
2091
- });
2092
- if (alignment.vertical === "bottom") lines.reverse();
2093
- return lines;
2094
- }
2095
2252
  calculateStartInBoundsPosition(textDef, lines, alignmentDirection, alignmentOffset, inBoundsPosition) {
2096
2253
  const padding = textDef.padding;
2097
2254
  const alignment = textDef.alignment;
@@ -2106,10 +2263,13 @@ class TextSystem extends RenderableSystem {
2106
2263
  }
2107
2264
  inBoundsPosition.multiply(alignmentDirection);
2108
2265
  }
2109
- calculateClipRect(textDef, inBoundsPosition, textScale, toMin, toMax) {
2266
+ calculateClipRect(text, textDef, inBoundsPosition, textScale, toMin, toMax) {
2110
2267
  toMin.subVectors(textDef.bounds.min, textDef.bounds.center).multiply(this.initialTextScale).sub(inBoundsPosition).divide(textScale);
2111
2268
  toMax.subVectors(textDef.bounds.max, textDef.bounds.center).multiply(this.initialTextScale).sub(inBoundsPosition).divide(textScale);
2112
- return [toMin.x, toMin.y, toMax.x, toMax.y];
2269
+ text.clipRect[0] = toMin.x;
2270
+ text.clipRect[1] = toMin.y;
2271
+ text.clipRect[2] = toMax.x;
2272
+ text.clipRect[3] = toMax.y;
2113
2273
  }
2114
2274
  }
2115
2275
  function getAlignmentDirection(alignment) {
@@ -2147,7 +2307,8 @@ class LayerSystem {
2147
2307
  __publicField(this, "mapLayerDefToParent", /* @__PURE__ */ new Map());
2148
2308
  __publicField(this, "layerDefRenderOrder", []);
2149
2309
  __publicField(this, "pendingDefs", /* @__PURE__ */ new Set());
2150
- __publicField(this, "useUpdateBuffering", false);
2310
+ __publicField(this, "culledDefs", /* @__PURE__ */ new Set());
2311
+ __publicField(this, "useUpdateBuffering", true);
2151
2312
  this.renderer = renderer;
2152
2313
  this.materialSystem = new MaterialSystem();
2153
2314
  this.meshSystem = new MeshSystem(this.materialSystem, this.renderer);
@@ -2181,30 +2342,57 @@ class LayerSystem {
2181
2342
  */
2182
2343
  updateDefs(defs) {
2183
2344
  for (const def of defs) {
2184
- if (this.useUpdateBuffering) this.pendingDefs.add(def);
2185
- else this.updateDef(def);
2345
+ if (this.useUpdateBuffering) {
2346
+ this.pendingDefs.add(def);
2347
+ this.culledDefs.delete(def);
2348
+ } else this.updateDef(def);
2186
2349
  }
2187
2350
  }
2188
2351
  /**
2189
2352
  * Drain the queued updates within a time budget.
2190
2353
  * Returns true if any def was updated during this call.
2354
+ * @param scene {@link Scene} instance to get the world matrix from
2355
+ * @param camera {@link Camera} instance to do frustum culling against
2191
2356
  * @param timeBudgetMs frame time budget to perform updates in milliseconds
2192
2357
  * @returns true if any def was updated during this call
2193
2358
  */
2194
- processPendingUpdates(timeBudgetMs = 5) {
2359
+ processPendingUpdates(scene, camera, timeBudgetMs = -1) {
2195
2360
  if (!this.useUpdateBuffering) return false;
2196
- if (this.pendingDefs.size === 0) return false;
2361
+ if (this.pendingDefs.size === 0 && this.culledDefs.size === 0) return false;
2197
2362
  const startTime = performance.now();
2198
- let processed = 0;
2199
- while (this.pendingDefs.size && performance.now() - startTime < timeBudgetMs) {
2200
- const def = this.pendingDefs.values().next().value;
2363
+ _matrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse).multiply(scene.matrixWorld);
2364
+ _frustum.setFromProjectionMatrix(_matrix, camera.coordinateSystem);
2365
+ let processedCount = 0;
2366
+ const overBudget = () => timeBudgetMs > 0 && performance.now() - startTime >= timeBudgetMs;
2367
+ for (const def of this.pendingDefs) {
2368
+ if (overBudget()) break;
2369
+ let culled = false;
2201
2370
  this.pendingDefs.delete(def);
2371
+ if (isTextDef(def) || isImageDef(def)) {
2372
+ const bounds = def.bounds;
2373
+ _box.min.set(bounds.min.x, bounds.min.y, 0);
2374
+ _box.max.set(bounds.max.x, bounds.max.y, 0);
2375
+ culled = !_frustum.intersectsBox(_box);
2376
+ }
2377
+ if (culled) this.culledDefs.add(def);
2378
+ else {
2379
+ this.updateDef(def);
2380
+ processedCount++;
2381
+ }
2382
+ }
2383
+ for (const def of this.culledDefs) {
2384
+ if (overBudget()) break;
2385
+ this.culledDefs.delete(def);
2202
2386
  this.updateDef(def);
2203
- processed++;
2387
+ processedCount++;
2204
2388
  }
2205
2389
  const took = (performance.now() - startTime).toFixed(2);
2206
- if (processed) logger$5.debug(`processed ${processed} defs in ${took}ms, ${this.pendingDefs.size} remaining`);
2207
- return processed > 0;
2390
+ if (processedCount > 0) {
2391
+ const culled = this.culledDefs.size;
2392
+ const remaining = this.pendingDefs.size;
2393
+ logger$5.debug(`processed ${processedCount} defs (${culled} culled, ${remaining} remaining) in ${took}ms`);
2394
+ }
2395
+ return processedCount > 0;
2208
2396
  }
2209
2397
  /**
2210
2398
  * Build the scene graph from the given scene definition.
@@ -2337,6 +2525,9 @@ class LayerSystem {
2337
2525
  return fullName;
2338
2526
  }
2339
2527
  }
2528
+ const _matrix = new Matrix4();
2529
+ const _frustum = new Frustum();
2530
+ const _box = new Box3();
2340
2531
  /*!
2341
2532
  * camera-controls
2342
2533
  * https://github.com/yomotsu/camera-controls
@@ -4947,6 +5138,17 @@ class CameraSystem {
4947
5138
  if (zoomFactor <= 0) return this.zoomIdentityDistance;
4948
5139
  return this.zoomIdentityDistance / zoomFactor;
4949
5140
  }
5141
+ /**
5142
+ * Set the zoom factor bounds.
5143
+ * @param minZoom Minimum zoom factor. Default is 0.1.
5144
+ * @param maxZoom Maximum zoom factor. Default is 35.
5145
+ */
5146
+ setZoomBounds(minZoom, maxZoom) {
5147
+ if (!this.zoomBounds) return;
5148
+ if (minZoom) this.zoomBounds[0] = minZoom;
5149
+ if (maxZoom) this.zoomBounds[1] = maxZoom;
5150
+ this.updateCamera();
5151
+ }
4950
5152
  /**
4951
5153
  * Initializes the camera with the given zoom bounds.
4952
5154
  * @param zoomBounds [minZoom, maxZoom]
@@ -5292,7 +5494,7 @@ class ViewportSystem {
5292
5494
  */
5293
5495
  initViewport(sceneDef) {
5294
5496
  if (!this.renderer.isExternalMode) this.sceneSystem.initScene(sceneDef.viewbox);
5295
- this.cameraSystem.initCamera([0.1, sceneDef.viewbox.size.x > 1e5 ? 100 : 35]);
5497
+ this.cameraSystem.initCamera([0.1, sceneDef.viewbox.size.x > 1e4 ? 100 : 35]);
5296
5498
  }
5297
5499
  /** Updates the viewport when the renderer size changes. */
5298
5500
  updateViewport() {
@@ -5397,12 +5599,28 @@ class ViewportSystem {
5397
5599
  zoomFactorToDistance(zoomFactor) {
5398
5600
  return this.cameraSystem.zoomFactorToDistance(zoomFactor);
5399
5601
  }
5602
+ /**
5603
+ * Set the maximum zoom factor. Default is 35.
5604
+ * @param maxZoom Maximum zoom factor
5605
+ */
5606
+ setMaxZoom(maxZoom) {
5607
+ this.cameraSystem.setZoomBounds(void 0, maxZoom);
5608
+ }
5609
+ /**
5610
+ * Set the minimum zoom factor. Default is 0.1.
5611
+ * @param minZoom Minimum zoom factor
5612
+ */
5613
+ setMinZoom(minZoom) {
5614
+ this.cameraSystem.setZoomBounds(minZoom, void 0);
5615
+ }
5400
5616
  }
5401
5617
  function asViewportAPI(viewportSystem) {
5402
5618
  return {
5403
5619
  canvasToSvg: viewportSystem.canvasToSvg.bind(viewportSystem),
5404
5620
  setStaticTransform: viewportSystem.setStaticTransform.bind(viewportSystem),
5405
- setDynamicTransform: viewportSystem.setDynamicTransform.bind(viewportSystem)
5621
+ setDynamicTransform: viewportSystem.setDynamicTransform.bind(viewportSystem),
5622
+ setMaxZoom: viewportSystem.setMaxZoom.bind(viewportSystem),
5623
+ setMinZoom: viewportSystem.setMinZoom.bind(viewportSystem)
5406
5624
  };
5407
5625
  }
5408
5626
  function eventToCanvas(event) {
@@ -6418,10 +6636,13 @@ class Renderer {
6418
6636
  const api = asViewportAPI(this.viewportSystem);
6419
6637
  const guard = (name) => this.assertInitialized(`viewport.${name}`) && this.assertNotDisposed(`viewport.${name}`);
6420
6638
  const guardExternal = (name) => guard(name) && this.assertExternalMode(`viewport.${name}`);
6639
+ const guardNotExternal = (name) => guard(name) && this.assertNotExternalMode(`viewport.${name}`);
6421
6640
  this.viewportAPI = {
6422
6641
  canvasToSvg: guardFn(guard, api.canvasToSvg, { x: 0, y: 0 }),
6423
6642
  setStaticTransform: guardFn(guardExternal, api.setStaticTransform),
6424
- setDynamicTransform: guardFn(guardExternal, api.setDynamicTransform)
6643
+ setDynamicTransform: guardFn(guardExternal, api.setDynamicTransform),
6644
+ setMaxZoom: guardFn(guardNotExternal, api.setMaxZoom),
6645
+ setMinZoom: guardFn(guardNotExternal, api.setMinZoom)
6425
6646
  };
6426
6647
  return this.viewportAPI;
6427
6648
  }
@@ -6521,12 +6742,13 @@ class Renderer {
6521
6742
  (_b = (_a2 = this.ui) == null ? void 0 : _a2.stats) == null ? void 0 : _b.begin();
6522
6743
  if (this.isExternalMode) this.renderer.resetState();
6523
6744
  else this.resizeCanvasToDisplaySize();
6524
- this.viewportSystem.updatePtScale();
6525
- const hasDefsUpdated = this.layerSystem.processPendingUpdates();
6745
+ const { scene, camera } = this.viewportSystem;
6526
6746
  const hasControlsUpdated = this.interactionsSystem.updateControls(this.clock.getDelta());
6747
+ this.viewportSystem.updatePtScale();
6748
+ const hasDefsUpdated = this.layerSystem.processPendingUpdates(scene, camera, 3);
6527
6749
  const needsRedraw = this.needsRedraw || hasControlsUpdated || hasDefsUpdated || this.isExternalMode || this.ui;
6528
6750
  if (needsRedraw) {
6529
- this.renderer.render(this.viewportSystem.scene, this.viewportSystem.camera);
6751
+ this.renderer.render(scene, camera);
6530
6752
  this.needsRedraw = false;
6531
6753
  }
6532
6754
  (_d = (_c = this.ui) == null ? void 0 : _c.stats) == null ? void 0 : _d.end();
@@ -6562,7 +6784,8 @@ class Renderer {
6562
6784
  // https://webgl2fundamentals.org/webgl/lessons/webgl-resizing-the-canvas.html
6563
6785
  resizeCanvasToDisplaySize() {
6564
6786
  const dpr = window.devicePixelRatio;
6565
- const { width, height } = this.canvas.getBoundingClientRect();
6787
+ const width = this.canvas.clientWidth;
6788
+ const height = this.canvas.clientHeight;
6566
6789
  const displayWidth = Math.floor(width * dpr);
6567
6790
  const displayHeight = Math.floor(height * dpr);
6568
6791
  if (this.canvas.width !== displayWidth || this.canvas.height !== displayHeight || this.renderer.getPixelRatio() !== dpr) {
@@ -6670,8 +6893,6 @@ export {
6670
6893
  Polygon,
6671
6894
  Rect,
6672
6895
  Renderer,
6673
- createVector2,
6674
- createVector3,
6675
6896
  isImageDef,
6676
6897
  isImageLayer,
6677
6898
  isLayerDef,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expofp/renderer",
3
- "version": "2.2.1",
3
+ "version": "2.3.1",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"