zincjs 2.4.1-beta.3 → 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.
@@ -27,7 +27,28 @@ const SceneLoader = function (sceneIn) {
27
27
  this.progressMap = {};
28
28
  let viewLoaded = false;
29
29
  let errorDownload = false;
30
+ let generation = 0;
30
31
  const primitivesLoader = new PrimitivesLoader();
32
+
33
+ /**
34
+ * Cancel all pending downloads, objects from the cancelled downloads
35
+ * will be discarded instead of being added into the scene once they
36
+ * arrive.
37
+ */
38
+ this.cancelPendingLoads = () => {
39
+ generation++;
40
+ this.toBeDownloaded = 0;
41
+ this.progressMap = {};
42
+ viewLoaded = false;
43
+ errorDownload = false;
44
+ }
45
+
46
+ //Return a function which returns true if the loads requested before
47
+ //this call have been cancelled.
48
+ const createStaleCheck = () => {
49
+ const requestGeneration = generation;
50
+ return () => requestGeneration !== generation;
51
+ }
31
52
  /**
32
53
  * This function returns a three component array, which contains
33
54
  * [totalsize, totalLoaded and errorDownload] of all the downloads happening
@@ -62,7 +83,9 @@ const SceneLoader = function (sceneIn) {
62
83
  }
63
84
 
64
85
  this.onError = finishCallback => {
86
+ const isStale = createStaleCheck();
65
87
  return xhr => {
88
+ if (isStale()) return;
66
89
  this.toBeDownloaded = this.toBeDownloaded - 1;
67
90
  errorDownload = true;
68
91
  console.error(`There is an issue with external resource ${xhr?.responseURL ? ": " + xhr?.responseURL: ""}.`);
@@ -116,6 +139,7 @@ const SceneLoader = function (sceneIn) {
116
139
  this.loadViewURL = (url, finishCallback) => {
117
140
  this.toBeDownloaded += 1;
118
141
  const requestURL = url;
142
+ const isStale = createStaleCheck();
119
143
  fetch(requestURL)
120
144
  .then((response) => {
121
145
  if (!response.ok) {
@@ -124,6 +148,7 @@ const SceneLoader = function (sceneIn) {
124
148
  return response.json();
125
149
  })
126
150
  .then((viewData) => {
151
+ if (isStale()) return;
127
152
  scene.setupMultipleViews("default", { "default" : viewData });
128
153
  scene.resetView();
129
154
  viewLoaded = true;
@@ -176,6 +201,7 @@ const SceneLoader = function (sceneIn) {
176
201
  */
177
202
  this.loadFromViewURL = (targetRegion, jsonFilePrefix, finishCallback) => {
178
203
  const requestURL = jsonFilePrefix + "_view.json";
204
+ const isStale = createStaleCheck();
179
205
  fetch(requestURL)
180
206
  .then((response) => {
181
207
  if (!response.ok) {
@@ -184,6 +210,7 @@ const SceneLoader = function (sceneIn) {
184
210
  return response.json();
185
211
  })
186
212
  .then((viewData) => {
213
+ if (isStale()) return;
187
214
  scene.loadView(viewData);
188
215
  const urls = [];
189
216
  const filename_prefix = jsonFilePrefix + "_";
@@ -209,7 +236,12 @@ const SceneLoader = function (sceneIn) {
209
236
  //Internal loader for a regular zinc geometry.
210
237
  const linesloader = (region, localTimeEnabled, localMorphColour, groupName,
211
238
  anatomicalId, renderOrder, lod, tubeLines, finishCallback) => {
239
+ const isStale = createStaleCheck();
212
240
  return (geometry, materials) => {
241
+ if (isStale()) {
242
+ geometry.dispose();
243
+ return;
244
+ }
213
245
  const newLines = tubeLines ? new TubeLines() : new Lines();
214
246
  let material = undefined;
215
247
  if (materials && materials[0]) {
@@ -276,7 +308,9 @@ const SceneLoader = function (sceneIn) {
276
308
  }
277
309
 
278
310
  const glyphsetloader = (region, glyphurl, groupName, finishCallback, options) => {
311
+ const isStale = createStaleCheck();
279
312
  return (data) => {
313
+ if (isStale()) return;
280
314
  let glyphsetData = data;
281
315
  if (typeof glyphsetData === 'string' || glyphsetData instanceof String) {
282
316
  glyphsetData = JSON.parse(data);
@@ -292,6 +326,7 @@ const SceneLoader = function (sceneIn) {
292
326
  newGlyphset.setDuration(scene.getDuration());
293
327
  newGlyphset.groupName = groupName;
294
328
  let myCallback = () => {
329
+ if (isStale()) return;
295
330
  --this.toBeDownloaded;
296
331
  if (finishCallback != undefined && (typeof finishCallback == 'function'))
297
332
  finishCallback(newGlyphset);
@@ -358,7 +393,12 @@ const SceneLoader = function (sceneIn) {
358
393
 
359
394
  //Internal loader for zinc pointset.
360
395
  const pointsetloader = (region, localTimeEnabled, localMorphColour, groupName, anatomicalId, renderOrder, finishCallback) => {
396
+ const isStale = createStaleCheck();
361
397
  return (geometry, materials) => {
398
+ if (isStale()) {
399
+ geometry.dispose();
400
+ return;
401
+ }
362
402
  const newPointset = new Pointset();
363
403
  let material = new THREE.PointsMaterial(
364
404
  {
@@ -408,7 +448,8 @@ const SceneLoader = function (sceneIn) {
408
448
  const loader = new STLLoader();
409
449
  loader.crossOrigin = "Anonymous";
410
450
  loader.load(url, meshloader(region, colour, opacity, false,
411
- false, groupName, undefined, undefined, undefined, finishCallback));
451
+ false, groupName, undefined, undefined, {}, finishCallback),
452
+ this.onProgress(url), this.onError(finishCallback));
412
453
  }
413
454
 
414
455
  /**
@@ -426,8 +467,9 @@ const SceneLoader = function (sceneIn) {
426
467
  const opacity = Zinc.defaultOpacity;
427
468
  const loader = new OBJLoader();
428
469
  loader.crossOrigin = "Anonymous";
429
- loader.load(url, meshloader(region, colour, opacity, false,
430
- false, groupName, undefined, undefined, undefined, finishCallback));
470
+ loader.load(url, objloader(region, colour, opacity, false,
471
+ false, groupName, undefined, undefined, {}, finishCallback),
472
+ this.onProgress(url), this.onError(finishCallback));
431
473
  }
432
474
 
433
475
  /**
@@ -458,17 +500,14 @@ const SceneLoader = function (sceneIn) {
458
500
  if (morphColour != undefined)
459
501
  localMorphColour = morphColour ? true : false;
460
502
  let loader = primitivesLoader;
461
- if (fileFormat !== undefined) {
462
- if (fileFormat == "STL") {
463
- loader = new STLLoader();
464
- } else if (fileFormat == "OBJ") {
465
- loader = new OBJLoader();
466
- loader.crossOrigin = "Anonymous";
467
- loader.load(url, objloader(region, colour, opacity, localTimeEnabled,
468
- localMorphColour, groupName, anatomicalId, finishCallback), this.onProgress(url), this.onError,
469
- options.loaderOptions);
470
- return;
471
- }
503
+ if (fileFormat == "STL" || fileFormat == "OBJ") {
504
+ const typedLoader = fileFormat == "STL" ? meshloader : objloader;
505
+ loader = fileFormat == "STL" ? new STLLoader() : new OBJLoader();
506
+ loader.crossOrigin = "Anonymous";
507
+ loader.load(url, typedLoader(region, colour, opacity, localTimeEnabled,
508
+ localMorphColour, groupName, anatomicalId, renderOrder, options ? options : {},
509
+ finishCallback), this.onProgress(url), this.onError(finishCallback));
510
+ return;
472
511
  }
473
512
  if (isInline) {
474
513
  const object = primitivesLoader.parse( url );
@@ -554,7 +593,12 @@ const SceneLoader = function (sceneIn) {
554
593
  }
555
594
  if (newTexture) {
556
595
  newTexture.groupName = groupName;
596
+ const isStale = createStaleCheck();
557
597
  let myCallback = () => {
598
+ if (isStale()) {
599
+ newTexture.dispose();
600
+ return;
601
+ }
558
602
  //Add zincObject after it has sort out all the required download
559
603
  region.addZincObject(newTexture);
560
604
  --this.toBeDownloaded;
@@ -582,6 +626,7 @@ const SceneLoader = function (sceneIn) {
582
626
  loadTexture(region, undefined, url, groupName, finishCallback, options);
583
627
  } else {
584
628
  const requestURL = url;
629
+ const isStale = createStaleCheck();
585
630
  fetch(requestURL)
586
631
  .then((response) => {
587
632
  if (!response.ok) {
@@ -591,6 +636,7 @@ const SceneLoader = function (sceneIn) {
591
636
  return response.json().then((textureData) => ({ textureData, referenceURL }));
592
637
  })
593
638
  .then(({textureData, referenceURL}) => {
639
+ if (isStale()) return;
594
640
  loadTexture(region, referenceURL, textureData, groupName, finishCallback, options);
595
641
  })
596
642
  .catch((error) => {
@@ -662,14 +708,19 @@ const SceneLoader = function (sceneIn) {
662
708
  options,
663
709
  finishCallback
664
710
  ) => {
711
+ const isStale = createStaleCheck();
665
712
  return (geometry, materials) => {
713
+ if (isStale()) {
714
+ geometry.dispose();
715
+ return;
716
+ }
666
717
  let material = undefined;
667
718
  if (materials && materials[0]) {
668
719
  material = materials[0];
669
720
  }
670
721
  const zincGeometry = addZincGeometry(region, geometry, colour, opacity,
671
722
  localTimeEnabled, localMorphColour, undefined, material, groupName, renderOrder, anatomicalId);
672
- if (options.lod && options.lod.levels) {
723
+ if (options?.lod?.levels) {
673
724
  for (const [key, value] of Object.entries(options.lod.levels)) {
674
725
  zincGeometry.addLOD(primitivesLoader, key, value.URL, value.Index, options.lod.preload);
675
726
  }
@@ -682,6 +733,48 @@ const SceneLoader = function (sceneIn) {
682
733
  };
683
734
  }
684
735
 
736
+ //Internal loader for OBJ files, OBJLoader returns a group and each mesh
737
+ //in it is added as a separate zinc geometry.
738
+ const objloader = (
739
+ region,
740
+ colour,
741
+ opacity,
742
+ localTimeEnabled,
743
+ localMorphColour,
744
+ groupName,
745
+ anatomicalId,
746
+ renderOrder,
747
+ options,
748
+ finishCallback
749
+ ) => {
750
+ const isStale = createStaleCheck();
751
+ const onMeshLoaded = meshloader(region, colour, opacity, localTimeEnabled,
752
+ localMorphColour, groupName, anatomicalId, renderOrder, options, finishCallback);
753
+ return (group) => {
754
+ const meshes = [];
755
+ group.traverse((child) => {
756
+ if (child.isMesh && child.geometry) {
757
+ meshes.push(child);
758
+ }
759
+ if (child.material) {
760
+ const materials = Array.isArray(child.material) ? child.material : [child.material];
761
+ materials.forEach((material) => material.dispose());
762
+ }
763
+ });
764
+ if (isStale()) {
765
+ meshes.forEach((mesh) => mesh.geometry.dispose());
766
+ return;
767
+ }
768
+ if (meshes.length === 0) {
769
+ --this.toBeDownloaded;
770
+ return;
771
+ }
772
+ //meshloader decrements the counter once per mesh
773
+ this.toBeDownloaded += meshes.length - 1;
774
+ meshes.forEach((mesh) => onMeshLoaded(mesh.geometry));
775
+ };
776
+ }
777
+
685
778
  //Turn ISO 8601 duration string into an array.
686
779
  const parseDuration = (durationString) => {
687
780
  const regex = /P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/;
@@ -831,7 +924,8 @@ const SceneLoader = function (sceneIn) {
831
924
  */
832
925
  this.loadGLTF = (region, url, finishCallback, allCompletedCallback, options) => {
833
926
  const loader = new GLTFToZincJSLoader();
834
- loader.load(scene, region, url, finishCallback, allCompletedCallback, options);
927
+ loader.load(scene, region, url, finishCallback, allCompletedCallback, options,
928
+ createStaleCheck());
835
929
  }
836
930
 
837
931
  let loadRegions = (currentRegion, referenceURL, regions, callback) => {
@@ -962,9 +1056,9 @@ const SceneLoader = function (sceneIn) {
962
1056
  // view file does not receive callback
963
1057
  let callback = new metaFinishCallback(numberOfObjects, finishCallback, allCompletedCallback);
964
1058
  // Prioritise the view file and settings before loading anything else
965
- for (let i = 0; i < metadata.length; i++)
1059
+ for (let i = 0; i < filteredMetada.length; i++)
966
1060
  readViewAndSettingsItem(referenceURL, filteredMetada[i], callback);
967
- for (let i = 0; i < metadata.length; i++) {
1061
+ for (let i = 0; i < filteredMetada.length; i++) {
968
1062
  readVersionOneRegionPath(targetRegion, referenceURL, filteredMetada[i], i, callback);
969
1063
  }
970
1064
  }
@@ -993,6 +1087,7 @@ const SceneLoader = function (sceneIn) {
993
1087
  */
994
1088
  this.loadMetadataURL = (targetRegion, url, finishCallback, allCompletedCallback, options) => {
995
1089
  const requestURL = url;
1090
+ const isStale = createStaleCheck();
996
1091
  fetch(requestURL)
997
1092
  .then((response) => {
998
1093
  if (!response.ok) {
@@ -1002,6 +1097,7 @@ const SceneLoader = function (sceneIn) {
1002
1097
  return response.json().then((metadata) => ({ metadata, referenceURL }));
1003
1098
  })
1004
1099
  .then(({metadata, referenceURL}) => {
1100
+ if (isStale()) return;
1005
1101
  scene.resetMetadata();
1006
1102
  scene.resetDuration();
1007
1103
  viewLoaded = false;
@@ -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
+ };
@@ -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 };