zincjs 3.0.1 → 3.0.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zincjs",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
4
4
  "description": "ZincJS (Web-based-Zinc-Visualisation)",
5
5
  "type": "module",
6
6
  "main": "build/zinc.js",
@@ -1,5 +1,11 @@
1
1
  import * as THREE from 'three/webgpu';
2
- import { keepIndexType, toBufferGeometry, updateMorphColorAttribute } from '../utilities';
2
+ import {
3
+ keepIndexType,
4
+ extractVertices,
5
+ getTubeLinesGeometry,
6
+ toBufferGeometry,
7
+ updateMorphColorAttribute,
8
+ } from '../utilities';
3
9
  import { LineSegments } from '../three/line/LineSegments';
4
10
 
5
11
  /**
@@ -28,6 +34,9 @@ const LOD = function (parent) {
28
34
  this._parent = parent;
29
35
  //Levels loaded after dispose are discarded
30
36
  this._disposed = false;
37
+ this._dirty = true;
38
+ this._bandMin = 0;
39
+ this._bandMax = -1;
31
40
 
32
41
  /*
33
42
  * Add a level of LOD at the specified distance
@@ -224,7 +233,7 @@ const LOD = function (parent) {
224
233
  const options = {
225
234
  localTimeEnabled: this._parent.timeEnabled,
226
235
  localMorphColour: this._parent.morphColour,
227
- isLines: this._parent.isLines,
236
+ isLines: this._parent.isLines || this._parent.isTubeLines,
228
237
  };
229
238
  const geometry = toBufferGeometry(geometryIn, options);
230
239
  let mesh = undefined;
@@ -232,6 +241,15 @@ const LOD = function (parent) {
232
241
  mesh = new THREE.Mesh(geometry, material);
233
242
  } else if (this._parent.isLines) {
234
243
  mesh = new LineSegments(geometry, material);
244
+ } else if (this._parent.isTubeLines) {
245
+ const { radius, radialSegments, smooth } = this._parent.getGeometryConfig();
246
+ const tubeGeometry = getTubeLinesGeometry(
247
+ extractVertices(geometry),
248
+ radius,
249
+ radialSegments,
250
+ smooth,
251
+ );
252
+ mesh = new THREE.Mesh(tubeGeometry, material);
235
253
  }
236
254
  mesh.userData = this._parent;
237
255
  mesh.renderOrder = this._renderOrder;
@@ -348,50 +366,35 @@ const LOD = function (parent) {
348
366
  /* Update layers based on the */
349
367
  this.update = (camera, center) => {
350
368
  const levels = this.levels;
351
- if (levels.length > 1) {
352
- const distance = camera.cameraObject.position.distanceTo(center);
353
- let visibleIndex = -1;
354
- let optimalIndex = -1;
355
- let i, l;
356
- //Found a visible index that is within range of the LOD
357
- for (i = 0, l = levels.length; i < l; i++) {
358
- if (distance >= levels[i].distance) {
359
- //Check if a level is loading
360
- if (levels[i].morph) {
361
- if (visibleIndex > -1 && levels[visibleIndex].morph) {
362
- levels[visibleIndex].morph.visible = false;
363
- }
364
- visibleIndex = i;
365
- levels[i].morph.visible = true;
366
- optimalIndex = -1;
367
- } else {
368
- optimalIndex = i;
369
- }
370
- } else {
371
- break;
372
- }
373
- }
374
- if (optimalIndex > -1) {
375
- this.loadLevel(optimalIndex);
376
- }
377
- for (; i < l; i++) {
378
- if (levels[i].morph) {
379
- //Set visibility of other morph to false
380
- //and set the closest lod to true if
381
- //none is found
382
- if (visibleIndex > -1) {
383
- levels[i].morph.visible = false;
384
- } else {
385
- levels[i].morph.visible = true;
386
- visibleIndex = i;
387
- }
388
- }
389
- }
390
- if (this._currentLevel != visibleIndex) {
391
- this._currentLevel = visibleIndex;
392
- this.checkTransparentMesh();
393
- this._parent?.requestRender?.();
394
- }
369
+ const l = levels.length;
370
+ if (l < 2) return;
371
+ const distance = camera.cameraObject.position.distanceTo(center);
372
+
373
+ // Still in the same band and nothing new has loaded
374
+ if (!this._dirty && distance >= this._bandMin && distance < this._bandMax) return;
375
+ this._dirty = false;
376
+
377
+ // Furthest level whose threshold has been passed (levels are sorted by distance)
378
+ let target = -1;
379
+ while (target + 1 < l && distance >= levels[target + 1].distance) target++;
380
+ this._bandMin = target >= 0 ? levels[target].distance : -Infinity;
381
+ this._bandMax = target + 1 < l ? levels[target + 1].distance : Infinity;
382
+
383
+ if (target > -1 && !levels[target].morph) this.loadLevel(target);
384
+
385
+ // Show the nearest loaded level at or below target, else the nearest loaded one above
386
+ let visible = -1;
387
+ for (let i = target; i >= 0 && visible < 0; i--) if (levels[i].morph) visible = i;
388
+ for (let i = target + 1; i < l && visible < 0; i++) if (levels[i].morph) visible = i;
389
+
390
+ for (let i = 0; i < l; i++) {
391
+ if (levels[i].morph) levels[i].morph.visible = i === visible;
392
+ }
393
+
394
+ if (this._currentLevel !== visible) {
395
+ this._currentLevel = visible;
396
+ this.checkTransparentMesh();
397
+ this._parent?.requestRender?.();
395
398
  }
396
399
  };
397
400
 
@@ -52,6 +52,9 @@ const Marker = function (zincObject) {
52
52
  let initialise = () => {
53
53
  sprite = new THREE.Sprite(spriteMaterial);
54
54
  sprite.center.set(0.5, 0);
55
+ //Culling sphere ignores sizeAttenuation: false, it may cull sprite
56
+ //and label independently
57
+ sprite.frustumCulled = false;
55
58
  this.morph.add(sprite);
56
59
  this.morph.position.set(0, 0, 0);
57
60
  this.morph.renderOrder = 10000;
@@ -133,6 +136,7 @@ const Marker = function (zincObject) {
133
136
  }
134
137
  userSprite = new THREE.Sprite(userMaterial);
135
138
  userSprite.center.set(0.5, 0);
139
+ userSprite.frustumCulled = false;
136
140
  userSprite.scale.set(size[0], size[1], size[2]);
137
141
  userSprite.userData = this;
138
142
  this.setUserSprite();
@@ -201,6 +205,7 @@ const Marker = function (zincObject) {
201
205
  if (!label && numberIn) {
202
206
  label = createNewSpriteText(numberIn, 0.012, 'black', 'Asap', 120, 700);
203
207
  label.renderOrder = 10001;
208
+ label.frustumCulled = false;
204
209
  this.morph.add(label);
205
210
  }
206
211
  number = numberIn;
@@ -74,6 +74,9 @@ const MarkerCluster = function (sceneIn) {
74
74
  const sprite = new THREE.Sprite(spriteMaterial);
75
75
  sprite.clusterIndex = index;
76
76
  sprite.center.set(0.5, 0);
77
+ //Culling sphere ignores sizeAttenuation: false, it may cull sprite
78
+ //and label independently
79
+ sprite.frustumCulled = false;
77
80
  sprite.position.set(0, 0, 0);
78
81
  sprite.renderOrder = 10000;
79
82
  sprite.scale.set(size[0], size[1], size[2]);
@@ -102,6 +105,7 @@ const MarkerCluster = function (sceneIn) {
102
105
  }
103
106
  sprite.label = createNewSpriteText(number, 0.012, 'black', 'Asap', 120, 700);
104
107
  sprite.label.renderOrder = 10001;
108
+ sprite.label.frustumCulled = false;
105
109
  sprite.number = number;
106
110
  sprite.group.add(sprite.label);
107
111
  }
@@ -177,6 +181,7 @@ const MarkerCluster = function (sceneIn) {
177
181
  const current = Date.now();
178
182
  if (current - start > 500) {
179
183
  let clusters = [];
184
+ //Make a shallow copy of the markers array
180
185
  getCluster({ ...this.markers }, clusters);
181
186
  drawClusters(clusters);
182
187
  scene?.invalidate?.();
@@ -336,6 +336,20 @@ const TextureSlides = function (textureIn) {
336
336
  return this.cachedBoundingBox;
337
337
  };
338
338
 
339
+ /**
340
+ * Get the normalised coordinates of a point in world coordinates.
341
+ * The returned values can be used as the value of {@link SLIDE_SETTINGS}.
342
+ * Values within [0, 1] are inside the texture box.
343
+ *
344
+ * @param {Array} position - World coordinates [x, y, z].
345
+ * @return {Array} - Normalised coordinates [x, y, z].
346
+ */
347
+ this.getPositionInBox = (position) => {
348
+ const matrix = updateWorldMatrixFromAncestors(this.morph).clone().invert();
349
+ const vector = new THREE.Vector3(...position).applyMatrix4(matrix);
350
+ return vector.toArray();
351
+ };
352
+
339
353
  this.applyTransformation = (rotation, position, scale) => {
340
354
  this.requestRender();
341
355
  const matrix = new THREE.Matrix4();
@@ -1,5 +1,5 @@
1
1
  import * as THREE from 'three/webgpu';
2
- import { mergeGeometries } from '../utilities';
2
+ import { extractVertices, getTubeLinesGeometry } from '../utilities';
3
3
  import { ZincObject } from './zincObject';
4
4
 
5
5
  /**
@@ -31,13 +31,27 @@ const TubeLines = function () {
31
31
  this.createLineSegment = (geometryIn, materialIn, options) => {
32
32
  if (geometryIn && materialIn) {
33
33
  dataIn = { geometryIn, materialIn, options };
34
- const geometry = getTubeLinesGeometry(extractVertices(geometryIn));
34
+ const { radius, radialSegments, smooth } = geometryConfig;
35
+ const geometry = getTubeLinesGeometry(
36
+ extractVertices(geometryIn),
37
+ radius,
38
+ radialSegments,
39
+ smooth,
40
+ );
35
41
  const material = new THREE.MeshStandardMaterial({ color: materialIn.color });
36
42
  const mesh = new THREE.Mesh(geometry, material);
37
43
  this.setMesh(mesh, options.localTimeEnabled, options.localMorphColour);
38
44
  }
39
45
  };
40
46
 
47
+ /**
48
+ * Return the settings used for creating the tube line.
49
+ *
50
+ */
51
+ this.getGeometryConfig = () => {
52
+ return geometryConfig;
53
+ };
54
+
41
55
  /**
42
56
  * Set the width for the lines.
43
57
  *
@@ -90,50 +104,13 @@ const TubeLines = function () {
90
104
  mesh.geometry.dispose();
91
105
 
92
106
  geometryConfig = Object.assign(geometryConfig, { radius, radialSegments });
93
- mesh.geometry = getTubeLinesGeometry(extractVertices(geometryIn));
94
- }
95
- };
96
-
97
- /**
98
- * Read the position attribute of a BufferGeometry into an array of
99
- * THREE.Vector3, since TubeGeometry/CatmullRomCurve3/LineCurve3 need
100
- * actual Vector3 instances to work with, not a flat typed array.
101
- *
102
- * @param {THREE.BufferGeometry} geometry
103
- * @returns {Array}
104
- */
105
- const extractVertices = (geometry) => {
106
- const position = geometry.getAttribute('position');
107
- const vertices = [];
108
- for (let i = 0; i < position.count; i++) {
109
- vertices.push(new THREE.Vector3().fromBufferAttribute(position, i));
110
- }
111
- return vertices;
112
- };
113
-
114
- /**
115
- * Get merged geometry from list of geometry vertices
116
- *
117
- * @param {Array} vertices - An array of THREE.Vector3 vertices.
118
- * @returns {Object}
119
- */
120
- const getTubeLinesGeometry = (vertices) => {
121
- const { radius, radialSegments, smooth } = geometryConfig;
122
- let finalGeometry;
123
- if (smooth) {
124
- const curve = new THREE.CatmullRomCurve3(vertices);
125
- finalGeometry = new THREE.TubeGeometry(curve, vertices.length, radius, radialSegments, false);
126
- } else {
127
- const geometries = [];
128
- for (let i = 0; i + 1 < vertices.length; i = i + 2) {
129
- const curve = new THREE.LineCurve3(vertices[i], vertices[i + 1]);
130
- const tubeGeometry = new THREE.TubeGeometry(curve, 1, radius, radialSegments, false);
131
- geometries.push(tubeGeometry);
132
- }
133
- finalGeometry = mergeGeometries(geometries, true);
134
- geometries.forEach((g) => g.dispose());
107
+ mesh.geometry = getTubeLinesGeometry(
108
+ extractVertices(geometryIn),
109
+ radius,
110
+ radialSegments,
111
+ geometryConfig.smooth,
112
+ );
135
113
  }
136
- return finalGeometry;
137
114
  };
138
115
  };
139
116
 
package/src/renderer.js CHANGED
@@ -38,7 +38,6 @@ const Renderer = function (containerIn) {
38
38
  let contextLostCallbackFunctions_id = 0;
39
39
  let contextRestoredCallbackFunctions = {};
40
40
  let contextRestoredCallbackFunctions_id = 0;
41
- let animated_id = undefined;
42
41
  let cameraOrtho = undefined,
43
42
  sceneOrtho = undefined,
44
43
  logoSprite = undefined;
@@ -179,6 +178,8 @@ const Renderer = function (containerIn) {
179
178
  }
180
179
  renderer = new THREE.WebGPURenderer(parameters);
181
180
  await renderer.init();
181
+ //animate() may have been called before the renderer was ready
182
+ if (isRendering) renderer.setAnimationLoop(this.render);
182
183
  //GPU glyph animation requires the WebGPU backend, the WebGL fallback
183
184
  //cannot index storage buffers freely.
184
185
  setGlyphComputeSupported(renderer.backend?.isWebGPUBackend === true);
@@ -378,22 +379,13 @@ const Renderer = function (containerIn) {
378
379
  currentScene.updateDirectionalLight();
379
380
  };
380
381
 
381
- let runAnimation = () => {
382
- if (isRendering) {
383
- animated_id = requestAnimationFrame(runAnimation);
384
- this.render();
385
- } else {
386
- cancelAnimationFrame(animated_id);
387
- animated_id = undefined;
388
- }
389
- };
390
-
391
382
  /**
392
383
  * Stop the animation and renderer to get into the render loop.
393
384
  */
394
385
  this.stopAnimate = () => {
395
386
  if (isRendering) {
396
387
  isRendering = false;
388
+ renderer?.setAnimationLoop(null);
397
389
  }
398
390
  };
399
391
 
@@ -405,7 +397,10 @@ const Renderer = function (containerIn) {
405
397
  clock.reset();
406
398
  isRendering = true;
407
399
  needsRender = true;
408
- runAnimation();
400
+ //WebGPURenderer always runs its own requestAnimationFrame loop which
401
+ //updates the node frame and the per frame info, render from that loop
402
+ //instead of running a second one.
403
+ renderer?.setAnimationLoop(this.render);
409
404
  }
410
405
  };
411
406
 
@@ -646,7 +641,16 @@ const Renderer = function (containerIn) {
646
641
  }
647
642
  clock.update();
648
643
  const delta = clock.getDelta();
649
- let changed = currentScene.renderGeometries(playRate, delta, this.playAnimation);
644
+ //Objects are only updated on every frame when render on demand is off
645
+ //or the renderer itself has requested a frame, e.g. on resize
646
+ const forceUpdate = !renderOnDemand || needsRender;
647
+ let changed = currentScene.renderGeometries(
648
+ playRate,
649
+ delta,
650
+ this.playAnimation,
651
+ undefined,
652
+ forceUpdate,
653
+ );
650
654
  if (additionalActiveScenes.length > 0) {
651
655
  //Additional scenes are displayed with the current scene's camera
652
656
  sharedCamera.controls = currentScene.getZincCameraControls();
@@ -654,7 +658,9 @@ const Renderer = function (containerIn) {
654
658
  }
655
659
  for (let i = 0; i < additionalActiveScenes.length; i++) {
656
660
  const sceneItem = additionalActiveScenes[i];
657
- if (sceneItem.renderGeometries(playRate, delta, this.playAnimation, sharedCamera))
661
+ if (
662
+ sceneItem.renderGeometries(playRate, delta, this.playAnimation, sharedCamera, forceUpdate)
663
+ )
658
664
  changed = true;
659
665
  }
660
666
  for (let key in preRenderCallbackFunctions) {
@@ -756,7 +762,6 @@ const Renderer = function (containerIn) {
756
762
  * Dispose all memory allocated, this will effetively destroy all scenes.
757
763
  */
758
764
  this.dispose = () => {
759
- if (isRendering) cancelAnimationFrame(animated_id);
760
765
  for (const key in sceneMap) {
761
766
  if (sceneMap.hasOwnProperty(key)) {
762
767
  sceneMap[key].clearAll();
package/src/scene.js CHANGED
@@ -624,11 +624,25 @@ const Scene = function (containerIn, rendererIn) {
624
624
  * Update geometries and glyphsets based on the calculated time.
625
625
  * @private
626
626
  */
627
- this.renderGeometries = (playRate, delta, playAnimation, sharedCamera) => {
627
+ this.renderGeometries = (playRate, delta, playAnimation, sharedCamera, forceUpdate) => {
628
628
  //Anything requested since the last frame
629
629
  let changed = needsRender;
630
630
  needsRender = false;
631
631
  cameraUpdated = false;
632
+ //These are plain properties, check if they have changed before
633
+ //the objects are updated so markers are toggled on this frame
634
+ if (
635
+ lastDisplayState.displayMarkers !== this.displayMarkers ||
636
+ lastDisplayState.displayMinimap !== this.displayMinimap ||
637
+ lastDisplayState.displayMiniAxes !== this.displayMiniAxes ||
638
+ lastDisplayState.playAnimation !== playAnimation
639
+ ) {
640
+ lastDisplayState.displayMarkers = this.displayMarkers;
641
+ lastDisplayState.displayMinimap = this.displayMinimap;
642
+ lastDisplayState.displayMiniAxes = this.displayMiniAxes;
643
+ lastDisplayState.playAnimation = playAnimation;
644
+ changed = true;
645
+ }
632
646
  //Additional active scenes are displayed with the current scene's camera
633
647
  const cameraControls = sharedCamera ? sharedCamera.controls : zincCameraControls;
634
648
  //Reused on every frame to avoid allocations
@@ -680,25 +694,30 @@ const Scene = function (containerIn, rendererIn) {
680
694
  zincCameraControls.calculateHeightPerPixelAtZeroDepth(getDrawingHeight());
681
695
  }
682
696
  }
683
- rootRegion.renderGeometries(playRate, delta, playAnimation, cameraControls, options, true);
697
+ //Walking every object is costly for large scenes, only do it when
698
+ //something may have changed, LOD and markers depend on the camera
699
+ if (
700
+ forceUpdate ||
701
+ changed ||
702
+ options.ndcToBeUpdated ||
703
+ playAnimation ||
704
+ //Clusters are recalculated during the walk
705
+ (markerCluster.isEnabled() && markerCluster.markerUpdateRequired)
706
+ ) {
707
+ rootRegion.renderGeometries(
708
+ playRate,
709
+ delta,
710
+ playAnimation,
711
+ cameraControls,
712
+ options,
713
+ true,
714
+ );
715
+ }
684
716
  } else if (!sharedCamera && zincCameraControls.update(0)) {
685
717
  changed = true;
686
718
  cameraUpdated = true;
687
719
  }
688
720
  }
689
- //These are plain properties, check if they have changed
690
- if (
691
- lastDisplayState.displayMarkers !== this.displayMarkers ||
692
- lastDisplayState.displayMinimap !== this.displayMinimap ||
693
- lastDisplayState.displayMiniAxes !== this.displayMiniAxes ||
694
- lastDisplayState.playAnimation !== playAnimation
695
- ) {
696
- lastDisplayState.displayMarkers = this.displayMarkers;
697
- lastDisplayState.displayMinimap = this.displayMinimap;
698
- lastDisplayState.displayMiniAxes = this.displayMiniAxes;
699
- lastDisplayState.playAnimation = playAnimation;
700
- changed = true;
701
- }
702
721
  //The flag is only cleared when the minimap is drawn
703
722
  if (this.minimapScissor.updateRequired && (this.displayMinimap || this.displayMiniAxes)) {
704
723
  changed = true;
package/src/utilities.js CHANGED
@@ -31,6 +31,23 @@ function createNewURL(target, reference) {
31
31
  }
32
32
  }
33
33
 
34
+ /**
35
+ * Read the position attribute of a BufferGeometry into an array of
36
+ * THREE.Vector3, since TubeGeometry/CatmullRomCurve3/LineCurve3 need
37
+ * actual Vector3 instances to work with, not a flat typed array.
38
+ *
39
+ * @param {THREE.BufferGeometry} geometry
40
+ * @returns {Array}
41
+ */
42
+ const extractVertices = (geometry) => {
43
+ const position = geometry.getAttribute('position');
44
+ const vertices = [];
45
+ for (let i = 0; i < position.count; i++) {
46
+ vertices.push(new THREE.Vector3().fromBufferAttribute(position, i));
47
+ }
48
+ return vertices;
49
+ };
50
+
34
51
  /*
35
52
  * Compute the world matrix of an object from its ancestors and return it.
36
53
  * three.js only recomputes world matrices flagged by matrixWorldNeedsUpdate,
@@ -85,6 +102,30 @@ function getBoundingBox(mesh, cachedBox, b1, v1, v2) {
85
102
  cachedBox.applyMatrix4(updateWorldMatrixFromAncestors(mesh));
86
103
  }
87
104
 
105
+ /**
106
+ * Create tube line geometry based on lines geometry, radius, segments and smooth.
107
+ *
108
+ * @param {Array} vertices - An array of THREE.Vector3 vertices.
109
+ * @returns {Object}
110
+ */
111
+ function getTubeLinesGeometry(vertices, radius, radialSegments, smooth) {
112
+ let finalGeometry;
113
+ if (smooth) {
114
+ const curve = new THREE.CatmullRomCurve3(vertices);
115
+ finalGeometry = new THREE.TubeGeometry(curve, vertices.length, radius, radialSegments, false);
116
+ } else {
117
+ const geometries = [];
118
+ for (let i = 0; i + 1 < vertices.length; i = i + 2) {
119
+ const curve = new THREE.LineCurve3(vertices[i], vertices[i + 1]);
120
+ const tubeGeometry = new THREE.TubeGeometry(curve, 1, radius, radialSegments, false);
121
+ geometries.push(tubeGeometry);
122
+ }
123
+ finalGeometry = mergeGeometries(geometries, true);
124
+ geometries.forEach((g) => g.dispose());
125
+ }
126
+ return finalGeometry;
127
+ }
128
+
88
129
  //Convenient function
89
130
  function loadExternalFile(url, data, callback, errorCallback) {
90
131
  // Set up an asynchronous request
@@ -908,8 +949,10 @@ export {
908
949
  createBufferGeometry,
909
950
  createNewSpriteText,
910
951
  createNewURL,
952
+ extractVertices,
911
953
  getBoundingBox,
912
954
  getCircularTexture,
955
+ getTubeLinesGeometry,
913
956
  keepIndexType,
914
957
  updateWorldMatrixFromAncestors,
915
958
  useStandardIndexType,