@niivue/niivue 0.44.2-dev.1 → 0.45.0

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/dist/index.js CHANGED
@@ -95525,7 +95525,7 @@ var forEach3 = function() {
95525
95525
  }();
95526
95526
 
95527
95527
  // package.json
95528
- var version = "0.44.2-dev.1";
95528
+ var version = "0.45.0";
95529
95529
 
95530
95530
  // src/logger.ts
95531
95531
  var _Log = class _Log {
@@ -96321,46 +96321,6 @@ out vec4 color;
96321
96321
  void main() {
96322
96322
  color = lineColor;
96323
96323
  }`;
96324
- var fragRectOutlineShader = `#version 300 es
96325
- #line 723
96326
- precision highp int;
96327
- precision highp float;
96328
-
96329
- uniform vec4 lineColor;
96330
- uniform vec4 leftTopWidthHeight;
96331
- uniform float thickness; // line thickness in pixels
96332
- uniform vec2 canvasWidthHeight;
96333
-
96334
- out vec4 color;
96335
-
96336
- void main() {
96337
- // fragment position in screen coordinates
96338
- vec2 fragCoord = gl_FragCoord.xy;
96339
-
96340
- // canvas height
96341
- float canvasHeight = canvasWidthHeight.y;
96342
-
96343
- // 'top' and 'bottom' to match gl_FragCoord.y coordinate system
96344
- float top = canvasHeight - leftTopWidthHeight.y;
96345
- float bottom = top - leftTopWidthHeight.w;
96346
-
96347
- // left and right edges
96348
- float left = leftTopWidthHeight.x;
96349
- float right = left + leftTopWidthHeight.z;
96350
-
96351
- bool withinLeft = fragCoord.x >= left && fragCoord.x <= left + thickness;
96352
- bool withinRight = fragCoord.x <= right && fragCoord.x >= right - thickness;
96353
- bool withinTop = fragCoord.y <= top && fragCoord.y >= top - thickness;
96354
- bool withinBottom = fragCoord.y >= bottom && fragCoord.y <= bottom + thickness;
96355
-
96356
- bool isOutline = withinLeft || withinRight || withinTop || withinBottom;
96357
-
96358
- if (isOutline) {
96359
- color = lineColor;
96360
- } else {
96361
- discard;
96362
- }
96363
- }`;
96364
96324
  var vertColorbarShader = `#version 300 es
96365
96325
  #line 490
96366
96326
  layout(location=0) in vec3 pos;
@@ -119194,7 +119154,6 @@ var DRAG_MODE = /* @__PURE__ */ ((DRAG_MODE2) => {
119194
119154
  DRAG_MODE2[DRAG_MODE2["pan"] = 3] = "pan";
119195
119155
  DRAG_MODE2[DRAG_MODE2["slicer3D"] = 4] = "slicer3D";
119196
119156
  DRAG_MODE2[DRAG_MODE2["callbackOnly"] = 5] = "callbackOnly";
119197
- DRAG_MODE2[DRAG_MODE2["roiSelection"] = 6] = "roiSelection";
119198
119157
  return DRAG_MODE2;
119199
119158
  })(DRAG_MODE || {});
119200
119159
  var DEFAULT_OPTIONS = {
@@ -119264,27 +119223,10 @@ var DEFAULT_OPTIONS = {
119264
119223
  renderOverlayBlend: 1,
119265
119224
  sliceMosaicString: "",
119266
119225
  centerMosaic: false,
119267
- penSize: 1,
119268
- // in voxels, since all drawing is done using bitmap indices
119269
119226
  clickToSegment: false,
119270
- clickToSegmentRadius: 3,
119271
- // in mm
119272
- clickToSegmentBright: true,
119273
- clickToSegmentAutoIntensity: false,
119274
- // new option, but keep clickToSegmentBright for backwards compatibility
119275
- clickToSegmentIntensityMax: NaN,
119276
- // NaN will use auto threshold (default flood fill behavior from before)
119277
- clickToSegmentIntensityMin: NaN,
119278
- // NaN will use auto threshold (default flood fill behavior from before)
119279
- // 0 will use auto threshold (default flood fill behavior from before)
119280
- // Take the voxel intensity at the click point and use this percentage +/- to threshold the flood fill operation.
119281
- // If greater than 0, clickedVoxelIntensity +/- clickedVoxelIntensity * clickToSegmentPercent will be used
119282
- // for the clickToSegmentIntensityMin and clickToSegmentIntensityMax values.
119283
- clickToSegmentPercent: 0,
119284
- clickToSegmentMaxDistanceMM: Number.POSITIVE_INFINITY,
119285
- // default value is infinity for backwards compatibility with flood fill routine.
119286
- selectionBoxLineThickness: 4,
119287
- selectionBoxIsOutline: false
119227
+ clickToSegmentRadius: 2,
119228
+ clickToSegmentSteps: 10,
119229
+ clickToSegmentBright: true
119288
119230
  };
119289
119231
  var INITIAL_SCENE_DATA = {
119290
119232
  gamma: 1,
@@ -120978,11 +120920,12 @@ var defaultSaveImageOptions = {
120978
120920
  isSaveDrawing: false,
120979
120921
  volumeByIndex: 0
120980
120922
  };
120981
- var Niivue = class {
120923
+ var Niivue = class _Niivue {
120982
120924
  /**
120983
120925
  * @param options - options object to set modifiable Niivue properties
120984
120926
  */
120985
120927
  constructor(options = DEFAULT_OPTIONS) {
120928
+ __publicField(this, "loaders", {});
120986
120929
  __publicField(this, "canvas", null);
120987
120930
  // the reference to the canvas element on the page
120988
120931
  __publicField(this, "_gl", null);
@@ -121025,7 +120968,6 @@ var Niivue = class {
121025
120968
  __publicField(this, "orientCubeShader");
121026
120969
  __publicField(this, "orientCubeShaderVAO", null);
121027
120970
  __publicField(this, "rectShader");
121028
- __publicField(this, "rectOutlineShader");
121029
120971
  __publicField(this, "renderShader");
121030
120972
  __publicField(this, "lineShader");
121031
120973
  __publicField(this, "line3DShader");
@@ -121078,20 +121020,7 @@ var Niivue = class {
121078
121020
  __publicField(this, "extentsMax");
121079
121021
  // ResizeObserver
121080
121022
  __publicField(this, "resizeObserver", null);
121081
- // syncOpts: Record<string, unknown> = {}
121082
- __publicField(this, "syncOpts", {
121083
- "3d": false,
121084
- // legacy option
121085
- "2d": false,
121086
- // legacy option
121087
- zoomPan: false,
121088
- cal_min: false,
121089
- cal_max: false,
121090
- clipPlane: false,
121091
- gamma: false,
121092
- sliceType: false,
121093
- crosshair: false
121094
- });
121023
+ __publicField(this, "syncOpts", {});
121095
121024
  __publicField(this, "readyForSync", false);
121096
121025
  // UI Data
121097
121026
  __publicField(this, "uiData", {
@@ -121570,9 +121499,6 @@ var Niivue = class {
121570
121499
  * @see {@link https://niivue.github.io/niivue/features/sync.mesh.html | live demo usage}
121571
121500
  */
121572
121501
  syncWith(otherNV, syncOpts = { "2d": true, "3d": true }) {
121573
- if (!(otherNV instanceof Array)) {
121574
- otherNV = [otherNV];
121575
- }
121576
121502
  this.otherNV = otherNV;
121577
121503
  this.syncOpts = { ...syncOpts };
121578
121504
  }
@@ -121588,55 +121514,9 @@ var Niivue = class {
121588
121514
  * @see {@link https://niivue.github.io/niivue/features/sync.mesh.html | live demo usage}
121589
121515
  */
121590
121516
  broadcastTo(otherNV, syncOpts = { "2d": true, "3d": true }) {
121591
- if (!(otherNV instanceof Array)) {
121592
- otherNV = [otherNV];
121593
- }
121594
121517
  this.otherNV = otherNV;
121595
121518
  this.syncOpts = syncOpts;
121596
121519
  }
121597
- doSync3d(otherNV) {
121598
- otherNV.scene.renderAzimuth = this.scene.renderAzimuth;
121599
- otherNV.scene.renderElevation = this.scene.renderElevation;
121600
- otherNV.scene.volScaleMultiplier = this.scene.volScaleMultiplier;
121601
- }
121602
- // both crosshair and zoomPan
121603
- doSync2d(otherNV) {
121604
- const thisMM = this.frac2mm(this.scene.crosshairPos);
121605
- otherNV.scene.crosshairPos = otherNV.mm2frac(thisMM);
121606
- otherNV.scene.pan2Dxyzmm = vec4_exports.clone(this.scene.pan2Dxyzmm);
121607
- }
121608
- doSyncGamma(otherNV) {
121609
- const thisGamma = this.scene.gamma;
121610
- const otherGamma = otherNV.scene.gamma;
121611
- if (thisGamma !== otherGamma) {
121612
- otherNV.setGamma(thisGamma);
121613
- }
121614
- }
121615
- doSyncZoomPan(otherNV) {
121616
- otherNV.scene.pan2Dxyzmm = vec4_exports.clone(this.scene.pan2Dxyzmm);
121617
- }
121618
- doSyncCrosshair(otherNV) {
121619
- const thisMM = this.frac2mm(this.scene.crosshairPos);
121620
- otherNV.scene.crosshairPos = otherNV.mm2frac(thisMM);
121621
- }
121622
- doSyncCalMin(otherNV) {
121623
- if (this.volumes[0].cal_min !== otherNV.volumes[0].cal_min) {
121624
- otherNV.volumes[0].cal_min = this.volumes[0].cal_min;
121625
- otherNV.updateGLVolume();
121626
- }
121627
- }
121628
- doSyncCalMax(otherNV) {
121629
- if (this.volumes[0].cal_max !== otherNV.volumes[0].cal_max) {
121630
- otherNV.volumes[0].cal_max = this.volumes[0].cal_max;
121631
- otherNV.updateGLVolume();
121632
- }
121633
- }
121634
- doSyncSliceType(otherNV) {
121635
- otherNV.setSliceType(this.opts.sliceType);
121636
- }
121637
- doSyncClipPlane(otherNV) {
121638
- otherNV.setClipPlane(this.scene.clipPlaneDepthAziElev);
121639
- }
121640
121520
  /**
121641
121521
  * Sync the scene controls (orientation, crosshair location, etc.) from one Niivue instance to another. useful for using one canvas to drive another.
121642
121522
  * @internal
@@ -121653,39 +121533,46 @@ var Niivue = class {
121653
121533
  if (!this.gl.canvas.matches(":focus")) {
121654
121534
  return;
121655
121535
  }
121656
- for (let i2 = 0; i2 < this.otherNV.length; i2++) {
121657
- if (this.otherNV[i2] === this) {
121658
- continue;
121659
- }
121660
- if (this.syncOpts.gamma) {
121661
- this.doSyncGamma(this.otherNV[i2]);
121662
- }
121663
- if (this.syncOpts.crosshair) {
121664
- this.doSyncCrosshair(this.otherNV[i2]);
121665
- }
121666
- if (this.syncOpts.zoomPan) {
121667
- this.doSyncZoomPan(this.otherNV[i2]);
121668
- }
121669
- if (this.syncOpts.sliceType) {
121670
- this.doSyncSliceType(this.otherNV[i2]);
121671
- }
121672
- if (this.syncOpts.cal_min) {
121673
- this.doSyncCalMin(this.otherNV[i2]);
121674
- }
121675
- if (this.syncOpts.cal_max) {
121676
- this.doSyncCalMax(this.otherNV[i2]);
121677
- }
121678
- if (this.syncOpts.clipPlane) {
121679
- this.doSyncClipPlane(this.otherNV[i2]);
121536
+ const thisMM = this.frac2mm(this.scene.crosshairPos);
121537
+ if (this.otherNV instanceof _Niivue) {
121538
+ const thisGamma = this.scene.gamma;
121539
+ const otherGamma = this.otherNV.scene.gamma;
121540
+ if (thisGamma !== otherGamma) {
121541
+ this.otherNV.setGamma(thisGamma);
121680
121542
  }
121681
121543
  if (this.syncOpts["2d"]) {
121682
- this.doSync2d(this.otherNV[i2]);
121544
+ this.otherNV.scene.crosshairPos = this.otherNV.mm2frac(thisMM);
121545
+ this.otherNV.scene.pan2Dxyzmm = vec4_exports.clone(this.scene.pan2Dxyzmm);
121683
121546
  }
121684
121547
  if (this.syncOpts["3d"]) {
121685
- this.doSync3d(this.otherNV[i2]);
121548
+ this.otherNV.scene.renderAzimuth = this.scene.renderAzimuth;
121549
+ this.otherNV.scene.renderElevation = this.scene.renderElevation;
121550
+ this.otherNV.scene.volScaleMultiplier = this.scene.volScaleMultiplier;
121551
+ }
121552
+ this.otherNV.drawScene();
121553
+ this.otherNV.createOnLocationChange();
121554
+ } else if (Array.isArray(this.otherNV)) {
121555
+ for (let i2 = 0; i2 < this.otherNV.length; i2++) {
121556
+ if (this.otherNV[i2] === this) {
121557
+ continue;
121558
+ }
121559
+ const thisGamma = this.scene.gamma;
121560
+ const otherGamma = this.otherNV[i2].scene.gamma;
121561
+ if (thisGamma !== otherGamma) {
121562
+ this.otherNV[i2].setGamma(thisGamma);
121563
+ }
121564
+ if (this.syncOpts["2d"]) {
121565
+ this.otherNV[i2].scene.crosshairPos = this.otherNV[i2].mm2frac(thisMM);
121566
+ this.otherNV[i2].scene.pan2Dxyzmm = vec4_exports.clone(this.scene.pan2Dxyzmm);
121567
+ }
121568
+ if (this.syncOpts["3d"]) {
121569
+ this.otherNV[i2].scene.renderAzimuth = this.scene.renderAzimuth;
121570
+ this.otherNV[i2].scene.renderElevation = this.scene.renderElevation;
121571
+ this.otherNV[i2].scene.volScaleMultiplier = this.scene.volScaleMultiplier;
121572
+ }
121573
+ this.otherNV[i2].drawScene();
121574
+ this.otherNV[i2].createOnLocationChange();
121686
121575
  }
121687
- this.otherNV[i2].drawScene();
121688
- this.otherNV[i2].createOnLocationChange();
121689
121576
  }
121690
121577
  }
121691
121578
  /** Not documented publicly for now
@@ -122002,9 +121889,6 @@ var Niivue = class {
122002
121889
  const fracStart = this.canvasPos2frac([this.uiData.dragStart[0], this.uiData.dragStart[1]]);
122003
121890
  const fracEnd = this.canvasPos2frac([this.uiData.dragEnd[0], this.uiData.dragEnd[1]]);
122004
121891
  this.generateMouseUpCallback(fracStart, fracEnd);
122005
- if (this.opts.dragMode === 6 /* roiSelection */) {
122006
- return;
122007
- }
122008
121892
  if (this.opts.dragMode !== 1 /* contrast */) {
122009
121893
  return;
122010
121894
  }
@@ -122356,6 +122240,11 @@ var Niivue = class {
122356
122240
  if (ext === "GZ") {
122357
122241
  ext = re.exec(fullname.slice(0, -3))[1];
122358
122242
  ext = ext.toUpperCase();
122243
+ } else if (ext === "CBOR") {
122244
+ const endExt = ext;
122245
+ ext = re.exec(fullname.slice(0, -5))[1];
122246
+ ext = ext.toUpperCase();
122247
+ ext = `${ext}.${endExt}`;
122359
122248
  }
122360
122249
  return upperCase ? ext : ext.toLowerCase();
122361
122250
  }
@@ -122376,6 +122265,28 @@ var Niivue = class {
122376
122265
  }
122377
122266
  async addVolumesFromUrl(imageOptionsArray) {
122378
122267
  const promises = imageOptionsArray.map(async (imageItem) => {
122268
+ const ext = this.getFileExt(imageItem.url);
122269
+ if (this.loaders[ext]) {
122270
+ let itemToLoad = imageItem.url;
122271
+ const toExt = this.loaders[ext].toExt;
122272
+ let name = imageItem.name || imageItem.url;
122273
+ name = name.split("/").pop();
122274
+ if (typeof imageItem.url === "string") {
122275
+ const url = imageItem.url;
122276
+ try {
122277
+ const response = await fetch(url);
122278
+ if (!response.ok) {
122279
+ throw new Error(`Failed to load file: ${response.statusText}`);
122280
+ }
122281
+ itemToLoad = await response.arrayBuffer();
122282
+ } catch (error) {
122283
+ throw new Error(`Failed to load url ${url}: ${error}`);
122284
+ }
122285
+ }
122286
+ const buffer2 = await this.loaders[ext].loader(itemToLoad);
122287
+ imageItem.url = buffer2;
122288
+ imageItem.name = `${name}.${toExt}`;
122289
+ }
122379
122290
  const imageOptions = {
122380
122291
  url: imageItem.url,
122381
122292
  headers: imageItem.headers,
@@ -122515,6 +122426,40 @@ var Niivue = class {
122515
122426
  this.addVolume(volume);
122516
122427
  });
122517
122428
  }
122429
+ /* useLoader
122430
+
122431
+ registers an external file loader for niivue to use when reading files.
122432
+
122433
+ the loader must return an array buffer of the file contents for niivue
122434
+ to parse and the extension of the file so niivue can infer the file type to load.
122435
+
122436
+ example:
122437
+
122438
+ const myCustomLoader = (File) => {
122439
+ // ... do parsing here ...
122440
+ return {
122441
+ arrayBuffer: ArrayBuffer,
122442
+ fileExt: 'iwi.cbor',
122443
+ positions: Float32Array | [], // allows for mesh loading to mz3
122444
+ indices: Uint32Array | [], // allows for mesh loading to mz3
122445
+ }
122446
+
122447
+ nv.useLoader({
122448
+ loader: myCustomLoader,
122449
+ fileExt: 'iwi.cbor',
122450
+ toExt: 'nii'
122451
+ })
122452
+
122453
+ */
122454
+ useLoader(loader, fileExt, toExt) {
122455
+ this.loaders = {
122456
+ ...this.loaders,
122457
+ [fileExt.toUpperCase()]: {
122458
+ loader,
122459
+ toExt
122460
+ }
122461
+ };
122462
+ }
122518
122463
  // not included in public docs
122519
122464
  dropListener(e) {
122520
122465
  e.stopPropagation();
@@ -124186,6 +124131,37 @@ var Niivue = class {
124186
124131
  this.document.meshes = this.meshes;
124187
124132
  return this.document.download(fileName, compress);
124188
124133
  }
124134
+ // generic loadImages that wraps loadVolumes and loadMeshes
124135
+ async loadImages(images) {
124136
+ const volumes = [];
124137
+ const meshes = [];
124138
+ for (const image of images) {
124139
+ if ("url" in image) {
124140
+ const ext = this.getFileExt(image.url);
124141
+ if (this.loaders[ext]) {
124142
+ const toExt = this.loaders[ext].toExt.toUpperCase();
124143
+ if (MESH_EXTENSIONS.includes(toExt)) {
124144
+ meshes.push(image);
124145
+ } else {
124146
+ volumes.push(image);
124147
+ }
124148
+ continue;
124149
+ }
124150
+ if (MESH_EXTENSIONS.includes(ext.toUpperCase())) {
124151
+ meshes.push(image);
124152
+ } else {
124153
+ volumes.push(image);
124154
+ }
124155
+ }
124156
+ }
124157
+ if (volumes.length > 0) {
124158
+ await this.loadVolumes(volumes);
124159
+ }
124160
+ if (meshes.length > 0) {
124161
+ await this.loadMeshes(meshes);
124162
+ }
124163
+ return this;
124164
+ }
124189
124165
  /**
124190
124166
  * load an array of volume objects
124191
124167
  * @param volumeList - the array of objects to load. each object must have a resolvable "url" property at a minimum
@@ -124205,27 +124181,7 @@ var Niivue = class {
124205
124181
  this.volumes = [];
124206
124182
  this.gl.clearColor(0, 0, 0, 1);
124207
124183
  this.gl.clear(this.gl.COLOR_BUFFER_BIT);
124208
- if (volumeList.length > 1) {
124209
- await this.addVolumesFromUrl(volumeList);
124210
- return this;
124211
- }
124212
- const imageOptions = {
124213
- url: volumeList[0].url,
124214
- headers: volumeList[0].headers,
124215
- name: volumeList[0].name,
124216
- colormap: volumeList[0].colormap ? volumeList[0].colormap : volumeList[0].colorMap,
124217
- colormapNegative: volumeList[0].colormapNegative ? volumeList[0].colormapNegative : volumeList[0].colorMapNegative,
124218
- opacity: volumeList[0].opacity,
124219
- urlImgData: volumeList[0].urlImgData,
124220
- cal_min: volumeList[0].cal_min,
124221
- cal_max: volumeList[0].cal_max,
124222
- trustCalMinMax: this.opts.trustCalMinMax,
124223
- isManifest: volumeList[0].isManifest,
124224
- frame4D: volumeList[0].frame4D,
124225
- limitFrames4D: volumeList[0].limitFrames4D || this.opts.limitFrames4D,
124226
- colorbarVisible: volumeList[0].colorbarVisible
124227
- };
124228
- await this.addVolumeFromUrl(imageOptions);
124184
+ await this.addVolumesFromUrl(volumeList);
124229
124185
  return this;
124230
124186
  }
124231
124187
  /**
@@ -124256,6 +124212,28 @@ var Niivue = class {
124256
124212
  async addMeshesFromUrl(meshOptions) {
124257
124213
  const promises = meshOptions.map(async (meshItem) => {
124258
124214
  const ext = this.getFileExt(meshItem.url);
124215
+ if (this.loaders[ext]) {
124216
+ let itemToLoad = meshItem.url;
124217
+ const toExt = this.loaders[ext].toExt;
124218
+ let name = meshItem.name || meshItem.url;
124219
+ name = name.split("/").pop();
124220
+ if (typeof meshItem.url === "string") {
124221
+ const url = meshItem.url;
124222
+ try {
124223
+ const response = await fetch(url);
124224
+ if (!response.ok) {
124225
+ throw new Error(`Failed to load file: ${response.statusText}`);
124226
+ }
124227
+ itemToLoad = await response.arrayBuffer();
124228
+ } catch (error) {
124229
+ throw new Error(`Failed to load url ${url}: ${error}`);
124230
+ }
124231
+ }
124232
+ const { positions, indices } = await this.loaders[ext].loader(itemToLoad);
124233
+ meshItem.name = `${name}.${toExt}`;
124234
+ const mz3 = NVMeshUtilities.createMZ3(positions, indices, false);
124235
+ meshItem.buffer = mz3;
124236
+ }
124259
124237
  if (ext === "JCON" || ext === "JSON") {
124260
124238
  const response = await fetch(meshItem.url, {});
124261
124239
  const json = await response.json();
@@ -124296,13 +124274,7 @@ var Niivue = class {
124296
124274
  this.meshes = [];
124297
124275
  this.gl.clearColor(0, 0, 0, 1);
124298
124276
  this.gl.clear(this.gl.COLOR_BUFFER_BIT);
124299
- if (meshList.length > 1) {
124300
- await this.addMeshesFromUrl(meshList);
124301
- this.updateGLVolume();
124302
- this.drawScene();
124303
- return this;
124304
- }
124305
- await this.addMeshFromUrl(meshList[0]);
124277
+ await this.addMeshesFromUrl(meshList);
124306
124278
  this.updateGLVolume();
124307
124279
  this.drawScene();
124308
124280
  return this;
@@ -124576,23 +124548,6 @@ var Niivue = class {
124576
124548
  y = Math.min(Math.max(y, 0), dy - 1);
124577
124549
  z = Math.min(Math.max(z, 0), dz - 1);
124578
124550
  this.drawBitmap[x + y * dx + z * dx * dy] = penValue;
124579
- const isAx = this.drawPenAxCorSag === 0;
124580
- const isCor = this.drawPenAxCorSag === 1;
124581
- const isSag = this.drawPenAxCorSag === 2;
124582
- if (this.opts.penSize > 1) {
124583
- const halfPenSize = Math.floor(this.opts.penSize / 2);
124584
- for (let i2 = -halfPenSize; i2 <= halfPenSize; i2++) {
124585
- for (let j = -halfPenSize; j <= halfPenSize; j++) {
124586
- if (isAx) {
124587
- this.drawBitmap[x + i2 + (y + j) * dx + z * dx * dy] = penValue;
124588
- } else if (isCor) {
124589
- this.drawBitmap[x + i2 + y * dx + (z + j) * dx * dy] = penValue;
124590
- } else if (isSag) {
124591
- this.drawBitmap[x + (y + j) * dx + (z + i2) * dx * dy] = penValue;
124592
- }
124593
- }
124594
- }
124595
- }
124596
124551
  }
124597
124552
  // not included in public docs
124598
124553
  // create line between to voxels in drawing
@@ -124674,98 +124629,6 @@ var Niivue = class {
124674
124629
  }
124675
124630
  }
124676
124631
  }
124677
- /**
124678
- * Performs a 1-voxel binary dilation on a connected cluster within the drawing mask using the drawFloodFillCore function.
124679
- *
124680
- * @param seedXYZ - voxel index of the seed voxel in the mask array.
124681
- * @param neighbors - Number of neighbors to consider for connectivity and dilation (6, 18, or 26).
124682
- */
124683
- drawingBinaryDilationWithSeed(seedXYZ, neighbors = 6) {
124684
- try {
124685
- let xyz2vx = function(pt) {
124686
- return pt[0] + pt[1] * nx + pt[2] * nxy;
124687
- };
124688
- const mask = this.drawBitmap;
124689
- const xDim = this.back.dims[1];
124690
- const yDim = this.back.dims[2];
124691
- const zDim = this.back.dims[3];
124692
- const nx = xDim;
124693
- const nxy = xDim * yDim;
124694
- const totalVoxels = nxy * zDim;
124695
- const seedIndex = xyz2vx(seedXYZ);
124696
- if (seedIndex < 0 || seedIndex >= totalVoxels) {
124697
- throw new Error("Seed index is out of bounds.");
124698
- }
124699
- const seedValue = mask[seedIndex];
124700
- if (seedValue === 0) {
124701
- throw new Error("Seed voxel is not part of a filled cluster.");
124702
- }
124703
- const img = mask.slice();
124704
- for (let i2 = 0; i2 < totalVoxels; i2++) {
124705
- img[i2] = img[i2] === seedValue ? 1 : 0;
124706
- }
124707
- this.drawFloodFillCore(img, seedIndex, neighbors);
124708
- const outputMask = mask.slice();
124709
- const neighborOffsets = [];
124710
- const offsets6 = [-nxy, nxy, -xDim, xDim, -1, 1];
124711
- neighborOffsets.push(...offsets6);
124712
- if (neighbors > 6) {
124713
- neighborOffsets.push(
124714
- -xDim - 1,
124715
- -xDim + 1,
124716
- xDim - 1,
124717
- xDim + 1,
124718
- -nxy - xDim,
124719
- -nxy + xDim,
124720
- -nxy - 1,
124721
- -nxy + 1,
124722
- nxy - xDim,
124723
- nxy + xDim,
124724
- nxy - 1,
124725
- nxy + 1
124726
- );
124727
- }
124728
- if (neighbors > 18) {
124729
- neighborOffsets.push(
124730
- -nxy - xDim - 1,
124731
- -nxy - xDim + 1,
124732
- -nxy + xDim - 1,
124733
- -nxy + xDim + 1,
124734
- nxy - xDim - 1,
124735
- nxy - xDim + 1,
124736
- nxy + xDim - 1,
124737
- nxy + xDim + 1
124738
- );
124739
- }
124740
- for (let idx = 0; idx < totalVoxels; idx++) {
124741
- if (img[idx] === 2) {
124742
- const x = idx % xDim;
124743
- const y = Math.floor(idx % nxy / xDim);
124744
- const z = Math.floor(idx / nxy);
124745
- for (const offset of neighborOffsets) {
124746
- const neighborIdx = idx + offset;
124747
- if (neighborIdx < 0 || neighborIdx >= totalVoxels) {
124748
- continue;
124749
- }
124750
- const nx2 = neighborIdx % xDim;
124751
- const ny = Math.floor(neighborIdx % nxy / xDim);
124752
- const nz = Math.floor(neighborIdx / nxy);
124753
- if (Math.abs(nx2 - x) > 1 || Math.abs(ny - y) > 1 || Math.abs(nz - z) > 1) {
124754
- continue;
124755
- }
124756
- if (mask[neighborIdx] === 0) {
124757
- outputMask[neighborIdx] = seedValue;
124758
- }
124759
- }
124760
- }
124761
- }
124762
- this.drawBitmap = outputMask;
124763
- this.drawAddUndoBitmap();
124764
- this.refreshDrawing(true);
124765
- } catch (error) {
124766
- log.error("Error in drawingBinaryDilationWithSeed:", error);
124767
- }
124768
- }
124769
124632
  // a voxel can be defined as having 6, 18 or 26 neighbors:
124770
124633
  // 6: neighbors share faces (distance=1)
124771
124634
  // 18: neighbors share faces (distance=1) or edges (1.4)
@@ -124847,7 +124710,7 @@ var Niivue = class {
124847
124710
  }
124848
124711
  // not included in public docs
124849
124712
  // set all connected voxels in drawing to new color
124850
- drawFloodFill(seedXYZ, newColor = 0, growSelectedCluster = 0, forceMin = NaN, forceMax = NaN, neighbors = 6, maxDistanceMM = Number.POSITIVE_INFINITY) {
124713
+ drawFloodFill(seedXYZ, newColor = 0, growSelectedCluster = 0, forceMin = NaN, forceMax = NaN, neighbors = 6) {
124851
124714
  if (!this.drawBitmap) {
124852
124715
  throw new Error("drawBitmap undefined");
124853
124716
  }
@@ -124869,25 +124732,9 @@ var Niivue = class {
124869
124732
  if (img.length !== nxy * dims[2]) {
124870
124733
  return;
124871
124734
  }
124872
- function vx2xyz(vx) {
124873
- const Z = Math.floor(vx / nxy);
124874
- const Y = Math.floor((vx - Z * nxy) / nx);
124875
- const X = Math.floor(vx % nx);
124876
- return [X, Y, Z];
124877
- }
124878
124735
  function xyz2vx(pt) {
124879
124736
  return pt[0] + pt[1] * nx + pt[2] * nxy;
124880
124737
  }
124881
- const vx2mm = (xyz) => {
124882
- return this.vox2mm(xyz, this.back.matRAS);
124883
- };
124884
- const seedMM = vx2mm(seedXYZ);
124885
- const maxDistanceMM2 = maxDistanceMM ** 2;
124886
- function isWithinDistance(vx) {
124887
- const xyzMM = vx2mm(vx2xyz(vx));
124888
- const dist22 = (xyzMM[0] - seedMM[0]) ** 2 + (xyzMM[1] - seedMM[1]) ** 2 + (xyzMM[2] - seedMM[2]) ** 2;
124889
- return dist22 <= maxDistanceMM2;
124890
- }
124891
124738
  const seedVx = xyz2vx(seedXYZ);
124892
124739
  const seedColor = img[seedVx];
124893
124740
  if (seedColor === newColor) {
@@ -124901,9 +124748,6 @@ var Niivue = class {
124901
124748
  for (let i2 = 1; i2 < nxyz; i2++) {
124902
124749
  img[i2] = 0;
124903
124750
  if (this.drawBitmap[i2] === seedColor) {
124904
- if (!isWithinDistance(i2)) {
124905
- continue;
124906
- }
124907
124751
  img[i2] = 1;
124908
124752
  }
124909
124753
  }
@@ -124933,9 +124777,6 @@ var Niivue = class {
124933
124777
  for (let i2 = 1; i2 < nxyz; i2++) {
124934
124778
  img[i2] = 0;
124935
124779
  if (backImg[i2] >= mn && backImg[i2] <= mx) {
124936
- if (!isWithinDistance(i2)) {
124937
- continue;
124938
- }
124939
124780
  img[i2] = 1;
124940
124781
  }
124941
124782
  }
@@ -125557,8 +125398,6 @@ var Niivue = class {
125557
125398
  gl.bindVertexArray(this.unusedVAO);
125558
125399
  this.rectShader = new Shader(gl, vertRectShader, fragRectShader);
125559
125400
  this.rectShader.use(gl);
125560
- this.rectOutlineShader = new Shader(gl, vertRectShader, fragRectOutlineShader);
125561
- this.rectOutlineShader.use(gl);
125562
125401
  this.lineShader = new Shader(gl, vertLineShader, fragRectShader);
125563
125402
  this.lineShader.use(gl);
125564
125403
  this.line3DShader = new Shader(gl, vertLine3DShader, fragRectShader);
@@ -125711,37 +125550,14 @@ var Niivue = class {
125711
125550
  }
125712
125551
  /**
125713
125552
  * basic statistics for selected voxel-based image
125714
- * @param options - an object containing the following properties:
125715
- * - layer: selects image to describe
125716
- * - masks: optional binary images to filter voxels
125717
- * - drawingIsMask: a boolean indicating if the drawing is used as a mask
125718
- * - roiIsMask: a boolean indicating if the ROI is used as a mask
125719
- * - startVox: the starting voxel coordinates
125720
- * - endVox: the ending voxel coordinates
125721
- * @returns numeric values to describe image or regions of images
125722
- * @example
125723
- * niivue.getDescriptives({
125724
- * layer: 0,
125725
- * masks: [],
125726
- * drawingIsMask: true, // drawingIsMask and roiIsMask are mutually exclusive
125727
- * roiIsMask: false,
125728
- * startVox: [10, 20, 30], // ignored if roiIsMask is false
125729
- * endVox: [40, 50, 60] // ignored if roiIsMask is false
125730
- * });
125553
+ * @param layer - selects image to describe
125554
+ * @param masks - are optional binary images to filter voxles
125555
+ * @returns numeric values to describe image
125556
+ * @example niivue.getDescriptives(0);
125731
125557
  * @see {@link https://niivue.github.io/niivue/features/draw2.html | live demo usage}
125732
125558
  */
125733
- getDescriptives(options) {
125734
- const {
125735
- layer = 0,
125736
- masks = [],
125737
- drawingIsMask = false,
125738
- roiIsMask = false,
125739
- startVox = [0, 0, 0],
125740
- endVox = [0, 0, 0]
125741
- } = options;
125742
- let area = null;
125559
+ getDescriptives(layer = 0, masks = [], drawingIsMask = false) {
125743
125560
  const hdr = this.volumes[layer].hdr;
125744
- const pixDimsRAS = this.volumes[layer].pixDimsRAS;
125745
125561
  let slope = hdr.scl_slope;
125746
125562
  if (isNaN(slope)) {
125747
125563
  slope = 1;
@@ -125779,37 +125595,6 @@ var Niivue = class {
125779
125595
  mask[i2] = 0;
125780
125596
  }
125781
125597
  }
125782
- } else if (masks.length < 1 && roiIsMask) {
125783
- mask.fill(0);
125784
- console.log("startVox", startVox);
125785
- console.log("endVox", endVox);
125786
- const xrange = this.calculateMinMaxVoxIdx([startVox[0], endVox[0]]);
125787
- const yrange = this.calculateMinMaxVoxIdx([startVox[1], endVox[1]]);
125788
- const zrange = this.calculateMinMaxVoxIdx([startVox[2], endVox[2]]);
125789
- if (startVox[0] - endVox[0] === 0) {
125790
- xrange[1] = startVox[0] + 1;
125791
- } else if (startVox[1] - endVox[1] === 0) {
125792
- yrange[1] = startVox[1] + 1;
125793
- } else if (startVox[2] - endVox[2] === 0) {
125794
- zrange[1] = startVox[2] + 1;
125795
- }
125796
- const xdim = hdr.dims[1];
125797
- const ydim = hdr.dims[2];
125798
- for (let z = zrange[0]; z < zrange[1]; z++) {
125799
- const zi = z * xdim * ydim;
125800
- for (let y = yrange[0]; y < yrange[1]; y++) {
125801
- const yi = y * xdim;
125802
- for (let x = xrange[0]; x < xrange[1]; x++) {
125803
- const index = zi + yi + x;
125804
- mask[index] = 1;
125805
- }
125806
- }
125807
- }
125808
- const xmm = Math.abs(pixDimsRAS[1] * (endVox[0] - startVox[0]));
125809
- const ymm = Math.abs(pixDimsRAS[2] * (endVox[1] - startVox[1]));
125810
- const zmm = Math.abs(pixDimsRAS[3] * (endVox[2] - startVox[2]));
125811
- const mms = [xmm, ymm, zmm];
125812
- area = mms.filter((m) => m !== 0).reduce((a, b) => a * b, 1);
125813
125598
  }
125814
125599
  let k = 0;
125815
125600
  let M = 0;
@@ -125863,8 +125648,7 @@ var Niivue = class {
125863
125648
  cal_min: this.volumes[layer].cal_min,
125864
125649
  cal_max: this.volumes[layer].cal_max,
125865
125650
  robust_min: this.volumes[layer].robust_min,
125866
- robust_max: this.volumes[layer].robust_max,
125867
- area
125651
+ robust_max: this.volumes[layer].robust_max
125868
125652
  };
125869
125653
  }
125870
125654
  // not included in public docs
@@ -127419,51 +127203,30 @@ var Niivue = class {
127419
127203
  }
127420
127204
  if (this.opts.drawingEnabled) {
127421
127205
  const pt = this.frac2vox(this.scene.crosshairPos);
127206
+ const ptMM = this.frac2mm(this.scene.crosshairPos);
127422
127207
  if (this.opts.clickToSegment) {
127423
- const voxelIntensity = this.back.getValue(pt[0], pt[1], pt[2]);
127424
- if (this.opts.clickToSegmentPercent > 0 && this.opts.clickToSegmentPercent <= 1) {
127425
- this.opts.clickToSegmentIntensityMax = voxelIntensity * (1 + this.opts.clickToSegmentPercent);
127426
- this.opts.clickToSegmentIntensityMin = voxelIntensity * (1 - this.opts.clickToSegmentPercent);
127427
- }
127428
- if (this.opts.clickToSegmentAutoIntensity) {
127429
- if (voxelIntensity > (this.back.cal_min + this.back.cal_max) * 0.5) {
127430
- this.opts.clickToSegmentBright = true;
127431
- } else {
127432
- this.opts.clickToSegmentBright = false;
127433
- }
127434
- }
127208
+ const radius = this.opts.clickToSegmentRadius;
127209
+ const steps = this.opts.clickToSegmentSteps;
127435
127210
  const brightOrDark = this.opts.clickToSegmentBright ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
127211
+ this.drawPenFillPts = [];
127436
127212
  this.drawPenAxCorSag = axCorSag;
127437
- if (this.drawPenAxCorSag === 0 /* AXIAL */) {
127438
- const pixDims = [this.back.pixDimsRAS[0], this.back.pixDimsRAS[1]];
127439
- const minPixDim = Math.min(...pixDims);
127440
- this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim);
127441
- } else if (this.drawPenAxCorSag === 1 /* CORONAL */) {
127442
- const pixDims = [this.back.pixDimsRAS[0], this.back.pixDimsRAS[2]];
127443
- const minPixDim = Math.min(...pixDims);
127444
- this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim);
127445
- } else if (this.drawPenAxCorSag === 2 /* SAGITTAL */) {
127446
- const pixDims = [this.back.pixDimsRAS[1], this.back.pixDimsRAS[2]];
127447
- const minPixDim = Math.min(...pixDims);
127448
- this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim);
127449
- }
127450
- this.drawPt(pt[0], pt[1], pt[2], this.opts.penValue);
127451
- this.drawFloodFill(
127452
- [pt[0], pt[1], pt[2]],
127453
- 0,
127454
- brightOrDark,
127455
- this.opts.clickToSegmentIntensityMin,
127456
- this.opts.clickToSegmentIntensityMax,
127457
- this.opts.floodFillNeighbors,
127458
- this.opts.clickToSegmentMaxDistanceMM
127459
- );
127213
+ for (let i3 = 1; i3 <= steps; i3++) {
127214
+ const angle3 = i3 / steps * 2 * Math.PI;
127215
+ const xMM = ptMM[0] + radius * Math.cos(angle3);
127216
+ const yMM = ptMM[1] + radius * Math.sin(angle3);
127217
+ const zMM = ptMM[2];
127218
+ const xVox = this.back.mm2vox([xMM, yMM, zMM])[0];
127219
+ const yVox = this.back.mm2vox([xMM, yMM, zMM])[1];
127220
+ const zVox = this.back.mm2vox([xMM, yMM, zMM])[2];
127221
+ this.drawPt(xVox, yVox, zVox, this.opts.penValue);
127222
+ this.drawPenFillPts.push([xVox, yVox, pt[2]]);
127223
+ if (i3 === steps) {
127224
+ this.drawFloodFill([xVox, yVox, pt[2]], 0, brightOrDark, NaN, NaN, this.opts.floodFillNeighbors);
127225
+ }
127226
+ }
127460
127227
  this.drawScene();
127461
127228
  this.createOnLocationChange(axCorSag);
127462
- const info = this.getDescriptives({
127463
- layer: 0,
127464
- masks: [],
127465
- drawingIsMask: true
127466
- });
127229
+ const info = this.getDescriptives(0, [], true);
127467
127230
  this.onClickToSegment({ mL: info.volumeML, mm3: info.volumeMM3 });
127468
127231
  return;
127469
127232
  }
@@ -127681,41 +127444,20 @@ var Niivue = class {
127681
127444
  if (!this.rectShader) {
127682
127445
  throw new Error("rectShader undefined");
127683
127446
  }
127684
- if (!this.opts.selectionBoxIsOutline) {
127685
- this.rectShader.use(this.gl);
127686
- this.gl.enable(this.gl.BLEND);
127687
- this.gl.uniform4fv(this.rectShader.uniforms.lineColor, lineColor);
127688
- this.gl.uniform2fv(this.rectShader.uniforms.canvasWidthHeight, [this.gl.canvas.width, this.gl.canvas.height]);
127689
- this.gl.uniform4f(
127690
- this.rectShader.uniforms.leftTopWidthHeight,
127691
- leftTopWidthHeight[0],
127692
- leftTopWidthHeight[1],
127693
- leftTopWidthHeight[2],
127694
- leftTopWidthHeight[3]
127695
- );
127696
- this.gl.bindVertexArray(this.genericVAO);
127697
- this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
127698
- this.gl.bindVertexArray(this.unusedVAO);
127699
- } else {
127700
- this.rectOutlineShader.use(this.gl);
127701
- this.gl.enable(this.gl.BLEND);
127702
- this.gl.uniform1f(this.rectOutlineShader.uniforms.thickness, this.opts.selectionBoxLineThickness);
127703
- this.gl.uniform4fv(this.rectOutlineShader.uniforms.lineColor, lineColor);
127704
- this.gl.uniform2fv(this.rectOutlineShader.uniforms.canvasWidthHeight, [
127705
- this.gl.canvas.width,
127706
- this.gl.canvas.height
127707
- ]);
127708
- this.gl.uniform4f(
127709
- this.rectOutlineShader.uniforms.leftTopWidthHeight,
127710
- leftTopWidthHeight[0],
127711
- leftTopWidthHeight[1],
127712
- leftTopWidthHeight[2],
127713
- leftTopWidthHeight[3]
127714
- );
127715
- this.gl.bindVertexArray(this.genericVAO);
127716
- this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
127717
- this.gl.bindVertexArray(this.unusedVAO);
127718
- }
127447
+ this.rectShader.use(this.gl);
127448
+ this.gl.enable(this.gl.BLEND);
127449
+ this.gl.uniform4fv(this.rectShader.uniforms.lineColor, lineColor);
127450
+ this.gl.uniform2fv(this.rectShader.uniforms.canvasWidthHeight, [this.gl.canvas.width, this.gl.canvas.height]);
127451
+ this.gl.uniform4f(
127452
+ this.rectShader.uniforms.leftTopWidthHeight,
127453
+ leftTopWidthHeight[0],
127454
+ leftTopWidthHeight[1],
127455
+ leftTopWidthHeight[2],
127456
+ leftTopWidthHeight[3]
127457
+ );
127458
+ this.gl.bindVertexArray(this.genericVAO);
127459
+ this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
127460
+ this.gl.bindVertexArray(this.unusedVAO);
127719
127461
  }
127720
127462
  drawCircle(leftTopWidthHeight, circleColor = this.opts.fontColor, fillPercent = 1) {
127721
127463
  if (!this.circleShader) {
@@ -130417,14 +130159,10 @@ var Niivue = class {
130417
130159
  width,
130418
130160
  height
130419
130161
  ]);
130420
- return;
130421
130162
  }
130422
130163
  if (this.opts.clickToSegment) {
130423
130164
  const x = this.mousePos[0];
130424
130165
  const y = this.mousePos[1];
130425
- if (this.inRenderTile(x, y) >= 0) {
130426
- return;
130427
- }
130428
130166
  const tileIdx = this.tileIndex(x, y);
130429
130167
  if (tileIdx > -1) {
130430
130168
  const fovMM = this.screenSlices[tileIdx].fovMM;