@niivue/niivue 0.44.2-dev.1 → 0.44.2-dev.2

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/build/index.js CHANGED
@@ -10,7 +10,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
10
10
  import { mat4 as mat46, vec2, vec3 as vec311, vec4 as vec48 } from "gl-matrix";
11
11
 
12
12
  // package.json
13
- var version = "0.44.2-dev.1";
13
+ var version = "0.45.1";
14
14
 
15
15
  // src/logger.ts
16
16
  var _Log = class _Log {
@@ -806,46 +806,6 @@ out vec4 color;
806
806
  void main() {
807
807
  color = lineColor;
808
808
  }`;
809
- var fragRectOutlineShader = `#version 300 es
810
- #line 723
811
- precision highp int;
812
- precision highp float;
813
-
814
- uniform vec4 lineColor;
815
- uniform vec4 leftTopWidthHeight;
816
- uniform float thickness; // line thickness in pixels
817
- uniform vec2 canvasWidthHeight;
818
-
819
- out vec4 color;
820
-
821
- void main() {
822
- // fragment position in screen coordinates
823
- vec2 fragCoord = gl_FragCoord.xy;
824
-
825
- // canvas height
826
- float canvasHeight = canvasWidthHeight.y;
827
-
828
- // 'top' and 'bottom' to match gl_FragCoord.y coordinate system
829
- float top = canvasHeight - leftTopWidthHeight.y;
830
- float bottom = top - leftTopWidthHeight.w;
831
-
832
- // left and right edges
833
- float left = leftTopWidthHeight.x;
834
- float right = left + leftTopWidthHeight.z;
835
-
836
- bool withinLeft = fragCoord.x >= left && fragCoord.x <= left + thickness;
837
- bool withinRight = fragCoord.x <= right && fragCoord.x >= right - thickness;
838
- bool withinTop = fragCoord.y <= top && fragCoord.y >= top - thickness;
839
- bool withinBottom = fragCoord.y >= bottom && fragCoord.y <= bottom + thickness;
840
-
841
- bool isOutline = withinLeft || withinRight || withinTop || withinBottom;
842
-
843
- if (isOutline) {
844
- color = lineColor;
845
- } else {
846
- discard;
847
- }
848
- }`;
849
809
  var vertColorbarShader = `#version 300 es
850
810
  #line 490
851
811
  layout(location=0) in vec3 pos;
@@ -22290,7 +22250,6 @@ var DRAG_MODE = /* @__PURE__ */ ((DRAG_MODE2) => {
22290
22250
  DRAG_MODE2[DRAG_MODE2["pan"] = 3] = "pan";
22291
22251
  DRAG_MODE2[DRAG_MODE2["slicer3D"] = 4] = "slicer3D";
22292
22252
  DRAG_MODE2[DRAG_MODE2["callbackOnly"] = 5] = "callbackOnly";
22293
- DRAG_MODE2[DRAG_MODE2["roiSelection"] = 6] = "roiSelection";
22294
22253
  return DRAG_MODE2;
22295
22254
  })(DRAG_MODE || {});
22296
22255
  var DEFAULT_OPTIONS = {
@@ -22360,27 +22319,10 @@ var DEFAULT_OPTIONS = {
22360
22319
  renderOverlayBlend: 1,
22361
22320
  sliceMosaicString: "",
22362
22321
  centerMosaic: false,
22363
- penSize: 1,
22364
- // in voxels, since all drawing is done using bitmap indices
22365
22322
  clickToSegment: false,
22366
- clickToSegmentRadius: 3,
22367
- // in mm
22368
- clickToSegmentBright: true,
22369
- clickToSegmentAutoIntensity: false,
22370
- // new option, but keep clickToSegmentBright for backwards compatibility
22371
- clickToSegmentIntensityMax: NaN,
22372
- // NaN will use auto threshold (default flood fill behavior from before)
22373
- clickToSegmentIntensityMin: NaN,
22374
- // NaN will use auto threshold (default flood fill behavior from before)
22375
- // 0 will use auto threshold (default flood fill behavior from before)
22376
- // Take the voxel intensity at the click point and use this percentage +/- to threshold the flood fill operation.
22377
- // If greater than 0, clickedVoxelIntensity +/- clickedVoxelIntensity * clickToSegmentPercent will be used
22378
- // for the clickToSegmentIntensityMin and clickToSegmentIntensityMax values.
22379
- clickToSegmentPercent: 0,
22380
- clickToSegmentMaxDistanceMM: Number.POSITIVE_INFINITY,
22381
- // default value is infinity for backwards compatibility with flood fill routine.
22382
- selectionBoxLineThickness: 4,
22383
- selectionBoxIsOutline: false
22323
+ clickToSegmentRadius: 2,
22324
+ clickToSegmentSteps: 10,
22325
+ clickToSegmentBright: true
22384
22326
  };
22385
22327
  var INITIAL_SCENE_DATA = {
22386
22328
  gamma: 1,
@@ -24077,11 +24019,12 @@ var defaultSaveImageOptions = {
24077
24019
  isSaveDrawing: false,
24078
24020
  volumeByIndex: 0
24079
24021
  };
24080
- var Niivue = class {
24022
+ var Niivue = class _Niivue {
24081
24023
  /**
24082
24024
  * @param options - options object to set modifiable Niivue properties
24083
24025
  */
24084
24026
  constructor(options = DEFAULT_OPTIONS) {
24027
+ __publicField(this, "loaders", {});
24085
24028
  __publicField(this, "canvas", null);
24086
24029
  // the reference to the canvas element on the page
24087
24030
  __publicField(this, "_gl", null);
@@ -24124,7 +24067,6 @@ var Niivue = class {
24124
24067
  __publicField(this, "orientCubeShader");
24125
24068
  __publicField(this, "orientCubeShaderVAO", null);
24126
24069
  __publicField(this, "rectShader");
24127
- __publicField(this, "rectOutlineShader");
24128
24070
  __publicField(this, "renderShader");
24129
24071
  __publicField(this, "lineShader");
24130
24072
  __publicField(this, "line3DShader");
@@ -24177,20 +24119,7 @@ var Niivue = class {
24177
24119
  __publicField(this, "extentsMax");
24178
24120
  // ResizeObserver
24179
24121
  __publicField(this, "resizeObserver", null);
24180
- // syncOpts: Record<string, unknown> = {}
24181
- __publicField(this, "syncOpts", {
24182
- "3d": false,
24183
- // legacy option
24184
- "2d": false,
24185
- // legacy option
24186
- zoomPan: false,
24187
- cal_min: false,
24188
- cal_max: false,
24189
- clipPlane: false,
24190
- gamma: false,
24191
- sliceType: false,
24192
- crosshair: false
24193
- });
24122
+ __publicField(this, "syncOpts", {});
24194
24123
  __publicField(this, "readyForSync", false);
24195
24124
  // UI Data
24196
24125
  __publicField(this, "uiData", {
@@ -24669,9 +24598,6 @@ var Niivue = class {
24669
24598
  * @see {@link https://niivue.github.io/niivue/features/sync.mesh.html | live demo usage}
24670
24599
  */
24671
24600
  syncWith(otherNV, syncOpts = { "2d": true, "3d": true }) {
24672
- if (!(otherNV instanceof Array)) {
24673
- otherNV = [otherNV];
24674
- }
24675
24601
  this.otherNV = otherNV;
24676
24602
  this.syncOpts = { ...syncOpts };
24677
24603
  }
@@ -24687,55 +24613,9 @@ var Niivue = class {
24687
24613
  * @see {@link https://niivue.github.io/niivue/features/sync.mesh.html | live demo usage}
24688
24614
  */
24689
24615
  broadcastTo(otherNV, syncOpts = { "2d": true, "3d": true }) {
24690
- if (!(otherNV instanceof Array)) {
24691
- otherNV = [otherNV];
24692
- }
24693
24616
  this.otherNV = otherNV;
24694
24617
  this.syncOpts = syncOpts;
24695
24618
  }
24696
- doSync3d(otherNV) {
24697
- otherNV.scene.renderAzimuth = this.scene.renderAzimuth;
24698
- otherNV.scene.renderElevation = this.scene.renderElevation;
24699
- otherNV.scene.volScaleMultiplier = this.scene.volScaleMultiplier;
24700
- }
24701
- // both crosshair and zoomPan
24702
- doSync2d(otherNV) {
24703
- const thisMM = this.frac2mm(this.scene.crosshairPos);
24704
- otherNV.scene.crosshairPos = otherNV.mm2frac(thisMM);
24705
- otherNV.scene.pan2Dxyzmm = vec48.clone(this.scene.pan2Dxyzmm);
24706
- }
24707
- doSyncGamma(otherNV) {
24708
- const thisGamma = this.scene.gamma;
24709
- const otherGamma = otherNV.scene.gamma;
24710
- if (thisGamma !== otherGamma) {
24711
- otherNV.setGamma(thisGamma);
24712
- }
24713
- }
24714
- doSyncZoomPan(otherNV) {
24715
- otherNV.scene.pan2Dxyzmm = vec48.clone(this.scene.pan2Dxyzmm);
24716
- }
24717
- doSyncCrosshair(otherNV) {
24718
- const thisMM = this.frac2mm(this.scene.crosshairPos);
24719
- otherNV.scene.crosshairPos = otherNV.mm2frac(thisMM);
24720
- }
24721
- doSyncCalMin(otherNV) {
24722
- if (this.volumes[0].cal_min !== otherNV.volumes[0].cal_min) {
24723
- otherNV.volumes[0].cal_min = this.volumes[0].cal_min;
24724
- otherNV.updateGLVolume();
24725
- }
24726
- }
24727
- doSyncCalMax(otherNV) {
24728
- if (this.volumes[0].cal_max !== otherNV.volumes[0].cal_max) {
24729
- otherNV.volumes[0].cal_max = this.volumes[0].cal_max;
24730
- otherNV.updateGLVolume();
24731
- }
24732
- }
24733
- doSyncSliceType(otherNV) {
24734
- otherNV.setSliceType(this.opts.sliceType);
24735
- }
24736
- doSyncClipPlane(otherNV) {
24737
- otherNV.setClipPlane(this.scene.clipPlaneDepthAziElev);
24738
- }
24739
24619
  /**
24740
24620
  * Sync the scene controls (orientation, crosshair location, etc.) from one Niivue instance to another. useful for using one canvas to drive another.
24741
24621
  * @internal
@@ -24752,39 +24632,46 @@ var Niivue = class {
24752
24632
  if (!this.gl.canvas.matches(":focus")) {
24753
24633
  return;
24754
24634
  }
24755
- for (let i = 0; i < this.otherNV.length; i++) {
24756
- if (this.otherNV[i] === this) {
24757
- continue;
24758
- }
24759
- if (this.syncOpts.gamma) {
24760
- this.doSyncGamma(this.otherNV[i]);
24761
- }
24762
- if (this.syncOpts.crosshair) {
24763
- this.doSyncCrosshair(this.otherNV[i]);
24764
- }
24765
- if (this.syncOpts.zoomPan) {
24766
- this.doSyncZoomPan(this.otherNV[i]);
24767
- }
24768
- if (this.syncOpts.sliceType) {
24769
- this.doSyncSliceType(this.otherNV[i]);
24770
- }
24771
- if (this.syncOpts.cal_min) {
24772
- this.doSyncCalMin(this.otherNV[i]);
24773
- }
24774
- if (this.syncOpts.cal_max) {
24775
- this.doSyncCalMax(this.otherNV[i]);
24776
- }
24777
- if (this.syncOpts.clipPlane) {
24778
- this.doSyncClipPlane(this.otherNV[i]);
24635
+ const thisMM = this.frac2mm(this.scene.crosshairPos);
24636
+ if (this.otherNV instanceof _Niivue) {
24637
+ const thisGamma = this.scene.gamma;
24638
+ const otherGamma = this.otherNV.scene.gamma;
24639
+ if (thisGamma !== otherGamma) {
24640
+ this.otherNV.setGamma(thisGamma);
24779
24641
  }
24780
24642
  if (this.syncOpts["2d"]) {
24781
- this.doSync2d(this.otherNV[i]);
24643
+ this.otherNV.scene.crosshairPos = this.otherNV.mm2frac(thisMM);
24644
+ this.otherNV.scene.pan2Dxyzmm = vec48.clone(this.scene.pan2Dxyzmm);
24782
24645
  }
24783
24646
  if (this.syncOpts["3d"]) {
24784
- this.doSync3d(this.otherNV[i]);
24647
+ this.otherNV.scene.renderAzimuth = this.scene.renderAzimuth;
24648
+ this.otherNV.scene.renderElevation = this.scene.renderElevation;
24649
+ this.otherNV.scene.volScaleMultiplier = this.scene.volScaleMultiplier;
24650
+ }
24651
+ this.otherNV.drawScene();
24652
+ this.otherNV.createOnLocationChange();
24653
+ } else if (Array.isArray(this.otherNV)) {
24654
+ for (let i = 0; i < this.otherNV.length; i++) {
24655
+ if (this.otherNV[i] === this) {
24656
+ continue;
24657
+ }
24658
+ const thisGamma = this.scene.gamma;
24659
+ const otherGamma = this.otherNV[i].scene.gamma;
24660
+ if (thisGamma !== otherGamma) {
24661
+ this.otherNV[i].setGamma(thisGamma);
24662
+ }
24663
+ if (this.syncOpts["2d"]) {
24664
+ this.otherNV[i].scene.crosshairPos = this.otherNV[i].mm2frac(thisMM);
24665
+ this.otherNV[i].scene.pan2Dxyzmm = vec48.clone(this.scene.pan2Dxyzmm);
24666
+ }
24667
+ if (this.syncOpts["3d"]) {
24668
+ this.otherNV[i].scene.renderAzimuth = this.scene.renderAzimuth;
24669
+ this.otherNV[i].scene.renderElevation = this.scene.renderElevation;
24670
+ this.otherNV[i].scene.volScaleMultiplier = this.scene.volScaleMultiplier;
24671
+ }
24672
+ this.otherNV[i].drawScene();
24673
+ this.otherNV[i].createOnLocationChange();
24785
24674
  }
24786
- this.otherNV[i].drawScene();
24787
- this.otherNV[i].createOnLocationChange();
24788
24675
  }
24789
24676
  }
24790
24677
  /** Not documented publicly for now
@@ -25101,9 +24988,6 @@ var Niivue = class {
25101
24988
  const fracStart = this.canvasPos2frac([this.uiData.dragStart[0], this.uiData.dragStart[1]]);
25102
24989
  const fracEnd = this.canvasPos2frac([this.uiData.dragEnd[0], this.uiData.dragEnd[1]]);
25103
24990
  this.generateMouseUpCallback(fracStart, fracEnd);
25104
- if (this.opts.dragMode === 6 /* roiSelection */) {
25105
- return;
25106
- }
25107
24991
  if (this.opts.dragMode !== 1 /* contrast */) {
25108
24992
  return;
25109
24993
  }
@@ -25455,6 +25339,11 @@ var Niivue = class {
25455
25339
  if (ext === "GZ") {
25456
25340
  ext = re.exec(fullname.slice(0, -3))[1];
25457
25341
  ext = ext.toUpperCase();
25342
+ } else if (ext === "CBOR") {
25343
+ const endExt = ext;
25344
+ ext = re.exec(fullname.slice(0, -5))[1];
25345
+ ext = ext.toUpperCase();
25346
+ ext = `${ext}.${endExt}`;
25458
25347
  }
25459
25348
  return upperCase ? ext : ext.toLowerCase();
25460
25349
  }
@@ -25475,6 +25364,28 @@ var Niivue = class {
25475
25364
  }
25476
25365
  async addVolumesFromUrl(imageOptionsArray) {
25477
25366
  const promises = imageOptionsArray.map(async (imageItem) => {
25367
+ const ext = this.getFileExt(imageItem.name || imageItem.url);
25368
+ if (this.loaders[ext]) {
25369
+ let itemToLoad = imageItem.url;
25370
+ const toExt = this.loaders[ext].toExt;
25371
+ let name = imageItem.name || imageItem.url;
25372
+ name = name.split("/").pop();
25373
+ if (typeof imageItem.url === "string") {
25374
+ const url = imageItem.url;
25375
+ try {
25376
+ const response = await fetch(url);
25377
+ if (!response.ok) {
25378
+ throw new Error(`Failed to load file: ${response.statusText}`);
25379
+ }
25380
+ itemToLoad = await response.arrayBuffer();
25381
+ } catch (error) {
25382
+ throw new Error(`Failed to load url ${url}: ${error}`);
25383
+ }
25384
+ }
25385
+ const buffer = await this.loaders[ext].loader(itemToLoad);
25386
+ imageItem.url = buffer;
25387
+ imageItem.name = `${name}.${toExt}`;
25388
+ }
25478
25389
  const imageOptions = {
25479
25390
  url: imageItem.url,
25480
25391
  headers: imageItem.headers,
@@ -25614,6 +25525,40 @@ var Niivue = class {
25614
25525
  this.addVolume(volume);
25615
25526
  });
25616
25527
  }
25528
+ /* useLoader
25529
+
25530
+ registers an external file loader for niivue to use when reading files.
25531
+
25532
+ the loader must return an array buffer of the file contents for niivue
25533
+ to parse and the extension of the file so niivue can infer the file type to load.
25534
+
25535
+ example:
25536
+
25537
+ const myCustomLoader = (File) => {
25538
+ // ... do parsing here ...
25539
+ return {
25540
+ arrayBuffer: ArrayBuffer,
25541
+ fileExt: 'iwi.cbor',
25542
+ positions: Float32Array | [], // allows for mesh loading to mz3
25543
+ indices: Uint32Array | [], // allows for mesh loading to mz3
25544
+ }
25545
+
25546
+ nv.useLoader({
25547
+ loader: myCustomLoader,
25548
+ fileExt: 'iwi.cbor',
25549
+ toExt: 'nii'
25550
+ })
25551
+
25552
+ */
25553
+ useLoader(loader, fileExt, toExt) {
25554
+ this.loaders = {
25555
+ ...this.loaders,
25556
+ [fileExt.toUpperCase()]: {
25557
+ loader,
25558
+ toExt
25559
+ }
25560
+ };
25561
+ }
25617
25562
  // not included in public docs
25618
25563
  dropListener(e) {
25619
25564
  e.stopPropagation();
@@ -27285,6 +27230,37 @@ var Niivue = class {
27285
27230
  this.document.meshes = this.meshes;
27286
27231
  return this.document.download(fileName, compress2);
27287
27232
  }
27233
+ // generic loadImages that wraps loadVolumes and loadMeshes
27234
+ async loadImages(images) {
27235
+ const volumes = [];
27236
+ const meshes = [];
27237
+ for (const image of images) {
27238
+ if ("url" in image) {
27239
+ const ext = this.getFileExt(image.url);
27240
+ if (this.loaders[ext]) {
27241
+ const toExt = this.loaders[ext].toExt.toUpperCase();
27242
+ if (MESH_EXTENSIONS.includes(toExt)) {
27243
+ meshes.push(image);
27244
+ } else {
27245
+ volumes.push(image);
27246
+ }
27247
+ continue;
27248
+ }
27249
+ if (MESH_EXTENSIONS.includes(ext.toUpperCase())) {
27250
+ meshes.push(image);
27251
+ } else {
27252
+ volumes.push(image);
27253
+ }
27254
+ }
27255
+ }
27256
+ if (volumes.length > 0) {
27257
+ await this.loadVolumes(volumes);
27258
+ }
27259
+ if (meshes.length > 0) {
27260
+ await this.loadMeshes(meshes);
27261
+ }
27262
+ return this;
27263
+ }
27288
27264
  /**
27289
27265
  * load an array of volume objects
27290
27266
  * @param volumeList - the array of objects to load. each object must have a resolvable "url" property at a minimum
@@ -27304,27 +27280,7 @@ var Niivue = class {
27304
27280
  this.volumes = [];
27305
27281
  this.gl.clearColor(0, 0, 0, 1);
27306
27282
  this.gl.clear(this.gl.COLOR_BUFFER_BIT);
27307
- if (volumeList.length > 1) {
27308
- await this.addVolumesFromUrl(volumeList);
27309
- return this;
27310
- }
27311
- const imageOptions = {
27312
- url: volumeList[0].url,
27313
- headers: volumeList[0].headers,
27314
- name: volumeList[0].name,
27315
- colormap: volumeList[0].colormap ? volumeList[0].colormap : volumeList[0].colorMap,
27316
- colormapNegative: volumeList[0].colormapNegative ? volumeList[0].colormapNegative : volumeList[0].colorMapNegative,
27317
- opacity: volumeList[0].opacity,
27318
- urlImgData: volumeList[0].urlImgData,
27319
- cal_min: volumeList[0].cal_min,
27320
- cal_max: volumeList[0].cal_max,
27321
- trustCalMinMax: this.opts.trustCalMinMax,
27322
- isManifest: volumeList[0].isManifest,
27323
- frame4D: volumeList[0].frame4D,
27324
- limitFrames4D: volumeList[0].limitFrames4D || this.opts.limitFrames4D,
27325
- colorbarVisible: volumeList[0].colorbarVisible
27326
- };
27327
- await this.addVolumeFromUrl(imageOptions);
27283
+ await this.addVolumesFromUrl(volumeList);
27328
27284
  return this;
27329
27285
  }
27330
27286
  /**
@@ -27354,7 +27310,29 @@ var Niivue = class {
27354
27310
  */
27355
27311
  async addMeshesFromUrl(meshOptions) {
27356
27312
  const promises = meshOptions.map(async (meshItem) => {
27357
- const ext = this.getFileExt(meshItem.url);
27313
+ const ext = this.getFileExt(meshItem.name || meshItem.url);
27314
+ if (this.loaders[ext]) {
27315
+ let itemToLoad = meshItem.url;
27316
+ const toExt = this.loaders[ext].toExt;
27317
+ let name = meshItem.name || meshItem.url;
27318
+ name = name.split("/").pop();
27319
+ if (typeof meshItem.url === "string") {
27320
+ const url = meshItem.url;
27321
+ try {
27322
+ const response = await fetch(url);
27323
+ if (!response.ok) {
27324
+ throw new Error(`Failed to load file: ${response.statusText}`);
27325
+ }
27326
+ itemToLoad = await response.arrayBuffer();
27327
+ } catch (error) {
27328
+ throw new Error(`Failed to load url ${url}: ${error}`);
27329
+ }
27330
+ }
27331
+ const { positions, indices } = await this.loaders[ext].loader(itemToLoad);
27332
+ meshItem.name = `${name}.${toExt}`;
27333
+ const mz3 = NVMeshUtilities.createMZ3(positions, indices, false);
27334
+ meshItem.buffer = mz3;
27335
+ }
27358
27336
  if (ext === "JCON" || ext === "JSON") {
27359
27337
  const response = await fetch(meshItem.url, {});
27360
27338
  const json = await response.json();
@@ -27395,13 +27373,7 @@ var Niivue = class {
27395
27373
  this.meshes = [];
27396
27374
  this.gl.clearColor(0, 0, 0, 1);
27397
27375
  this.gl.clear(this.gl.COLOR_BUFFER_BIT);
27398
- if (meshList.length > 1) {
27399
- await this.addMeshesFromUrl(meshList);
27400
- this.updateGLVolume();
27401
- this.drawScene();
27402
- return this;
27403
- }
27404
- await this.addMeshFromUrl(meshList[0]);
27376
+ await this.addMeshesFromUrl(meshList);
27405
27377
  this.updateGLVolume();
27406
27378
  this.drawScene();
27407
27379
  return this;
@@ -27675,23 +27647,6 @@ var Niivue = class {
27675
27647
  y = Math.min(Math.max(y, 0), dy - 1);
27676
27648
  z = Math.min(Math.max(z, 0), dz - 1);
27677
27649
  this.drawBitmap[x + y * dx + z * dx * dy] = penValue;
27678
- const isAx = this.drawPenAxCorSag === 0;
27679
- const isCor = this.drawPenAxCorSag === 1;
27680
- const isSag = this.drawPenAxCorSag === 2;
27681
- if (this.opts.penSize > 1) {
27682
- const halfPenSize = Math.floor(this.opts.penSize / 2);
27683
- for (let i = -halfPenSize; i <= halfPenSize; i++) {
27684
- for (let j = -halfPenSize; j <= halfPenSize; j++) {
27685
- if (isAx) {
27686
- this.drawBitmap[x + i + (y + j) * dx + z * dx * dy] = penValue;
27687
- } else if (isCor) {
27688
- this.drawBitmap[x + i + y * dx + (z + j) * dx * dy] = penValue;
27689
- } else if (isSag) {
27690
- this.drawBitmap[x + (y + j) * dx + (z + i) * dx * dy] = penValue;
27691
- }
27692
- }
27693
- }
27694
- }
27695
27650
  }
27696
27651
  // not included in public docs
27697
27652
  // create line between to voxels in drawing
@@ -27773,98 +27728,6 @@ var Niivue = class {
27773
27728
  }
27774
27729
  }
27775
27730
  }
27776
- /**
27777
- * Performs a 1-voxel binary dilation on a connected cluster within the drawing mask using the drawFloodFillCore function.
27778
- *
27779
- * @param seedXYZ - voxel index of the seed voxel in the mask array.
27780
- * @param neighbors - Number of neighbors to consider for connectivity and dilation (6, 18, or 26).
27781
- */
27782
- drawingBinaryDilationWithSeed(seedXYZ, neighbors = 6) {
27783
- try {
27784
- let xyz2vx = function(pt) {
27785
- return pt[0] + pt[1] * nx + pt[2] * nxy;
27786
- };
27787
- const mask = this.drawBitmap;
27788
- const xDim = this.back.dims[1];
27789
- const yDim = this.back.dims[2];
27790
- const zDim = this.back.dims[3];
27791
- const nx = xDim;
27792
- const nxy = xDim * yDim;
27793
- const totalVoxels = nxy * zDim;
27794
- const seedIndex = xyz2vx(seedXYZ);
27795
- if (seedIndex < 0 || seedIndex >= totalVoxels) {
27796
- throw new Error("Seed index is out of bounds.");
27797
- }
27798
- const seedValue = mask[seedIndex];
27799
- if (seedValue === 0) {
27800
- throw new Error("Seed voxel is not part of a filled cluster.");
27801
- }
27802
- const img = mask.slice();
27803
- for (let i = 0; i < totalVoxels; i++) {
27804
- img[i] = img[i] === seedValue ? 1 : 0;
27805
- }
27806
- this.drawFloodFillCore(img, seedIndex, neighbors);
27807
- const outputMask = mask.slice();
27808
- const neighborOffsets = [];
27809
- const offsets6 = [-nxy, nxy, -xDim, xDim, -1, 1];
27810
- neighborOffsets.push(...offsets6);
27811
- if (neighbors > 6) {
27812
- neighborOffsets.push(
27813
- -xDim - 1,
27814
- -xDim + 1,
27815
- xDim - 1,
27816
- xDim + 1,
27817
- -nxy - xDim,
27818
- -nxy + xDim,
27819
- -nxy - 1,
27820
- -nxy + 1,
27821
- nxy - xDim,
27822
- nxy + xDim,
27823
- nxy - 1,
27824
- nxy + 1
27825
- );
27826
- }
27827
- if (neighbors > 18) {
27828
- neighborOffsets.push(
27829
- -nxy - xDim - 1,
27830
- -nxy - xDim + 1,
27831
- -nxy + xDim - 1,
27832
- -nxy + xDim + 1,
27833
- nxy - xDim - 1,
27834
- nxy - xDim + 1,
27835
- nxy + xDim - 1,
27836
- nxy + xDim + 1
27837
- );
27838
- }
27839
- for (let idx = 0; idx < totalVoxels; idx++) {
27840
- if (img[idx] === 2) {
27841
- const x = idx % xDim;
27842
- const y = Math.floor(idx % nxy / xDim);
27843
- const z = Math.floor(idx / nxy);
27844
- for (const offset of neighborOffsets) {
27845
- const neighborIdx = idx + offset;
27846
- if (neighborIdx < 0 || neighborIdx >= totalVoxels) {
27847
- continue;
27848
- }
27849
- const nx2 = neighborIdx % xDim;
27850
- const ny = Math.floor(neighborIdx % nxy / xDim);
27851
- const nz = Math.floor(neighborIdx / nxy);
27852
- if (Math.abs(nx2 - x) > 1 || Math.abs(ny - y) > 1 || Math.abs(nz - z) > 1) {
27853
- continue;
27854
- }
27855
- if (mask[neighborIdx] === 0) {
27856
- outputMask[neighborIdx] = seedValue;
27857
- }
27858
- }
27859
- }
27860
- }
27861
- this.drawBitmap = outputMask;
27862
- this.drawAddUndoBitmap();
27863
- this.refreshDrawing(true);
27864
- } catch (error) {
27865
- log.error("Error in drawingBinaryDilationWithSeed:", error);
27866
- }
27867
- }
27868
27731
  // a voxel can be defined as having 6, 18 or 26 neighbors:
27869
27732
  // 6: neighbors share faces (distance=1)
27870
27733
  // 18: neighbors share faces (distance=1) or edges (1.4)
@@ -27946,7 +27809,7 @@ var Niivue = class {
27946
27809
  }
27947
27810
  // not included in public docs
27948
27811
  // set all connected voxels in drawing to new color
27949
- drawFloodFill(seedXYZ, newColor = 0, growSelectedCluster = 0, forceMin = NaN, forceMax = NaN, neighbors = 6, maxDistanceMM = Number.POSITIVE_INFINITY) {
27812
+ drawFloodFill(seedXYZ, newColor = 0, growSelectedCluster = 0, forceMin = NaN, forceMax = NaN, neighbors = 6) {
27950
27813
  if (!this.drawBitmap) {
27951
27814
  throw new Error("drawBitmap undefined");
27952
27815
  }
@@ -27968,25 +27831,9 @@ var Niivue = class {
27968
27831
  if (img.length !== nxy * dims[2]) {
27969
27832
  return;
27970
27833
  }
27971
- function vx2xyz(vx) {
27972
- const Z = Math.floor(vx / nxy);
27973
- const Y = Math.floor((vx - Z * nxy) / nx);
27974
- const X = Math.floor(vx % nx);
27975
- return [X, Y, Z];
27976
- }
27977
27834
  function xyz2vx(pt) {
27978
27835
  return pt[0] + pt[1] * nx + pt[2] * nxy;
27979
27836
  }
27980
- const vx2mm = (xyz) => {
27981
- return this.vox2mm(xyz, this.back.matRAS);
27982
- };
27983
- const seedMM = vx2mm(seedXYZ);
27984
- const maxDistanceMM2 = maxDistanceMM ** 2;
27985
- function isWithinDistance(vx) {
27986
- const xyzMM = vx2mm(vx2xyz(vx));
27987
- const dist2 = (xyzMM[0] - seedMM[0]) ** 2 + (xyzMM[1] - seedMM[1]) ** 2 + (xyzMM[2] - seedMM[2]) ** 2;
27988
- return dist2 <= maxDistanceMM2;
27989
- }
27990
27837
  const seedVx = xyz2vx(seedXYZ);
27991
27838
  const seedColor = img[seedVx];
27992
27839
  if (seedColor === newColor) {
@@ -28000,9 +27847,6 @@ var Niivue = class {
28000
27847
  for (let i = 1; i < nxyz; i++) {
28001
27848
  img[i] = 0;
28002
27849
  if (this.drawBitmap[i] === seedColor) {
28003
- if (!isWithinDistance(i)) {
28004
- continue;
28005
- }
28006
27850
  img[i] = 1;
28007
27851
  }
28008
27852
  }
@@ -28032,9 +27876,6 @@ var Niivue = class {
28032
27876
  for (let i = 1; i < nxyz; i++) {
28033
27877
  img[i] = 0;
28034
27878
  if (backImg[i] >= mn && backImg[i] <= mx) {
28035
- if (!isWithinDistance(i)) {
28036
- continue;
28037
- }
28038
27879
  img[i] = 1;
28039
27880
  }
28040
27881
  }
@@ -28656,8 +28497,6 @@ var Niivue = class {
28656
28497
  gl.bindVertexArray(this.unusedVAO);
28657
28498
  this.rectShader = new Shader(gl, vertRectShader, fragRectShader);
28658
28499
  this.rectShader.use(gl);
28659
- this.rectOutlineShader = new Shader(gl, vertRectShader, fragRectOutlineShader);
28660
- this.rectOutlineShader.use(gl);
28661
28500
  this.lineShader = new Shader(gl, vertLineShader, fragRectShader);
28662
28501
  this.lineShader.use(gl);
28663
28502
  this.line3DShader = new Shader(gl, vertLine3DShader, fragRectShader);
@@ -28810,37 +28649,14 @@ var Niivue = class {
28810
28649
  }
28811
28650
  /**
28812
28651
  * basic statistics for selected voxel-based image
28813
- * @param options - an object containing the following properties:
28814
- * - layer: selects image to describe
28815
- * - masks: optional binary images to filter voxels
28816
- * - drawingIsMask: a boolean indicating if the drawing is used as a mask
28817
- * - roiIsMask: a boolean indicating if the ROI is used as a mask
28818
- * - startVox: the starting voxel coordinates
28819
- * - endVox: the ending voxel coordinates
28820
- * @returns numeric values to describe image or regions of images
28821
- * @example
28822
- * niivue.getDescriptives({
28823
- * layer: 0,
28824
- * masks: [],
28825
- * drawingIsMask: true, // drawingIsMask and roiIsMask are mutually exclusive
28826
- * roiIsMask: false,
28827
- * startVox: [10, 20, 30], // ignored if roiIsMask is false
28828
- * endVox: [40, 50, 60] // ignored if roiIsMask is false
28829
- * });
28652
+ * @param layer - selects image to describe
28653
+ * @param masks - are optional binary images to filter voxles
28654
+ * @returns numeric values to describe image
28655
+ * @example niivue.getDescriptives(0);
28830
28656
  * @see {@link https://niivue.github.io/niivue/features/draw2.html | live demo usage}
28831
28657
  */
28832
- getDescriptives(options) {
28833
- const {
28834
- layer = 0,
28835
- masks = [],
28836
- drawingIsMask = false,
28837
- roiIsMask = false,
28838
- startVox = [0, 0, 0],
28839
- endVox = [0, 0, 0]
28840
- } = options;
28841
- let area = null;
28658
+ getDescriptives(layer = 0, masks = [], drawingIsMask = false) {
28842
28659
  const hdr = this.volumes[layer].hdr;
28843
- const pixDimsRAS = this.volumes[layer].pixDimsRAS;
28844
28660
  let slope = hdr.scl_slope;
28845
28661
  if (isNaN(slope)) {
28846
28662
  slope = 1;
@@ -28878,37 +28694,6 @@ var Niivue = class {
28878
28694
  mask[i] = 0;
28879
28695
  }
28880
28696
  }
28881
- } else if (masks.length < 1 && roiIsMask) {
28882
- mask.fill(0);
28883
- console.log("startVox", startVox);
28884
- console.log("endVox", endVox);
28885
- const xrange = this.calculateMinMaxVoxIdx([startVox[0], endVox[0]]);
28886
- const yrange = this.calculateMinMaxVoxIdx([startVox[1], endVox[1]]);
28887
- const zrange = this.calculateMinMaxVoxIdx([startVox[2], endVox[2]]);
28888
- if (startVox[0] - endVox[0] === 0) {
28889
- xrange[1] = startVox[0] + 1;
28890
- } else if (startVox[1] - endVox[1] === 0) {
28891
- yrange[1] = startVox[1] + 1;
28892
- } else if (startVox[2] - endVox[2] === 0) {
28893
- zrange[1] = startVox[2] + 1;
28894
- }
28895
- const xdim = hdr.dims[1];
28896
- const ydim = hdr.dims[2];
28897
- for (let z = zrange[0]; z < zrange[1]; z++) {
28898
- const zi = z * xdim * ydim;
28899
- for (let y = yrange[0]; y < yrange[1]; y++) {
28900
- const yi = y * xdim;
28901
- for (let x = xrange[0]; x < xrange[1]; x++) {
28902
- const index = zi + yi + x;
28903
- mask[index] = 1;
28904
- }
28905
- }
28906
- }
28907
- const xmm = Math.abs(pixDimsRAS[1] * (endVox[0] - startVox[0]));
28908
- const ymm = Math.abs(pixDimsRAS[2] * (endVox[1] - startVox[1]));
28909
- const zmm = Math.abs(pixDimsRAS[3] * (endVox[2] - startVox[2]));
28910
- const mms = [xmm, ymm, zmm];
28911
- area = mms.filter((m) => m !== 0).reduce((a, b) => a * b, 1);
28912
28697
  }
28913
28698
  let k = 0;
28914
28699
  let M = 0;
@@ -28962,8 +28747,7 @@ var Niivue = class {
28962
28747
  cal_min: this.volumes[layer].cal_min,
28963
28748
  cal_max: this.volumes[layer].cal_max,
28964
28749
  robust_min: this.volumes[layer].robust_min,
28965
- robust_max: this.volumes[layer].robust_max,
28966
- area
28750
+ robust_max: this.volumes[layer].robust_max
28967
28751
  };
28968
28752
  }
28969
28753
  // not included in public docs
@@ -30518,51 +30302,30 @@ var Niivue = class {
30518
30302
  }
30519
30303
  if (this.opts.drawingEnabled) {
30520
30304
  const pt = this.frac2vox(this.scene.crosshairPos);
30305
+ const ptMM = this.frac2mm(this.scene.crosshairPos);
30521
30306
  if (this.opts.clickToSegment) {
30522
- const voxelIntensity = this.back.getValue(pt[0], pt[1], pt[2]);
30523
- if (this.opts.clickToSegmentPercent > 0 && this.opts.clickToSegmentPercent <= 1) {
30524
- this.opts.clickToSegmentIntensityMax = voxelIntensity * (1 + this.opts.clickToSegmentPercent);
30525
- this.opts.clickToSegmentIntensityMin = voxelIntensity * (1 - this.opts.clickToSegmentPercent);
30526
- }
30527
- if (this.opts.clickToSegmentAutoIntensity) {
30528
- if (voxelIntensity > (this.back.cal_min + this.back.cal_max) * 0.5) {
30529
- this.opts.clickToSegmentBright = true;
30530
- } else {
30531
- this.opts.clickToSegmentBright = false;
30532
- }
30533
- }
30307
+ const radius = this.opts.clickToSegmentRadius;
30308
+ const steps = this.opts.clickToSegmentSteps;
30534
30309
  const brightOrDark = this.opts.clickToSegmentBright ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
30310
+ this.drawPenFillPts = [];
30535
30311
  this.drawPenAxCorSag = axCorSag;
30536
- if (this.drawPenAxCorSag === 0 /* AXIAL */) {
30537
- const pixDims = [this.back.pixDimsRAS[0], this.back.pixDimsRAS[1]];
30538
- const minPixDim = Math.min(...pixDims);
30539
- this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim);
30540
- } else if (this.drawPenAxCorSag === 1 /* CORONAL */) {
30541
- const pixDims = [this.back.pixDimsRAS[0], this.back.pixDimsRAS[2]];
30542
- const minPixDim = Math.min(...pixDims);
30543
- this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim);
30544
- } else if (this.drawPenAxCorSag === 2 /* SAGITTAL */) {
30545
- const pixDims = [this.back.pixDimsRAS[1], this.back.pixDimsRAS[2]];
30546
- const minPixDim = Math.min(...pixDims);
30547
- this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim);
30312
+ for (let i2 = 1; i2 <= steps; i2++) {
30313
+ const angle = i2 / steps * 2 * Math.PI;
30314
+ const xMM = ptMM[0] + radius * Math.cos(angle);
30315
+ const yMM = ptMM[1] + radius * Math.sin(angle);
30316
+ const zMM = ptMM[2];
30317
+ const xVox = this.back.mm2vox([xMM, yMM, zMM])[0];
30318
+ const yVox = this.back.mm2vox([xMM, yMM, zMM])[1];
30319
+ const zVox = this.back.mm2vox([xMM, yMM, zMM])[2];
30320
+ this.drawPt(xVox, yVox, zVox, this.opts.penValue);
30321
+ this.drawPenFillPts.push([xVox, yVox, pt[2]]);
30322
+ if (i2 === steps) {
30323
+ this.drawFloodFill([xVox, yVox, pt[2]], 0, brightOrDark, NaN, NaN, this.opts.floodFillNeighbors);
30324
+ }
30548
30325
  }
30549
- this.drawPt(pt[0], pt[1], pt[2], this.opts.penValue);
30550
- this.drawFloodFill(
30551
- [pt[0], pt[1], pt[2]],
30552
- 0,
30553
- brightOrDark,
30554
- this.opts.clickToSegmentIntensityMin,
30555
- this.opts.clickToSegmentIntensityMax,
30556
- this.opts.floodFillNeighbors,
30557
- this.opts.clickToSegmentMaxDistanceMM
30558
- );
30559
30326
  this.drawScene();
30560
30327
  this.createOnLocationChange(axCorSag);
30561
- const info = this.getDescriptives({
30562
- layer: 0,
30563
- masks: [],
30564
- drawingIsMask: true
30565
- });
30328
+ const info = this.getDescriptives(0, [], true);
30566
30329
  this.onClickToSegment({ mL: info.volumeML, mm3: info.volumeMM3 });
30567
30330
  return;
30568
30331
  }
@@ -30780,41 +30543,20 @@ var Niivue = class {
30780
30543
  if (!this.rectShader) {
30781
30544
  throw new Error("rectShader undefined");
30782
30545
  }
30783
- if (!this.opts.selectionBoxIsOutline) {
30784
- this.rectShader.use(this.gl);
30785
- this.gl.enable(this.gl.BLEND);
30786
- this.gl.uniform4fv(this.rectShader.uniforms.lineColor, lineColor);
30787
- this.gl.uniform2fv(this.rectShader.uniforms.canvasWidthHeight, [this.gl.canvas.width, this.gl.canvas.height]);
30788
- this.gl.uniform4f(
30789
- this.rectShader.uniforms.leftTopWidthHeight,
30790
- leftTopWidthHeight[0],
30791
- leftTopWidthHeight[1],
30792
- leftTopWidthHeight[2],
30793
- leftTopWidthHeight[3]
30794
- );
30795
- this.gl.bindVertexArray(this.genericVAO);
30796
- this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
30797
- this.gl.bindVertexArray(this.unusedVAO);
30798
- } else {
30799
- this.rectOutlineShader.use(this.gl);
30800
- this.gl.enable(this.gl.BLEND);
30801
- this.gl.uniform1f(this.rectOutlineShader.uniforms.thickness, this.opts.selectionBoxLineThickness);
30802
- this.gl.uniform4fv(this.rectOutlineShader.uniforms.lineColor, lineColor);
30803
- this.gl.uniform2fv(this.rectOutlineShader.uniforms.canvasWidthHeight, [
30804
- this.gl.canvas.width,
30805
- this.gl.canvas.height
30806
- ]);
30807
- this.gl.uniform4f(
30808
- this.rectOutlineShader.uniforms.leftTopWidthHeight,
30809
- leftTopWidthHeight[0],
30810
- leftTopWidthHeight[1],
30811
- leftTopWidthHeight[2],
30812
- leftTopWidthHeight[3]
30813
- );
30814
- this.gl.bindVertexArray(this.genericVAO);
30815
- this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
30816
- this.gl.bindVertexArray(this.unusedVAO);
30817
- }
30546
+ this.rectShader.use(this.gl);
30547
+ this.gl.enable(this.gl.BLEND);
30548
+ this.gl.uniform4fv(this.rectShader.uniforms.lineColor, lineColor);
30549
+ this.gl.uniform2fv(this.rectShader.uniforms.canvasWidthHeight, [this.gl.canvas.width, this.gl.canvas.height]);
30550
+ this.gl.uniform4f(
30551
+ this.rectShader.uniforms.leftTopWidthHeight,
30552
+ leftTopWidthHeight[0],
30553
+ leftTopWidthHeight[1],
30554
+ leftTopWidthHeight[2],
30555
+ leftTopWidthHeight[3]
30556
+ );
30557
+ this.gl.bindVertexArray(this.genericVAO);
30558
+ this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
30559
+ this.gl.bindVertexArray(this.unusedVAO);
30818
30560
  }
30819
30561
  drawCircle(leftTopWidthHeight, circleColor = this.opts.fontColor, fillPercent = 1) {
30820
30562
  if (!this.circleShader) {
@@ -33516,14 +33258,10 @@ var Niivue = class {
33516
33258
  width,
33517
33259
  height
33518
33260
  ]);
33519
- return;
33520
33261
  }
33521
33262
  if (this.opts.clickToSegment) {
33522
33263
  const x = this.mousePos[0];
33523
33264
  const y = this.mousePos[1];
33524
- if (this.inRenderTile(x, y) >= 0) {
33525
- return;
33526
- }
33527
33265
  const tileIdx = this.tileIndex(x, y);
33528
33266
  if (tileIdx > -1) {
33529
33267
  const fovMM = this.screenSlices[tileIdx].fovMM;