zincjs 2.4.1-beta.4 → 2.4.1-beta.6

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
+ };
@@ -7,6 +7,7 @@ import {
7
7
  int,
8
8
  float,
9
9
  vec3,
10
+ vec4,
10
11
  uniform,
11
12
  mix,
12
13
  select,
@@ -28,6 +29,9 @@ const repeatModeToInt = (repeat_mode) => {
28
29
  }
29
30
  }
30
31
 
32
+ //Order of the vec4 fields of each glyph instance in the output buffer
33
+ const OUTPUT_FIELDS = { position: 0, axis1: 1, axis2: 2, axis3: 3, color: 4 };
34
+
31
35
  const multiplierForRepeatMode = (repeat_mode) => {
32
36
  if (repeat_mode == "AXES_2D" || repeat_mode == "MIRROR") return 2;
33
37
  if (repeat_mode == "AXES_3D") return 3;
@@ -66,9 +70,10 @@ const multiplierForRepeatMode = (repeat_mode) => {
66
70
  * @param {Array<number>} params.scaleFactors
67
71
  * @returns {Object} { uniforms, compute, outputs } - update
68
72
  * uniforms.bottomFrame/topFrame/proportion.value then pass `compute` to
69
- * renderer.compute()/computeAsync(); read results back from
70
- * outputs.position/axis1/axis2/axis3/color (each a StorageBufferNode -
71
- * the raw attribute to read back is `outputs.position.value` etc).
73
+ * renderer.compute()/computeAsync(); read results back with
74
+ * readbackGlyphTransform(). outputs.buffer is the interleaved storage
75
+ * buffer (outputs.stride vec4 per instance), outputs.position/axis1/
76
+ * axis2/axis3/color provide element(index) accessors for materials.
72
77
  */
73
78
  function createGlyphTransformCompute({
74
79
  baseCount,
@@ -87,18 +92,38 @@ function createGlyphTransformCompute({
87
92
  }) {
88
93
  const multiplier = multiplierForRepeatMode(repeat_mode);
89
94
 
90
- const positionsBuffer = attributeArray(positionsData, 'vec3');
91
- const axis1Buffer = attributeArray(axis1Data, 'vec3');
92
- const axis2Buffer = attributeArray(axis2Data, 'vec3');
93
- const axis3Buffer = attributeArray(axis3Data, 'vec3');
94
- const scaleBuffer = attributeArray(scaleData, 'vec3');
95
- const colorBuffer = colorData ? attributeArray(colorData, 'vec3') : undefined;
96
-
97
- const outPosition = attributeArray(outputCount, 'vec3');
98
- const outAxis1 = attributeArray(outputCount, 'vec3');
99
- const outAxis2 = attributeArray(outputCount, 'vec3');
100
- const outAxis3 = attributeArray(outputCount, 'vec3');
101
- const outColor = colorData ? attributeArray(outputCount, 'vec3') : undefined;
95
+ // All inputs are interleaved into a single storage buffer and all outputs
96
+ // into another, one vec4 per field. Separate buffers per field needed up
97
+ // to 11 storage buffers in the compute stage, more than some devices
98
+ // support (e.g. 10 on iOS). vec4 also avoids three.js padding vec3
99
+ // storage data into an extra copy.
100
+ const hasColor = !!colorData;
101
+ const inputSources = [positionsData, axis1Data, axis2Data, axis3Data, scaleData];
102
+ if (hasColor) inputSources.push(colorData);
103
+ const inputStride = inputSources.length;
104
+ const outputStride = hasColor ? OUTPUT_FIELDS.color + 1 : OUTPUT_FIELDS.axis3 + 1;
105
+ const recordCount = positionsData.length / 3;
106
+ const inputData = new Float32Array(recordCount * inputStride * 4);
107
+ for (let record = 0; record < recordCount; record++) {
108
+ for (let field = 0; field < inputStride; field++) {
109
+ const source = inputSources[field];
110
+ const target = (record * inputStride + field) * 4;
111
+ inputData[target] = source[record * 3];
112
+ inputData[target + 1] = source[record * 3 + 1];
113
+ inputData[target + 2] = source[record * 3 + 2];
114
+ }
115
+ }
116
+ const inputBuffer = attributeArray(inputData, 'vec4');
117
+ const outputBuffer = attributeArray(outputCount * outputStride, 'vec4');
118
+
119
+ const readInput = (recordIdx, field) =>
120
+ inputBuffer.element(recordIdx.mul(int(inputStride)).add(int(field))).xyz;
121
+ const writeOutput = (outIdx, field, value) =>
122
+ outputBuffer.element(outIdx.mul(int(outputStride)).add(int(field))).assign(vec4(value, 0));
123
+ //Accessor used by the render material to read an output field
124
+ const outputField = (field) => ({
125
+ element: (idx) => outputBuffer.element(int(idx).mul(int(outputStride)).add(int(field))).xyz,
126
+ });
102
127
 
103
128
  const uniforms = {
104
129
  bottomFrame: uniform(0, 'int'),
@@ -121,11 +146,11 @@ function createGlyphTransformCompute({
121
146
  const topIdx = uniforms.topFrame.mul(int(baseCount)).add(inputIdx);
122
147
 
123
148
  // Matches updateMorphGlyphsets(): bottom*proportion + top*(1-proportion).
124
- const point = mix(positionsBuffer.element(topIdx), positionsBuffer.element(bottomIdx), uniforms.proportion);
125
- const axis1 = mix(axis1Buffer.element(topIdx), axis1Buffer.element(bottomIdx), uniforms.proportion);
126
- const axis2 = mix(axis2Buffer.element(topIdx), axis2Buffer.element(bottomIdx), uniforms.proportion);
127
- const axis3 = mix(axis3Buffer.element(topIdx), axis3Buffer.element(bottomIdx), uniforms.proportion);
128
- const scale = mix(scaleBuffer.element(topIdx), scaleBuffer.element(bottomIdx), uniforms.proportion);
149
+ const point = mix(readInput(topIdx, 0), readInput(bottomIdx, 0), uniforms.proportion);
150
+ const axis1 = mix(readInput(topIdx, 1), readInput(bottomIdx, 1), uniforms.proportion);
151
+ const axis2 = mix(readInput(topIdx, 2), readInput(bottomIdx, 2), uniforms.proportion);
152
+ const axis3 = mix(readInput(topIdx, 3), readInput(bottomIdx, 3), uniforms.proportion);
153
+ const scale = mix(readInput(topIdx, 4), readInput(bottomIdx, 4), uniforms.proportion);
129
154
 
130
155
  // NOTE: each branch below assigns straight into the output storage
131
156
  // buffer elements (outPosition.element(outIdx).assign(...) etc.),
@@ -175,10 +200,10 @@ function createGlyphTransformCompute({
175
200
  a3.assign(a3.negate());
176
201
  });
177
202
 
178
- outPosition.element(outIdx).assign(p);
179
- outAxis1.element(outIdx).assign(a1);
180
- outAxis2.element(outIdx).assign(a2);
181
- outAxis3.element(outIdx).assign(a3);
203
+ writeOutput(outIdx, OUTPUT_FIELDS.position, p);
204
+ writeOutput(outIdx, OUTPUT_FIELDS.axis1, a1);
205
+ writeOutput(outIdx, OUTPUT_FIELDS.axis2, a2);
206
+ writeOutput(outIdx, OUTPUT_FIELDS.axis3, a3);
182
207
 
183
208
  }).Else(() => {
184
209
 
@@ -220,16 +245,16 @@ function createGlyphTransformCompute({
220
245
  .div(max(mag2, 1e-8));
221
246
  const finalAxis2 = select(mag2.greaterThan(0), axis2Raw.mul(scaling2), axis2Raw);
222
247
 
223
- outPosition.element(outIdx).assign(finalPoint);
224
- outAxis1.element(outIdx).assign(finalAxis1);
225
- outAxis2.element(outIdx).assign(finalAxis2);
226
- outAxis3.element(outIdx).assign(finalAxis3);
248
+ writeOutput(outIdx, OUTPUT_FIELDS.position, finalPoint);
249
+ writeOutput(outIdx, OUTPUT_FIELDS.axis1, finalAxis1);
250
+ writeOutput(outIdx, OUTPUT_FIELDS.axis2, finalAxis2);
251
+ writeOutput(outIdx, OUTPUT_FIELDS.axis3, finalAxis3);
227
252
 
228
253
  });
229
254
 
230
- if (colorBuffer) {
231
- const color = mix(colorBuffer.element(topIdx), colorBuffer.element(bottomIdx), uniforms.proportion);
232
- outColor.element(outIdx).assign(color);
255
+ if (hasColor) {
256
+ const color = mix(readInput(topIdx, 5), readInput(bottomIdx, 5), uniforms.proportion);
257
+ writeOutput(outIdx, OUTPUT_FIELDS.color, color);
233
258
  }
234
259
 
235
260
  } )().compute( outputCount );
@@ -238,11 +263,14 @@ function createGlyphTransformCompute({
238
263
  uniforms,
239
264
  compute: computeFn,
240
265
  outputs: {
241
- position: outPosition,
242
- axis1: outAxis1,
243
- axis2: outAxis2,
244
- axis3: outAxis3,
245
- color: outColor,
266
+ buffer: outputBuffer,
267
+ stride: outputStride,
268
+ count: outputCount,
269
+ position: outputField(OUTPUT_FIELDS.position),
270
+ axis1: outputField(OUTPUT_FIELDS.axis1),
271
+ axis2: outputField(OUTPUT_FIELDS.axis2),
272
+ axis3: outputField(OUTPUT_FIELDS.axis3),
273
+ color: hasColor ? outputField(OUTPUT_FIELDS.color) : undefined,
246
274
  },
247
275
  };
248
276
  }
@@ -264,27 +292,30 @@ function createGlyphTransformCompute({
264
292
  * @param {THREE.WebGPURenderer} renderer
265
293
  * @param {Object} glyphCompute - return value of createGlyphTransformCompute().
266
294
  * @returns {Promise<Object>} { position, axis1, axis2, axis3, color } -
267
- * plain Float32Arrays, one vec3 per output glyph instance (color only
268
- * present when the glyphset has colours).
295
+ * plain Float32Arrays with a 4 float stride per output glyph instance
296
+ * (color only present when the glyphset has colours).
269
297
  */
270
298
  async function readbackGlyphTransform(renderer, glyphCompute) {
271
- const { position, axis1, axis2, axis3, color } = glyphCompute.outputs;
272
- // Read sequentially rather than Promise.all-ing these - concurrent
273
- // getArrayBufferAsync() calls against the same device have been observed
274
- // to crash the (Node-only, test-time) WebGPU native backend; one at a
275
- // time is the safe pattern here.
276
- const positionResult = new Float32Array(await renderer.getArrayBufferAsync(position.value));
277
- const axis1Result = new Float32Array(await renderer.getArrayBufferAsync(axis1.value));
278
- const axis2Result = new Float32Array(await renderer.getArrayBufferAsync(axis2.value));
279
- const axis3Result = new Float32Array(await renderer.getArrayBufferAsync(axis3.value));
280
- const colorResult = color ? new Float32Array(await renderer.getArrayBufferAsync(color.value)) : undefined;
281
-
299
+ const { buffer, stride, count, color } = glyphCompute.outputs;
300
+ //A single readback of the interleaved output buffer
301
+ const data = new Float32Array(await renderer.getArrayBufferAsync(buffer.value));
302
+ //Split into one array per field, each with a 4 float stride per instance
303
+ const extract = (field) => {
304
+ const result = new Float32Array(count * 4);
305
+ for (let i = 0; i < count; i++) {
306
+ const source = (i * stride + field) * 4;
307
+ result[i * 4] = data[source];
308
+ result[i * 4 + 1] = data[source + 1];
309
+ result[i * 4 + 2] = data[source + 2];
310
+ }
311
+ return result;
312
+ };
282
313
  return {
283
- position: positionResult,
284
- axis1: axis1Result,
285
- axis2: axis2Result,
286
- axis3: axis3Result,
287
- color: colorResult,
314
+ position: extract(OUTPUT_FIELDS.position),
315
+ axis1: extract(OUTPUT_FIELDS.axis1),
316
+ axis2: extract(OUTPUT_FIELDS.axis2),
317
+ axis3: extract(OUTPUT_FIELDS.axis3),
318
+ color: color ? extract(OUTPUT_FIELDS.color) : undefined,
288
319
  };
289
320
  }
290
321