zincjs 2.4.1-beta.4 → 2.4.1-beta.5

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.
@@ -0,0 +1,211 @@
1
+ import * as THREE from 'three/webgpu';
2
+ import SpriteTextModule from 'three-spritetext';
3
+ import { requestRenderAll } from './renderRequests';
4
+ const SpriteText = SpriteTextModule.default || SpriteTextModule;
5
+
6
+ /*
7
+ * Text sprites (labels, marker numbers) are drawn on a canvas by
8
+ * three-spritetext. To keep the text sharp the canvas resolution is chosen
9
+ * from the size of the text on screen instead of a fixed font size, and it
10
+ * is redrawn once web fonts have finished loading.
11
+ */
12
+
13
+ //Canvas pixels per screen pixel, a little oversampling smooths the edges
14
+ const OVERSAMPLE = 1.5;
15
+ const MIN_FONT_SIZE = 16;
16
+ const MAX_FONT_SIZE = 256;
17
+ //Only redraw when the screen scale has changed by more than this ratio
18
+ const RESCALE_THRESHOLD = 0.1;
19
+
20
+ /*
21
+ * Screen pixels per unit of sprite height. Text sprites do not use size
22
+ * attenuation, their height is in view units at a distance of one, so this
23
+ * is viewportHeight * pixelRatio / (2 * tan(fov / 2)). Default to a 1000
24
+ * pixels viewport with the 40 degrees field of view used by the scenes.
25
+ */
26
+ let pixelsPerUnit = 1000 / (2 * Math.tan(20 * Math.PI / 180));
27
+
28
+ //Weak references to every live text sprite, so they can be redrawn
29
+ const sprites = new Set();
30
+ let fontListenerAdded = false;
31
+
32
+ const fontSizeForHeight = (textHeight) => {
33
+ const size = Math.round(textHeight * pixelsPerUnit * OVERSAMPLE);
34
+ return Math.min(Math.max(size, MIN_FONT_SIZE), MAX_FONT_SIZE);
35
+ }
36
+
37
+ /*
38
+ * The canvas matches the on screen size so mipmaps are not required,
39
+ * they blur the text when it is shrunk.
40
+ */
41
+ const applyTextureSettings = (sprite) => {
42
+ const texture = sprite.material?.map;
43
+ if (texture) {
44
+ texture.generateMipmaps = false;
45
+ texture.minFilter = THREE.LinearFilter;
46
+ texture.magFilter = THREE.LinearFilter;
47
+ texture.anisotropy = 1;
48
+ }
49
+ }
50
+
51
+ /*
52
+ * SpriteText redraws its canvas and creates a new texture in every setter,
53
+ * set all font properties before a single redraw instead.
54
+ */
55
+ const setFont = (sprite, fontFace, fontSize, fontWeight) => {
56
+ if ('_fontFace' in sprite && typeof sprite._genCanvas === 'function') {
57
+ sprite._fontFace = fontFace;
58
+ sprite._fontSize = fontSize;
59
+ sprite._fontWeight = fontWeight;
60
+ sprite._genCanvas();
61
+ } else {
62
+ sprite.fontFace = fontFace;
63
+ sprite.fontSize = fontSize;
64
+ sprite.fontWeight = fontWeight;
65
+ }
66
+ }
67
+
68
+ /*
69
+ * Redraw the sprite at the resolution required for its current height.
70
+ */
71
+ const refreshTextSprite = (sprite, force = false) => {
72
+ const fontSize = fontSizeForHeight(sprite.textHeight);
73
+ if (force || fontSize !== sprite.fontSize) {
74
+ setFont(sprite, sprite.fontFace, fontSize, sprite.fontWeight);
75
+ applyTextureSettings(sprite);
76
+ }
77
+ }
78
+
79
+ const forEachSprite = (callback) => {
80
+ sprites.forEach((ref) => {
81
+ const sprite = ref.deref();
82
+ if (sprite) {
83
+ callback(sprite);
84
+ } else {
85
+ sprites.delete(ref);
86
+ }
87
+ });
88
+ }
89
+
90
+ const redrawAll = () => {
91
+ forEachSprite((sprite) => refreshTextSprite(sprite, true));
92
+ requestRenderAll();
93
+ }
94
+
95
+ /*
96
+ * The canvas is drawn with a fallback font if a web font has not been
97
+ * loaded yet, redraw once it is available.
98
+ */
99
+ const waitForFont = (sprite) => {
100
+ if (typeof document === 'undefined' || !document.fonts) {
101
+ return;
102
+ }
103
+ if (!fontListenerAdded && document.fonts.addEventListener) {
104
+ //Catch fonts from stylesheets added after the sprites are created
105
+ document.fonts.addEventListener('loadingdone', redrawAll);
106
+ fontListenerAdded = true;
107
+ }
108
+ const font = `${sprite.fontWeight} ${sprite.fontSize}px ${sprite.fontFace}`;
109
+ try {
110
+ if (!document.fonts.check(font)) {
111
+ document.fonts.load(font).then(() => {
112
+ refreshTextSprite(sprite, true);
113
+ requestRenderAll();
114
+ }).catch(() => {});
115
+ }
116
+ } catch {
117
+ //Invalid font string, keep the fallback font
118
+ }
119
+ }
120
+
121
+ const registerTextSprite = (sprite) => {
122
+ sprites.add(new WeakRef(sprite));
123
+ }
124
+
125
+ /**
126
+ * Stop tracking a text sprite, call this when it is disposed.
127
+ */
128
+ const releaseTextSprite = (sprite) => {
129
+ sprites.forEach((ref) => {
130
+ const trackedSprite = ref.deref();
131
+ if (!trackedSprite || trackedSprite === sprite) {
132
+ sprites.delete(ref);
133
+ }
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Create a text sprite rendered with a node material, the canvas resolution
139
+ * is chosen from the size of the text on screen.
140
+ *
141
+ * @param {String} text - Text to display.
142
+ * @param {Number} textHeight - Height of a line of text in view units.
143
+ * @param {String} colour - CSS colour of the text.
144
+ * @param {String} fontFace - Font family.
145
+ * @param {Number|String} fontWeight - Font weight.
146
+ * @return {SpriteText}
147
+ */
148
+ const createTextSprite = (text, textHeight, colour, fontFace, fontWeight) => {
149
+ const sprite = colour ? new SpriteText(text, textHeight, colour) :
150
+ new SpriteText(text, textHeight);
151
+ setFont(sprite, fontFace, fontSizeForHeight(textHeight), fontWeight);
152
+ //Replace the SpriteMaterial created by SpriteText with a node material
153
+ //using the same canvas texture.
154
+ const originalMaterial = sprite.material;
155
+ sprite.material = new THREE.SpriteNodeMaterial({
156
+ map: originalMaterial.map,
157
+ });
158
+ originalMaterial.map = null;
159
+ originalMaterial.dispose();
160
+ sprite.material.sizeAttenuation = false;
161
+ applyTextureSettings(sprite);
162
+ registerTextSprite(sprite);
163
+ waitForFont(sprite);
164
+ return sprite;
165
+ }
166
+
167
+ /**
168
+ * Set the height of a text sprite and redraw it once at the matching
169
+ * resolution.
170
+ */
171
+ const setTextSpriteHeight = (sprite, textHeight) => {
172
+ if ('_textHeight' in sprite) {
173
+ sprite._textHeight = textHeight;
174
+ refreshTextSprite(sprite, true);
175
+ } else {
176
+ sprite.textHeight = textHeight;
177
+ refreshTextSprite(sprite);
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Update the number of screen pixels per unit of text height, e.g. when the
183
+ * viewport is resized. Text sprites are redrawn if it has changed
184
+ * significantly.
185
+ *
186
+ * @param {Number} value - viewportHeight * pixelRatio / (2 * tan(fov / 2))
187
+ */
188
+ const setTextPixelsPerUnit = (value) => {
189
+ if (!(value > 0) || !isFinite(value)) {
190
+ return;
191
+ }
192
+ if (Math.abs(value / pixelsPerUnit - 1) > RESCALE_THRESHOLD) {
193
+ pixelsPerUnit = value;
194
+ forEachSprite((sprite) => refreshTextSprite(sprite));
195
+ requestRenderAll();
196
+ }
197
+ }
198
+
199
+ const getTextPixelsPerUnit = () => pixelsPerUnit;
200
+
201
+ export {
202
+ applyTextureSettings as applyTextSpriteTextureSettings,
203
+ createTextSprite,
204
+ fontSizeForHeight,
205
+ getTextPixelsPerUnit,
206
+ refreshTextSprite,
207
+ registerTextSprite,
208
+ releaseTextSprite,
209
+ setTextPixelsPerUnit,
210
+ setTextSpriteHeight,
211
+ };
package/src/utilities.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as THREE from 'three/webgpu';
2
- import SpriteTextModule from 'three-spritetext';
3
- const SpriteText = SpriteTextModule.default || SpriteTextModule;
4
2
  import discPNG from './assets/disc.png';
3
+ import { requestRenderAll } from './renderRequests';
4
+ import { createTextSprite } from './textSprite';
5
5
 
6
6
  function createNewURL(target, reference) {
7
7
  const getNewURL = (target, reference) => {
@@ -31,6 +31,29 @@ function createNewURL(target, reference) {
31
31
  }
32
32
  }
33
33
 
34
+ /*
35
+ * Compute the world matrix of an object from its ancestors and return it.
36
+ * three.js only recomputes world matrices flagged by matrixWorldNeedsUpdate,
37
+ * which objects with matrixAutoUpdate set to false (zinc meshes and region
38
+ * groups) never set, they would otherwise stay stale until the next render.
39
+ */
40
+ function updateWorldMatrixFromAncestors(object) {
41
+ if (object.parent) {
42
+ updateWorldMatrixFromAncestors(object.parent);
43
+ }
44
+ if (object.matrixAutoUpdate) {
45
+ object.updateMatrix();
46
+ }
47
+ if (object.matrixWorldAutoUpdate !== false) {
48
+ if (object.parent) {
49
+ object.matrixWorld.multiplyMatrices(object.parent.matrixWorld, object.matrix);
50
+ } else {
51
+ object.matrixWorld.copy(object.matrix);
52
+ }
53
+ }
54
+ return object.matrixWorld;
55
+ }
56
+
34
57
  /*
35
58
  * Calculate the bounding box of a mesh, values will be
36
59
  * set for cachedBox, b1, v1 and v2 and they need to be
@@ -61,8 +84,7 @@ function getBoundingBox(mesh, cachedBox, b1, v1, v2) {
61
84
  cachedBox.setFromBufferAttribute(
62
85
  mesh.geometry.attributes.position);
63
86
  }
64
- mesh.updateWorldMatrix(true, true);
65
- cachedBox.applyMatrix4(mesh.matrixWorld);
87
+ cachedBox.applyMatrix4(updateWorldMatrixFromAncestors(mesh));
66
88
  }
67
89
 
68
90
 
@@ -128,31 +150,40 @@ const getColorsRGB = (colors, index) => {
128
150
  return [mycolor.r, mycolor.g, mycolor.b];
129
151
  }
130
152
 
153
+ //morphColor0/morphColor1 attribute -> morph colour target last copied into it
154
+ const morphColorSources = new WeakMap();
155
+
131
156
  const updateMorphColorAttribute = function(targetGeometry, morph) {
132
157
  if (morph && targetGeometry && targetGeometry.morphAttributes &&
133
158
  targetGeometry.morphAttributes[ "color" ]) {
134
159
  const morphColors = targetGeometry.morphAttributes[ "color" ];
135
160
  const influences = morph.morphTargetInfluences;
136
161
  const length = influences.length;
137
- let morphArray = [];
138
- for (let i = 0; i < length; i++) {
162
+ //Find the first two morph targets with non-zero influence
163
+ let found = 0;
164
+ let index0 = 0;
165
+ let index1 = 0;
166
+ let weight0 = 0;
167
+ let weight1 = 0;
168
+ for (let i = 0; i < length && found < 2; i++) {
139
169
  if (influences[i] > 0) {
140
- morphArray.push([i, influences[i]]);
170
+ if (found === 0) {
171
+ index0 = index1 = i;
172
+ weight0 = influences[i];
173
+ } else {
174
+ index1 = i;
175
+ weight1 = influences[i];
176
+ }
177
+ found++;
141
178
  }
142
179
  }
143
180
  //morphColorMix (0 = fully morphColor0, 1 = fully morphColor1) drives the
144
181
  //TSL colorNode set up by applyMorphColorNode - keep it in sync with
145
182
  //whichever two morph targets currently have non-zero influence.
146
183
  let mix = 0;
147
- let index0 = 0;
148
- let index1 = 0;
149
- if (morphArray.length >= 2) {
150
- index0 = morphArray[0][0];
151
- index1 = morphArray[1][0];
152
- const total = morphArray[0][1] + morphArray[1][1];
153
- mix = total > 0 ? morphArray[1][1] / total : 0;
154
- } else if (morphArray.length == 1) {
155
- index0 = index1 = morphArray[0][0];
184
+ if (found >= 2) {
185
+ const total = weight0 + weight1;
186
+ mix = total > 0 ? weight1 / total : 0;
156
187
  }
157
188
  //Keep morphColor0/morphColor1 as two persistent attributes and mutate
158
189
  //their contents in place (array.set + needsUpdate) rather than
@@ -176,10 +207,20 @@ const updateMorphColorAttribute = function(targetGeometry, morph) {
176
207
  targetGeometry.setAttribute('morphColor0', attribute0);
177
208
  targetGeometry.setAttribute('morphColor1', attribute1);
178
209
  }
179
- attribute0.array.set(morphColors[index0].array);
180
- attribute0.needsUpdate = true;
181
- attribute1.array.set(morphColors[index1].array);
182
- attribute1.needsUpdate = true;
210
+ //Only copy and upload when the pair of keyframes changes, between
211
+ //keyframes only morphColorMix changes.
212
+ const source0 = morphColors[index0];
213
+ const source1 = morphColors[index1];
214
+ if (morphColorSources.get(attribute0) !== source0) {
215
+ attribute0.array.set(source0.array);
216
+ attribute0.needsUpdate = true;
217
+ morphColorSources.set(attribute0, source0);
218
+ }
219
+ if (morphColorSources.get(attribute1) !== source1) {
220
+ attribute1.array.set(source1.array);
221
+ attribute1.needsUpdate = true;
222
+ morphColorSources.set(attribute1, source1);
223
+ }
183
224
  const morphColorMix = morph.material && morph.material.userData &&
184
225
  morph.material.userData.uniforms && morph.material.userData.uniforms.morphColorMix;
185
226
  if (morphColorMix) {
@@ -697,29 +738,81 @@ function createBufferGeometry(length, coords) {
697
738
  return undefined;
698
739
  };
699
740
 
741
+ /*
742
+ * WebGPURenderer converts 16 bit index buffers to 32 bit and keeps the
743
+ * converted array, doubling the memory used by indices. WebGPU supports 16
744
+ * bit indices natively, three.js only converts arrays whose constructor is
745
+ * exactly Uint16Array while the index format is chosen with
746
+ * instanceof Uint16Array. Indices are viewed through this subclass so they
747
+ * stay 16 bit, the values and getX() are unchanged.
748
+ */
749
+ class IndexUint16Array extends Uint16Array {}
750
+
751
+ function keepIndexType(geometry) {
752
+ const index = geometry?.index;
753
+ if (index && index.array.constructor === Uint16Array) {
754
+ const array = index.array;
755
+ //A view of the same buffer, no copy is made
756
+ index.array = new IndexUint16Array(array.buffer, array.byteOffset, array.length);
757
+ }
758
+ }
759
+
760
+ /*
761
+ * Temporarily view the indices of the object and its descendants as plain
762
+ * Uint16Array, for code which checks the exact array type such as
763
+ * GLTFExporter. Returns a function which undoes the change.
764
+ */
765
+ function useStandardIndexType(object) {
766
+ const changed = [];
767
+ object.traverse((child) => {
768
+ const index = child.geometry?.index;
769
+ if (index && index.array instanceof IndexUint16Array) {
770
+ const array = index.array;
771
+ index.array = new Uint16Array(array.buffer, array.byteOffset, array.length);
772
+ changed.push([index, array, index.array]);
773
+ }
774
+ });
775
+ return () => {
776
+ changed.forEach(([index, original, replacement]) => {
777
+ //Only restore if nothing else has replaced the array since
778
+ if (index.array === replacement) {
779
+ index.array = original;
780
+ }
781
+ });
782
+ };
783
+ }
784
+
785
+ //Shared by all pointsets
786
+ let circularTexture = undefined;
787
+
700
788
  function getCircularTexture() {
789
+ if (circularTexture) {
790
+ return circularTexture;
791
+ }
701
792
  const image = new Image();
702
- image.src = discPNG;
703
793
  const texture = new THREE.Texture();
704
794
  texture.image = image;
705
- texture.needsUpdate = true;
795
+ //Only upload once the image is available
796
+ image.onload = () => {
797
+ texture.needsUpdate = true;
798
+ requestRenderAll();
799
+ };
800
+ image.src = discPNG;
801
+ if (image.complete && image.naturalWidth > 0) {
802
+ texture.needsUpdate = true;
803
+ }
804
+ circularTexture = texture;
706
805
  return texture;
707
806
  }
708
807
 
808
+ /*
809
+ * Create a text sprite, e.g. marker numbers. The canvas resolution is chosen
810
+ * from the size of the text on screen, pixel is no longer used.
811
+ */
709
812
  function createNewSpriteText(text, height, colour, font, pixel, weight) {
710
- const sprite = new SpriteText(text, height, colour, font, pixel, weight);
711
- sprite.canvasScale = 4;
712
- sprite.fontFace = font;
713
- sprite.fontSize = pixel;
714
- sprite.fontWeight = weight;
715
- const canvasTexture = sprite.material.map; // Snag the canvas texture generated by the library
716
- sprite.material = new THREE.SpriteNodeMaterial({
717
- map: canvasTexture,
718
- });
719
- sprite.material.map.generateMipmaps = true;
720
- sprite.material.map.anisotropy = 4;
721
- sprite.material.sizeAttenuation = false;
722
- sprite.material.alphaTest = 0.5;
813
+ const sprite = createTextSprite(text, height, colour, font, weight);
814
+ //Only discard fully transparent pixels to keep the smooth edges
815
+ sprite.material.alphaTest = 0.05;
723
816
  sprite.material.transparent = true;
724
817
  sprite.material.depthWrite = false;
725
818
  sprite.material.depthTest = false;
@@ -775,21 +868,24 @@ function removeVertexAtIndex(geometry, index, maintainLength) {
775
868
 
776
869
  // Helper to remove elements from a typed array
777
870
  const removeElements = (attribute, name) => {
778
- let removed = false;
779
- const array = Array.from(attribute.array);
780
- if (array.length >= (start + deleteCount)) {
781
- array.splice(start, deleteCount); // Use splice for removal
782
- if (maintainLength) {
783
- array.concat(new Array(deleteCount).fill(0));
784
- }
785
- removed = true;
786
- const newArray = new Float32Array(array);
871
+ const array = attribute.array;
872
+ if (array.length < (start + deleteCount)) {
873
+ return false;
874
+ }
875
+ if (maintainLength) {
876
+ //Shift the remaining values in place and clear the end, this keeps
877
+ //the attribute and its GPU buffer.
878
+ array.copyWithin(start, start + deleteCount);
879
+ array.fill(0, array.length - deleteCount);
880
+ attribute.needsUpdate = true;
881
+ } else {
882
+ const newArray = new array.constructor(array.length - deleteCount);
883
+ newArray.set(array.subarray(0, start));
884
+ newArray.set(array.subarray(start + deleteCount), start);
787
885
  geometry.setAttribute(name, new THREE.BufferAttribute(newArray, itemSize));
788
886
  geometry.getAttribute(name).needsUpdate = true;
789
887
  }
790
- // Create new BufferAttribute with the modified array
791
-
792
- return removed;
888
+ return true;
793
889
  };
794
890
 
795
891
  removed = removeElements(positionAttribute, 'position');
@@ -875,6 +971,10 @@ export {
875
971
  createNewURL,
876
972
  getBoundingBox,
877
973
  getCircularTexture,
974
+ keepIndexType,
975
+ updateWorldMatrixFromAncestors,
976
+ useStandardIndexType,
977
+ IndexUint16Array,
878
978
  getColorsRGB,
879
979
  isRegionGroup,
880
980
  loadExternalFile,