zincjs 2.4.1-beta.2 → 2.4.1-beta.4

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.
@@ -26,6 +26,8 @@ const LOD = function (parent) {
26
26
  this._loader = undefined;
27
27
  //The owning Zinc Object
28
28
  this._parent = parent;
29
+ //Levels loaded after dispose are discarded
30
+ this._disposed = false;
29
31
 
30
32
  /*
31
33
  * Add a level of LOD at the specified distance
@@ -182,6 +184,7 @@ const LOD = function (parent) {
182
184
  }
183
185
 
184
186
  this.dispose = () => {
187
+ this._disposed = true;
185
188
  this.levels.forEach((level) => {
186
189
  if (level.morph && level.morph.geometry) {
187
190
  level.morph.geometry.dispose();
@@ -212,6 +215,10 @@ const LOD = function (parent) {
212
215
  */
213
216
  this.lodLoader = function (distance) {
214
217
  return (geometryIn) => {
218
+ if (this._disposed) {
219
+ geometryIn.dispose();
220
+ return;
221
+ }
215
222
  const material = this._material;
216
223
  const options = {
217
224
  localTimeEnabled: this._parent.timeEnabled,
@@ -3,7 +3,7 @@ import { Label } from './label';
3
3
  import { InstancedPoints } from '../three/InstancedPoints';
4
4
  import { ZincObject } from './zincObject';
5
5
  import { getCircularTexture, toBufferGeometry } from '../utilities';
6
- import { createInstancedPointsMaterial } from '../tls/pointsMaterial';
6
+ import { createInstancedPointsMaterial } from '../tsl/pointsMaterial';
7
7
 
8
8
  //Maximum number of points a programmatically built (addPoints) Pointset can
9
9
  //hold - the InstancedMesh backing it is allocated once with this capacity.
@@ -26,28 +26,26 @@ const createPointQuadGeometry = (capacity) => {
26
26
 
27
27
  /**
28
28
  * Extract the per time step, flat [x0, y0, z0, x1, y1, z1, ...] position
29
- * arrays from a (possibly time varying) BufferGeometry. Index 0 is always
30
- * the base frame, subsequent entries come from `morphAttributes.position`.
29
+ * arrays from a (possibly time varying) BufferGeometry.
30
+
31
31
  */
32
32
  const extractPositionFrames = (geometry, localTimeEnabled) => {
33
33
  const positionAttribute = geometry.getAttribute('position');
34
- const frames = [Float32Array.from(positionAttribute.array)];
35
34
  const morphPositions = geometry.morphAttributes.position;
36
35
  if (localTimeEnabled && morphPositions && morphPositions.length > 0) {
37
36
  const relative = geometry.morphTargetsRelative;
38
- morphPositions.forEach((attribute) => {
39
- if (relative) {
40
- const frame = new Float32Array(positionAttribute.array.length);
41
- for (let i = 0; i < frame.length; i++) {
42
- frame[i] = positionAttribute.array[i] + attribute.array[i];
43
- }
44
- frames.push(frame);
45
- } else {
46
- frames.push(Float32Array.from(attribute.array));
37
+ return morphPositions.map((attribute) => {
38
+ if (!relative) {
39
+ return Float32Array.from(attribute.array);
47
40
  }
41
+ const frame = new Float32Array(positionAttribute.array.length);
42
+ for (let i = 0; i < frame.length; i++) {
43
+ frame[i] = positionAttribute.array[i] + attribute.array[i];
44
+ }
45
+ return frame;
48
46
  });
49
47
  }
50
- return frames;
48
+ return [Float32Array.from(positionAttribute.array)];
51
49
  }
52
50
 
53
51
  /**
@@ -59,14 +57,7 @@ const extractPositionFrames = (geometry, localTimeEnabled) => {
59
57
  const extractColorFrames = (geometry, localMorphColour) => {
60
58
  const morphColors = geometry.morphAttributes.color;
61
59
  if (localMorphColour && morphColors && morphColors.length > 0) {
62
- const colorAttribute = geometry.getAttribute('color');
63
- const frames = [];
64
- frames.push(colorAttribute ? Float32Array.from(colorAttribute.array) :
65
- new Float32Array(morphColors[0].array.length));
66
- morphColors.forEach((attribute) => {
67
- frames.push(Float32Array.from(attribute.array));
68
- });
69
- return frames;
60
+ return morphColors.map((attribute) => Float32Array.from(attribute.array));
70
61
  }
71
62
  return undefined;
72
63
  }
@@ -70,4 +70,16 @@ const TexturePrimitive = function (textureIn) {
70
70
 
71
71
  TexturePrimitive.prototype = Object.create(ZincObject.prototype);
72
72
  TexturePrimitive.prototype.constructor = TexturePrimitive;
73
+
74
+ /**
75
+ * Free the textures and the other allocated memory.
76
+ */
77
+ TexturePrimitive.prototype.dispose = function() {
78
+ const textures = new Set(this.textureList);
79
+ if (this.texture) textures.add(this.texture);
80
+ textures.forEach(texture => texture.dispose());
81
+ this.texture = undefined;
82
+ this.textureList = [];
83
+ ZincObject.prototype.dispose.call(this);
84
+ }
73
85
  export { TexturePrimitive };
@@ -1,6 +1,6 @@
1
1
  import * as THREE from 'three/webgpu';
2
2
  //import * as shader from '../shaders/textureSlide.js';
3
- import { createWebGPUMaterial } from '../tls/textureSlides.js';
3
+ import { createWebGPUMaterial } from '../tsl/textureSlides.js';
4
4
  import { TexturePrimitive } from './texturePrimitive';
5
5
 
6
6
 
@@ -18,6 +18,8 @@ const cloneData3DTexture = (sourceTex) => {
18
18
  targetTex.wrapS = sourceTex.wrapS;
19
19
  targetTex.wrapT = sourceTex.wrapT;
20
20
  targetTex.wrapR = sourceTex.wrapR;
21
+ targetTex.colorSpace = sourceTex.colorSpace;
22
+ targetTex.anisotropy = sourceTex.anisotropy;
21
23
  targetTex.needsUpdate = true;
22
24
  return targetTex;
23
25
  }
@@ -275,6 +277,11 @@ const TextureSlides = function (textureIn) {
275
277
  if (slide.material)
276
278
  slide.material.dispose();
277
279
  });
280
+ if (maskTexture) {
281
+ maskTexture.dispose();
282
+ maskTexture = undefined;
283
+ maskEnabled = false;
284
+ }
278
285
  TexturePrimitive.prototype.dispose.call(this);
279
286
  this.boundingBoxUpdateRequired = true;
280
287
  }
@@ -565,6 +565,10 @@ ZincObject.prototype.getBoundingBox = function() {
565
565
  ZincObject.prototype.dispose = function() {
566
566
  //multilayyers
567
567
  this._lod.dispose();
568
+ if (this.marker) {
569
+ this.marker.dispose();
570
+ this.marker = undefined;
571
+ }
568
572
  this.animationGroup = undefined;
569
573
  this.mixer = undefined;
570
574
  this.morph = undefined;
package/src/region.js CHANGED
@@ -125,6 +125,15 @@ let Region = function (parentIn, sceneIn) {
125
125
  return parent;
126
126
  }
127
127
 
128
+ /**
129
+ * Get the {@link Scene} this region belongs to.
130
+ *
131
+ * @return {Scene}
132
+ */
133
+ this.getScene = () => {
134
+ return scene;
135
+ }
136
+
128
137
  /**
129
138
  * Get the array of each hierarachy from the root region to this region.
130
139
  *
@@ -436,13 +445,18 @@ let Region = function (parentIn, sceneIn) {
436
445
  */
437
446
  this.clear = transverse => {
438
447
  if (transverse) {
439
- children.forEach(childRegion => childRegion.clear(transverse));
448
+ children.forEach(childRegion => {
449
+ childRegion.clear(transverse);
450
+ group.remove(childRegion.getGroup());
451
+ });
440
452
  }
441
453
  zincObjects.forEach(zincObject => {
442
454
  group.remove(zincObject.getGroup());
443
455
  zincObject.dispose();
444
456
  });
445
- children = [];
457
+ if (transverse) {
458
+ children = [];
459
+ }
446
460
  zincObjects = [];
447
461
  }
448
462
 
package/src/renderer.js CHANGED
@@ -133,14 +133,20 @@ const Renderer = function (containerIn) {
133
133
  }
134
134
  //WebGPURenderer otherwise requests a device with the default
135
135
  //maxTextureArrayLayers (256 on many adapters), which is too low for
136
- //some currnetly sypported scaffold. We will request the highest possible
137
- //on the device
136
+ //some currnetly sypported scaffold, and the default
137
+ //maxStorageBuffersPerShaderStage (8 on many adapters), which is too
138
+ //low for compute passes like glyphset.js's GPU glyph-transform (11
139
+ //storage buffers). We will request the highest possible on the device
140
+ //for both.
138
141
  if (parameters.device === undefined && parameters.requiredLimits === undefined &&
139
142
  typeof navigator !== 'undefined' && navigator.gpu) {
140
143
  try {
141
144
  const adapter = await navigator.gpu.requestAdapter({ powerPreference: parameters.powerPreference });
142
145
  if (adapter) {
143
- parameters.requiredLimits = { maxTextureArrayLayers: adapter.limits.maxTextureArrayLayers };
146
+ parameters.requiredLimits = {
147
+ maxTextureArrayLayers: adapter.limits.maxTextureArrayLayers,
148
+ maxStorageBuffersPerShaderStage: adapter.limits.maxStorageBuffersPerShaderStage,
149
+ };
144
150
  }
145
151
  } catch (err) {
146
152
  //Fall through and let WebGPURenderer's own adapter/device request
@@ -719,10 +725,23 @@ const Renderer = function (containerIn) {
719
725
  }
720
726
  }
721
727
 
728
+ /**
729
+ * Check if the renderer is running on the WebGL 2 fallback backend.
730
+ * WebGPURenderer never uses WebGL 1, so this is false when WebGPU is used.
731
+ *
732
+ * @return {Boolean}
733
+ */
722
734
  this.isWebGL2 = () => {
723
- if (renderer)
724
- return renderer.capabilities.isWebGL2;
725
- return false;
735
+ return renderer?.backend?.isWebGLBackend === true;
736
+ }
737
+
738
+ /**
739
+ * Check if the renderer is running on the WebGPU backend.
740
+ *
741
+ * @return {Boolean}
742
+ */
743
+ this.isWebGPU = () => {
744
+ return renderer?.backend?.isWebGPUBackend === true;
726
745
  }
727
746
  };
728
747
 
package/src/scene.js CHANGED
@@ -9,7 +9,7 @@ import { SceneExporter } from './sceneExporter';
9
9
  import { SceneLoader } from './sceneLoader';
10
10
  import { Viewport } from './controls';
11
11
  import { createPointQuadGeometry } from './primitives/pointset';
12
- import { createInstancedPointsMaterial } from './tls/pointsMaterial';
12
+ import { createInstancedPointsMaterial } from './tsl/pointsMaterial';
13
13
  import {
14
14
  createBufferGeometry,
15
15
  createNewSpriteText,
@@ -988,8 +988,32 @@ const Scene = function (containerIn, rendererIn) {
988
988
  return undefined;
989
989
  }
990
990
 
991
+ /**
992
+ * Check if the renderer is running on the WebGL 2 fallback backend.
993
+ *
994
+ * @return {Boolean}
995
+ */
991
996
  this.isWebGL2 = () => {
992
- return rendererIn.isWebGL2();
997
+ return rendererIn?.backend?.isWebGLBackend === true;
998
+ }
999
+
1000
+ /**
1001
+ * Check if the renderer is running on the WebGPU backend.
1002
+ *
1003
+ * @return {Boolean}
1004
+ */
1005
+ this.isWebGPU = () => {
1006
+ return rendererIn?.backend?.isWebGPUBackend === true;
1007
+ }
1008
+
1009
+ /**
1010
+ * Get the THREE.WebGPURenderer this scene is being drawn with, for code
1011
+ * that needs to dispatch its own GPU work (e.g. a TSL compute pass).
1012
+ *
1013
+ * @return {THREE.WebGPURenderer}
1014
+ */
1015
+ this.getRenderer = () => {
1016
+ return rendererIn;
993
1017
  }
994
1018
 
995
1019
  /**
@@ -1001,7 +1025,7 @@ const Scene = function (containerIn, rendererIn) {
1001
1025
  rootRegion.clear(true);
1002
1026
  this.clearZincObjectAddedCallbacks();
1003
1027
  this.clearZincObjectRemovedCallbacks();
1004
- sceneLoader.toBeDwonloaded = 0;
1028
+ sceneLoader.cancelPendingLoads();
1005
1029
  if (zincCameraControls) {
1006
1030
  zincCameraControls.calculateMaxAllowedDistance(this);
1007
1031
  }
@@ -1,4 +1,49 @@
1
- import { GLTFExporter } from './three/GLTFExporter';
1
+ import { BufferAttribute } from 'three/webgpu';
2
+ import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter';
3
+
4
+ /*
5
+ * GLTFExporter only writes POSITION and NORMAL morph targets. glTF allows
6
+ * COLOR_n morph targets as well (GLTFLoader reads them), so add them to the
7
+ * targets the exporter has already written. Like other glTF morph targets
8
+ * they are stored relative to the base attribute.
9
+ */
10
+ class GLTFMorphColourPlugin {
11
+ constructor(writer) {
12
+ this.writer = writer;
13
+ this.name = 'ZINC_morph_colour';
14
+ }
15
+
16
+ writeMesh(mesh, meshDef) {
17
+ const geometry = mesh.geometry;
18
+ const morphColours = geometry && geometry.morphAttributes ?
19
+ geometry.morphAttributes.color : undefined;
20
+ const baseColour = geometry ? geometry.attributes.color : undefined;
21
+ const primitive = meshDef.primitives ? meshDef.primitives[0] : undefined;
22
+ if (!morphColours || !baseColour || !primitive || !primitive.targets)
23
+ return;
24
+ // All primitives of a mesh share the same targets array.
25
+ const targets = primitive.targets;
26
+ const itemSize = baseColour.itemSize;
27
+ const count = baseColour.count;
28
+ for (let i = 0; i < targets.length && i < morphColours.length; i++) {
29
+ const attribute = morphColours[i];
30
+ if (attribute.itemSize !== itemSize || attribute.count < count) {
31
+ console.warn('GLTFExporter: Colour morph target does not match the base colour, skipped.');
32
+ return;
33
+ }
34
+ const array = new Float32Array(count * itemSize);
35
+ for (let j = 0; j < count; j++) {
36
+ for (let a = 0; a < itemSize; a++) {
37
+ const value = attribute.getComponent(j, a);
38
+ array[j * itemSize + a] = geometry.morphTargetsRelative ?
39
+ value : value - baseColour.getComponent(j, a);
40
+ }
41
+ }
42
+ targets[i].COLOR_0 = this.writer.processAccessor(
43
+ new BufferAttribute(array, itemSize), geometry);
44
+ }
45
+ }
46
+ }
2
47
 
3
48
  /**
4
49
  * Provides an object which uses for exporting the scene
@@ -10,22 +55,59 @@ import { GLTFExporter } from './three/GLTFExporter';
10
55
  const SceneExporter = function (sceneIn) {
11
56
  const scene = sceneIn;
12
57
 
13
- this.exportGLTF = (binary) => {
58
+ // Matches the exporter's onlyVisible pruning - animations targeting a
59
+ // hidden node would otherwise produce channels with no target node.
60
+ const isVisible = (object) => {
61
+ for (let current = object; current; current = current.parent) {
62
+ if (current.visible === false) return false;
63
+ }
64
+ return true;
65
+ };
66
+
67
+ this.exportGLTF = async (binary) => {
14
68
  const rootRegion = scene.getRootRegion();
15
69
  const zincObjects = rootRegion.getAllObjects(true);
16
70
  const animations = [];
17
- zincObjects.forEach(zincObject => {
18
- if (zincObject.animationClip) {
19
- animations.push({clip: zincObject.animationClip[0], mesh: zincObject.getMorph()});
71
+ const glyphsetsToRestore = [];
72
+ for (const zincObject of zincObjects) {
73
+ if (zincObject.animationClip && zincObject.animationClip[0]) {
74
+ const mesh = zincObject.getMorph();
75
+ if (mesh && isVisible(mesh)) {
76
+ // The exporter resolves tracks against the scene root, but morph
77
+ // clips target '' (the mesh itself), so retarget them by uuid.
78
+ const clip = zincObject.animationClip[0].clone();
79
+ clip.tracks.forEach(track => { track.name = mesh.uuid + track.name; });
80
+ animations.push(clip);
81
+ }
82
+ }
83
+ // Glyphsets with GPU-compute-driven colour intentionally keep
84
+ // instanceColor null during normal rendering (see glyphset.js's
85
+ // prepareColorForExport() for why) - populate it just for this
86
+ // export, then undo it again afterwards.
87
+ if (zincObject.isGlyphset && zincObject.prepareColorForExport) {
88
+ await zincObject.prepareColorForExport();
89
+ glyphsetsToRestore.push(zincObject);
20
90
  }
21
- });
91
+ }
22
92
  const exporter = new GLTFExporter();
23
- const options = { binary, animations };
24
- return new Promise((resolve, reject) => {
25
- exporter.parse( scene.getThreeJSScene(), function ( gltf ) {
26
- resolve(gltf);
27
- }, options );
28
- });
93
+ exporter.register(writer => new GLTFMorphColourPlugin(writer));
94
+ const options = { binary, animations, onlyVisible: true };
95
+ try {
96
+ return await new Promise((resolve, reject) => {
97
+ exporter.parse(
98
+ scene.getThreeJSScene(),
99
+ function ( gltf ) {
100
+ resolve(gltf);
101
+ },
102
+ function ( error ) {
103
+ console.error("Unable to export GLTF.", error);
104
+ reject(error);
105
+ },
106
+ options );
107
+ });
108
+ } finally {
109
+ glyphsetsToRestore.forEach(zincObject => zincObject.clearColorExportState());
110
+ }
29
111
  }
30
112
  }
31
113
 
@@ -27,7 +27,30 @@ const SceneLoader = function (sceneIn) {
27
27
  this.progressMap = {};
28
28
  let viewLoaded = false;
29
29
  let errorDownload = false;
30
+ //Incremented when pending loads are cancelled, loads requested
31
+ //before the increment are discarded once they complete.
32
+ let generation = 0;
30
33
  const primitivesLoader = new PrimitivesLoader();
34
+
35
+ /**
36
+ * Cancel all pending downloads, objects from the cancelled downloads
37
+ * will be discarded instead of being added into the scene once they
38
+ * arrive.
39
+ */
40
+ this.cancelPendingLoads = () => {
41
+ generation++;
42
+ this.toBeDownloaded = 0;
43
+ this.progressMap = {};
44
+ viewLoaded = false;
45
+ errorDownload = false;
46
+ }
47
+
48
+ //Return a function which returns true if the loads requested before
49
+ //this call have been cancelled.
50
+ const createStaleCheck = () => {
51
+ const requestGeneration = generation;
52
+ return () => requestGeneration !== generation;
53
+ }
31
54
  /**
32
55
  * This function returns a three component array, which contains
33
56
  * [totalsize, totalLoaded and errorDownload] of all the downloads happening
@@ -62,7 +85,9 @@ const SceneLoader = function (sceneIn) {
62
85
  }
63
86
 
64
87
  this.onError = finishCallback => {
88
+ const isStale = createStaleCheck();
65
89
  return xhr => {
90
+ if (isStale()) return;
66
91
  this.toBeDownloaded = this.toBeDownloaded - 1;
67
92
  errorDownload = true;
68
93
  console.error(`There is an issue with external resource ${xhr?.responseURL ? ": " + xhr?.responseURL: ""}.`);
@@ -116,6 +141,7 @@ const SceneLoader = function (sceneIn) {
116
141
  this.loadViewURL = (url, finishCallback) => {
117
142
  this.toBeDownloaded += 1;
118
143
  const requestURL = url;
144
+ const isStale = createStaleCheck();
119
145
  fetch(requestURL)
120
146
  .then((response) => {
121
147
  if (!response.ok) {
@@ -124,6 +150,7 @@ const SceneLoader = function (sceneIn) {
124
150
  return response.json();
125
151
  })
126
152
  .then((viewData) => {
153
+ if (isStale()) return;
127
154
  scene.setupMultipleViews("default", { "default" : viewData });
128
155
  scene.resetView();
129
156
  viewLoaded = true;
@@ -176,6 +203,7 @@ const SceneLoader = function (sceneIn) {
176
203
  */
177
204
  this.loadFromViewURL = (targetRegion, jsonFilePrefix, finishCallback) => {
178
205
  const requestURL = jsonFilePrefix + "_view.json";
206
+ const isStale = createStaleCheck();
179
207
  fetch(requestURL)
180
208
  .then((response) => {
181
209
  if (!response.ok) {
@@ -184,6 +212,7 @@ const SceneLoader = function (sceneIn) {
184
212
  return response.json();
185
213
  })
186
214
  .then((viewData) => {
215
+ if (isStale()) return;
187
216
  scene.loadView(viewData);
188
217
  const urls = [];
189
218
  const filename_prefix = jsonFilePrefix + "_";
@@ -209,7 +238,12 @@ const SceneLoader = function (sceneIn) {
209
238
  //Internal loader for a regular zinc geometry.
210
239
  const linesloader = (region, localTimeEnabled, localMorphColour, groupName,
211
240
  anatomicalId, renderOrder, lod, tubeLines, finishCallback) => {
241
+ const isStale = createStaleCheck();
212
242
  return (geometry, materials) => {
243
+ if (isStale()) {
244
+ geometry.dispose();
245
+ return;
246
+ }
213
247
  const newLines = tubeLines ? new TubeLines() : new Lines();
214
248
  let material = undefined;
215
249
  if (materials && materials[0]) {
@@ -276,7 +310,9 @@ const SceneLoader = function (sceneIn) {
276
310
  }
277
311
 
278
312
  const glyphsetloader = (region, glyphurl, groupName, finishCallback, options) => {
313
+ const isStale = createStaleCheck();
279
314
  return (data) => {
315
+ if (isStale()) return;
280
316
  let glyphsetData = data;
281
317
  if (typeof glyphsetData === 'string' || glyphsetData instanceof String) {
282
318
  glyphsetData = JSON.parse(data);
@@ -292,6 +328,7 @@ const SceneLoader = function (sceneIn) {
292
328
  newGlyphset.setDuration(scene.getDuration());
293
329
  newGlyphset.groupName = groupName;
294
330
  let myCallback = () => {
331
+ if (isStale()) return;
295
332
  --this.toBeDownloaded;
296
333
  if (finishCallback != undefined && (typeof finishCallback == 'function'))
297
334
  finishCallback(newGlyphset);
@@ -358,7 +395,12 @@ const SceneLoader = function (sceneIn) {
358
395
 
359
396
  //Internal loader for zinc pointset.
360
397
  const pointsetloader = (region, localTimeEnabled, localMorphColour, groupName, anatomicalId, renderOrder, finishCallback) => {
398
+ const isStale = createStaleCheck();
361
399
  return (geometry, materials) => {
400
+ if (isStale()) {
401
+ geometry.dispose();
402
+ return;
403
+ }
362
404
  const newPointset = new Pointset();
363
405
  let material = new THREE.PointsMaterial(
364
406
  {
@@ -554,7 +596,12 @@ const SceneLoader = function (sceneIn) {
554
596
  }
555
597
  if (newTexture) {
556
598
  newTexture.groupName = groupName;
599
+ const isStale = createStaleCheck();
557
600
  let myCallback = () => {
601
+ if (isStale()) {
602
+ newTexture.dispose();
603
+ return;
604
+ }
558
605
  //Add zincObject after it has sort out all the required download
559
606
  region.addZincObject(newTexture);
560
607
  --this.toBeDownloaded;
@@ -582,6 +629,7 @@ const SceneLoader = function (sceneIn) {
582
629
  loadTexture(region, undefined, url, groupName, finishCallback, options);
583
630
  } else {
584
631
  const requestURL = url;
632
+ const isStale = createStaleCheck();
585
633
  fetch(requestURL)
586
634
  .then((response) => {
587
635
  if (!response.ok) {
@@ -591,6 +639,7 @@ const SceneLoader = function (sceneIn) {
591
639
  return response.json().then((textureData) => ({ textureData, referenceURL }));
592
640
  })
593
641
  .then(({textureData, referenceURL}) => {
642
+ if (isStale()) return;
594
643
  loadTexture(region, referenceURL, textureData, groupName, finishCallback, options);
595
644
  })
596
645
  .catch((error) => {
@@ -662,7 +711,12 @@ const SceneLoader = function (sceneIn) {
662
711
  options,
663
712
  finishCallback
664
713
  ) => {
714
+ const isStale = createStaleCheck();
665
715
  return (geometry, materials) => {
716
+ if (isStale()) {
717
+ geometry.dispose();
718
+ return;
719
+ }
666
720
  let material = undefined;
667
721
  if (materials && materials[0]) {
668
722
  material = materials[0];
@@ -831,7 +885,8 @@ const SceneLoader = function (sceneIn) {
831
885
  */
832
886
  this.loadGLTF = (region, url, finishCallback, allCompletedCallback, options) => {
833
887
  const loader = new GLTFToZincJSLoader();
834
- loader.load(scene, region, url, finishCallback, allCompletedCallback, options);
888
+ loader.load(scene, region, url, finishCallback, allCompletedCallback, options,
889
+ createStaleCheck());
835
890
  }
836
891
 
837
892
  let loadRegions = (currentRegion, referenceURL, regions, callback) => {
@@ -993,6 +1048,7 @@ const SceneLoader = function (sceneIn) {
993
1048
  */
994
1049
  this.loadMetadataURL = (targetRegion, url, finishCallback, allCompletedCallback, options) => {
995
1050
  const requestURL = url;
1051
+ const isStale = createStaleCheck();
996
1052
  fetch(requestURL)
997
1053
  .then((response) => {
998
1054
  if (!response.ok) {
@@ -1002,6 +1058,7 @@ const SceneLoader = function (sceneIn) {
1002
1058
  return response.json().then((metadata) => ({ metadata, referenceURL }));
1003
1059
  })
1004
1060
  .then(({metadata, referenceURL}) => {
1061
+ if (isStale()) return;
1005
1062
  scene.resetMetadata();
1006
1063
  scene.resetDuration();
1007
1064
  viewLoaded = false;
@@ -70,6 +70,16 @@ Texture.prototype.loadFromImages = async function (srcArrays) {
70
70
  return
71
71
  }
72
72
 
73
+ /**
74
+ * Free the memory allocated for this texture.
75
+ */
76
+ Texture.prototype.dispose = function () {
77
+ if (this.impl) {
78
+ this.impl.dispose();
79
+ this.impl = undefined;
80
+ }
81
+ }
82
+
73
83
  /**
74
84
  * Return true if the texture is ready for consumption.
75
85
  *
@@ -121,4 +121,13 @@ const TextureArray = function () {
121
121
  }
122
122
 
123
123
  TextureArray.prototype = Object.create(Texture.prototype);
124
+
125
+ /**
126
+ * Free the memory allocated for this texture array.
127
+ */
128
+ TextureArray.prototype.dispose = function () {
129
+ Texture.prototype.dispose.call(this);
130
+ this.imageData = undefined;
131
+ }
132
+
124
133
  export { TextureArray };
@@ -0,0 +1,53 @@
1
+ import * as THREE from 'three/webgpu';
2
+ import { Fn, instanceIndex, positionGeometry, materialColor, materialOpacity, vec4 } from 'three/tsl';
3
+ import { NODE_MATERIAL_CLASSES } from './morphColorMaterial';
4
+
5
+ /**
6
+ * Builds a NodeMaterial that renders each glyph instance's transform and
7
+ * colour read directly from a glyphCompute's (see ../tsl/glyphTransform.js)
8
+ * GPU output buffers, via a custom positionNode/colorNode - i.e. no
9
+ * instanceMatrix/instanceColor CPU sync is involved in producing what gets
10
+ * drawn. This is what lets Glyphset render every frame of an animation
11
+ * straight from the compute pass's output with zero GPU->CPU readback.
12
+ *
13
+ * Per NodeMaterial.setupPosition(), setting `positionNode` fully replaces
14
+ * (not composes with) the automatic per-instance instanceMatrix transform a
15
+ * stock THREE.InstancedMesh would otherwise apply - so instanceMatrix is
16
+ * simply irrelevant to rendering once this is set, no double-transform risk.
17
+ * (instanceMatrix is still kept in sync separately, when paused, purely so
18
+ * Glyphset's existing bounding-box/raycast code - which does need a
19
+ * CPU-side transform to read - keeps working unmodified.)
20
+ *
21
+ * @param {THREE.Material} sourceMaterial - the material JSONLoader produced
22
+ * for this glyph geometry; converted to its NodeMaterial equivalent.
23
+ * @param {Object} glyphCompute - return value of createGlyphTransformCompute().
24
+ * @returns {THREE.NodeMaterial}
25
+ */
26
+ function createGlyphInstancedMaterial(sourceMaterial, glyphCompute) {
27
+ let material = sourceMaterial;
28
+ if (!material || !material.isNodeMaterial) {
29
+ const NodeMaterialClass = (sourceMaterial && NODE_MATERIAL_CLASSES[sourceMaterial.type]) || THREE.MeshPhongNodeMaterial;
30
+ material = new NodeMaterialClass();
31
+ if (sourceMaterial) material.copy(sourceMaterial);
32
+ }
33
+
34
+ const { position, axis1, axis2, axis3, color } = glyphCompute.outputs;
35
+
36
+ material.positionNode = Fn(() => {
37
+ const idx = instanceIndex;
38
+ return position.element(idx)
39
+ .add(axis1.element(idx).mul(positionGeometry.x))
40
+ .add(axis2.element(idx).mul(positionGeometry.y))
41
+ .add(axis3.element(idx).mul(positionGeometry.z));
42
+ })();
43
+
44
+ if (color) {
45
+ material.colorNode = Fn(() => {
46
+ return vec4(color.element(instanceIndex).mul(materialColor), materialOpacity);
47
+ })();
48
+ }
49
+
50
+ return material;
51
+ }
52
+
53
+ export { createGlyphInstancedMaterial };