@niivue/niivue 0.44.2-dev.3 → 0.44.2-dev.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.45.1";
13
+ var version = "0.44.2-dev.4";
14
14
 
15
15
  // src/logger.ts
16
16
  var _Log = class _Log {
@@ -806,6 +806,46 @@ 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
+ }`;
809
849
  var vertColorbarShader = `#version 300 es
810
850
  #line 490
811
851
  layout(location=0) in vec3 pos;
@@ -22250,6 +22290,7 @@ var DRAG_MODE = /* @__PURE__ */ ((DRAG_MODE2) => {
22250
22290
  DRAG_MODE2[DRAG_MODE2["pan"] = 3] = "pan";
22251
22291
  DRAG_MODE2[DRAG_MODE2["slicer3D"] = 4] = "slicer3D";
22252
22292
  DRAG_MODE2[DRAG_MODE2["callbackOnly"] = 5] = "callbackOnly";
22293
+ DRAG_MODE2[DRAG_MODE2["roiSelection"] = 6] = "roiSelection";
22253
22294
  return DRAG_MODE2;
22254
22295
  })(DRAG_MODE || {});
22255
22296
  var DEFAULT_OPTIONS = {
@@ -22319,10 +22360,30 @@ var DEFAULT_OPTIONS = {
22319
22360
  renderOverlayBlend: 1,
22320
22361
  sliceMosaicString: "",
22321
22362
  centerMosaic: false,
22363
+ penSize: 1,
22364
+ // in voxels, since all drawing is done using bitmap indices
22322
22365
  clickToSegment: false,
22323
- clickToSegmentRadius: 2,
22324
- clickToSegmentSteps: 10,
22325
- clickToSegmentBright: true
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
+ clickToSegmentIs2D: false,
22383
+ selectionBoxLineThickness: 4,
22384
+ selectionBoxIsOutline: false,
22385
+ scrollRequiresFocus: false
22386
+ // determines if the cavas need to be focused to scroll
22326
22387
  };
22327
22388
  var INITIAL_SCENE_DATA = {
22328
22389
  gamma: 1,
@@ -24019,12 +24080,11 @@ var defaultSaveImageOptions = {
24019
24080
  isSaveDrawing: false,
24020
24081
  volumeByIndex: 0
24021
24082
  };
24022
- var Niivue = class _Niivue {
24083
+ var Niivue = class {
24023
24084
  /**
24024
24085
  * @param options - options object to set modifiable Niivue properties
24025
24086
  */
24026
24087
  constructor(options = DEFAULT_OPTIONS) {
24027
- __publicField(this, "loaders", {});
24028
24088
  __publicField(this, "canvas", null);
24029
24089
  // the reference to the canvas element on the page
24030
24090
  __publicField(this, "_gl", null);
@@ -24050,6 +24110,12 @@ var Niivue = class _Niivue {
24050
24110
  // the color lookup table for drawing
24051
24111
  __publicField(this, "drawOpacity", 0.8);
24052
24112
  // opacity of drawing (default)
24113
+ __publicField(this, "clickToSegmentIsGrowing", false);
24114
+ // flag to indicate if the clickToSegment flood fill growing is in progress with left mouse down + drag
24115
+ __publicField(this, "clickToSegmentGrowingBitmap", null);
24116
+ // the bitmap of the growing flood fill
24117
+ __publicField(this, "clickToSegmentXY", [0, 0]);
24118
+ // the x,y location of the clickToSegment flood fill
24053
24119
  __publicField(this, "renderDrawAmbientOcclusion", 0.4);
24054
24120
  __publicField(this, "colorbarHeight", 0);
24055
24121
  // height in pixels, set when colorbar is drawn
@@ -24067,6 +24133,7 @@ var Niivue = class _Niivue {
24067
24133
  __publicField(this, "orientCubeShader");
24068
24134
  __publicField(this, "orientCubeShaderVAO", null);
24069
24135
  __publicField(this, "rectShader");
24136
+ __publicField(this, "rectOutlineShader");
24070
24137
  __publicField(this, "renderShader");
24071
24138
  __publicField(this, "lineShader");
24072
24139
  __publicField(this, "line3DShader");
@@ -24119,7 +24186,20 @@ var Niivue = class _Niivue {
24119
24186
  __publicField(this, "extentsMax");
24120
24187
  // ResizeObserver
24121
24188
  __publicField(this, "resizeObserver", null);
24122
- __publicField(this, "syncOpts", {});
24189
+ // syncOpts: Record<string, unknown> = {}
24190
+ __publicField(this, "syncOpts", {
24191
+ "3d": false,
24192
+ // legacy option
24193
+ "2d": false,
24194
+ // legacy option
24195
+ zoomPan: false,
24196
+ cal_min: false,
24197
+ cal_max: false,
24198
+ clipPlane: false,
24199
+ gamma: false,
24200
+ sliceType: false,
24201
+ crosshair: false
24202
+ });
24123
24203
  __publicField(this, "readyForSync", false);
24124
24204
  // UI Data
24125
24205
  __publicField(this, "uiData", {
@@ -24598,6 +24678,9 @@ var Niivue = class _Niivue {
24598
24678
  * @see {@link https://niivue.github.io/niivue/features/sync.mesh.html | live demo usage}
24599
24679
  */
24600
24680
  syncWith(otherNV, syncOpts = { "2d": true, "3d": true }) {
24681
+ if (!(otherNV instanceof Array)) {
24682
+ otherNV = [otherNV];
24683
+ }
24601
24684
  this.otherNV = otherNV;
24602
24685
  this.syncOpts = { ...syncOpts };
24603
24686
  }
@@ -24613,9 +24696,55 @@ var Niivue = class _Niivue {
24613
24696
  * @see {@link https://niivue.github.io/niivue/features/sync.mesh.html | live demo usage}
24614
24697
  */
24615
24698
  broadcastTo(otherNV, syncOpts = { "2d": true, "3d": true }) {
24699
+ if (!(otherNV instanceof Array)) {
24700
+ otherNV = [otherNV];
24701
+ }
24616
24702
  this.otherNV = otherNV;
24617
24703
  this.syncOpts = syncOpts;
24618
24704
  }
24705
+ doSync3d(otherNV) {
24706
+ otherNV.scene.renderAzimuth = this.scene.renderAzimuth;
24707
+ otherNV.scene.renderElevation = this.scene.renderElevation;
24708
+ otherNV.scene.volScaleMultiplier = this.scene.volScaleMultiplier;
24709
+ }
24710
+ // both crosshair and zoomPan
24711
+ doSync2d(otherNV) {
24712
+ const thisMM = this.frac2mm(this.scene.crosshairPos);
24713
+ otherNV.scene.crosshairPos = otherNV.mm2frac(thisMM);
24714
+ otherNV.scene.pan2Dxyzmm = vec48.clone(this.scene.pan2Dxyzmm);
24715
+ }
24716
+ doSyncGamma(otherNV) {
24717
+ const thisGamma = this.scene.gamma;
24718
+ const otherGamma = otherNV.scene.gamma;
24719
+ if (thisGamma !== otherGamma) {
24720
+ otherNV.setGamma(thisGamma);
24721
+ }
24722
+ }
24723
+ doSyncZoomPan(otherNV) {
24724
+ otherNV.scene.pan2Dxyzmm = vec48.clone(this.scene.pan2Dxyzmm);
24725
+ }
24726
+ doSyncCrosshair(otherNV) {
24727
+ const thisMM = this.frac2mm(this.scene.crosshairPos);
24728
+ otherNV.scene.crosshairPos = otherNV.mm2frac(thisMM);
24729
+ }
24730
+ doSyncCalMin(otherNV) {
24731
+ if (this.volumes[0].cal_min !== otherNV.volumes[0].cal_min) {
24732
+ otherNV.volumes[0].cal_min = this.volumes[0].cal_min;
24733
+ otherNV.updateGLVolume();
24734
+ }
24735
+ }
24736
+ doSyncCalMax(otherNV) {
24737
+ if (this.volumes[0].cal_max !== otherNV.volumes[0].cal_max) {
24738
+ otherNV.volumes[0].cal_max = this.volumes[0].cal_max;
24739
+ otherNV.updateGLVolume();
24740
+ }
24741
+ }
24742
+ doSyncSliceType(otherNV) {
24743
+ otherNV.setSliceType(this.opts.sliceType);
24744
+ }
24745
+ doSyncClipPlane(otherNV) {
24746
+ otherNV.setClipPlane(this.scene.clipPlaneDepthAziElev);
24747
+ }
24619
24748
  /**
24620
24749
  * Sync the scene controls (orientation, crosshair location, etc.) from one Niivue instance to another. useful for using one canvas to drive another.
24621
24750
  * @internal
@@ -24632,46 +24761,39 @@ var Niivue = class _Niivue {
24632
24761
  if (!this.gl.canvas.matches(":focus")) {
24633
24762
  return;
24634
24763
  }
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);
24764
+ for (let i = 0; i < this.otherNV.length; i++) {
24765
+ if (this.otherNV[i] === this) {
24766
+ continue;
24767
+ }
24768
+ if (this.syncOpts.gamma) {
24769
+ this.doSyncGamma(this.otherNV[i]);
24770
+ }
24771
+ if (this.syncOpts.crosshair) {
24772
+ this.doSyncCrosshair(this.otherNV[i]);
24773
+ }
24774
+ if (this.syncOpts.zoomPan) {
24775
+ this.doSyncZoomPan(this.otherNV[i]);
24776
+ }
24777
+ if (this.syncOpts.sliceType) {
24778
+ this.doSyncSliceType(this.otherNV[i]);
24779
+ }
24780
+ if (this.syncOpts.cal_min) {
24781
+ this.doSyncCalMin(this.otherNV[i]);
24782
+ }
24783
+ if (this.syncOpts.cal_max) {
24784
+ this.doSyncCalMax(this.otherNV[i]);
24785
+ }
24786
+ if (this.syncOpts.clipPlane) {
24787
+ this.doSyncClipPlane(this.otherNV[i]);
24641
24788
  }
24642
24789
  if (this.syncOpts["2d"]) {
24643
- this.otherNV.scene.crosshairPos = this.otherNV.mm2frac(thisMM);
24644
- this.otherNV.scene.pan2Dxyzmm = vec48.clone(this.scene.pan2Dxyzmm);
24790
+ this.doSync2d(this.otherNV[i]);
24645
24791
  }
24646
24792
  if (this.syncOpts["3d"]) {
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();
24793
+ this.doSync3d(this.otherNV[i]);
24674
24794
  }
24795
+ this.otherNV[i].drawScene();
24796
+ this.otherNV[i].createOnLocationChange();
24675
24797
  }
24676
24798
  }
24677
24799
  /** Not documented publicly for now
@@ -24766,6 +24888,8 @@ var Niivue = class _Niivue {
24766
24888
  this.drawPenLocation = [NaN, NaN, NaN];
24767
24889
  this.drawPenAxCorSag = -1;
24768
24890
  this.uiData.mousedown = true;
24891
+ this.setDragStart(0, 0);
24892
+ this.setDragEnd(0, 0);
24769
24893
  log.debug("mouse down");
24770
24894
  log.debug(e);
24771
24895
  const pos = this.getNoPaddingNoBorderCanvasRelativeMousePosition(e, this.gl.canvas);
@@ -24773,6 +24897,9 @@ var Niivue = class _Niivue {
24773
24897
  return;
24774
24898
  }
24775
24899
  const [x, y] = [pos.x * this.uiData.dpr, pos.y * this.uiData.dpr];
24900
+ if (this.opts.clickToSegment) {
24901
+ this.clickToSegmentXY = [x, y];
24902
+ }
24776
24903
  const label = this.getLabelAtPoint([x, y]);
24777
24904
  if (label) {
24778
24905
  if (label.onClick) {
@@ -24973,8 +25100,15 @@ var Niivue = class _Niivue {
24973
25100
  if (this.drawPenFillPts.length > 0) {
24974
25101
  this.drawPenFilled();
24975
25102
  } else if (this.drawPenAxCorSag >= 0) {
24976
- this.drawAddUndoBitmap();
25103
+ if (this.opts.clickToSegment) {
25104
+ const sumBitmap = this.sumBitmap(this.drawBitmap);
25105
+ const sumSeg = this.sumBitmap(this.clickToSegmentGrowingBitmap);
25106
+ if (sumSeg > sumBitmap) {
25107
+ this.updateBitmapFromClickToSegment();
25108
+ }
25109
+ }
24977
25110
  }
25111
+ this.drawAddUndoBitmap();
24978
25112
  this.drawPenLocation = [NaN, NaN, NaN];
24979
25113
  this.drawPenAxCorSag = -1;
24980
25114
  if (isFunction(this.onMouseUp)) {
@@ -24988,6 +25122,9 @@ var Niivue = class _Niivue {
24988
25122
  const fracStart = this.canvasPos2frac([this.uiData.dragStart[0], this.uiData.dragStart[1]]);
24989
25123
  const fracEnd = this.canvasPos2frac([this.uiData.dragEnd[0], this.uiData.dragEnd[1]]);
24990
25124
  this.generateMouseUpCallback(fracStart, fracEnd);
25125
+ if (this.opts.dragMode === 6 /* roiSelection */) {
25126
+ return;
25127
+ }
24991
25128
  if (this.opts.dragMode !== 1 /* contrast */) {
24992
25129
  return;
24993
25130
  }
@@ -25290,6 +25427,35 @@ var Niivue = class _Niivue {
25290
25427
  if (this.thumbnailVisible) {
25291
25428
  return;
25292
25429
  }
25430
+ const dragStartSum = this.uiData.dragStart.reduce((a, b) => a + b, 0);
25431
+ const dragEndSum = this.uiData.dragEnd.reduce((a, b) => a + b, 0);
25432
+ const validDrag = dragStartSum > 0 && dragEndSum > 0;
25433
+ if (this.opts.dragMode === 6 /* roiSelection */ && validDrag) {
25434
+ const delta = e.deltaY > 0 ? 1 : -1;
25435
+ if (this.uiData.dragStart[0] < this.uiData.dragEnd[0]) {
25436
+ this.uiData.dragStart[0] -= delta;
25437
+ this.uiData.dragEnd[0] += delta;
25438
+ } else {
25439
+ this.uiData.dragStart[0] += delta;
25440
+ this.uiData.dragEnd[0] -= delta;
25441
+ }
25442
+ if (this.uiData.dragStart[1] < this.uiData.dragEnd[1]) {
25443
+ this.uiData.dragStart[1] -= delta;
25444
+ this.uiData.dragEnd[1] += delta;
25445
+ } else {
25446
+ this.uiData.dragStart[1] += delta;
25447
+ this.uiData.dragEnd[1] -= delta;
25448
+ }
25449
+ this.uiData.isDragging = true;
25450
+ this.drawScene();
25451
+ this.uiData.isDragging = false;
25452
+ const tileIdx = this.tileIndex(this.uiData.dragStart[0], this.uiData.dragStart[1]);
25453
+ this.generateMouseUpCallback(
25454
+ this.screenXY2TextureFrac(this.uiData.dragStart[0], this.uiData.dragStart[1], tileIdx),
25455
+ this.screenXY2TextureFrac(this.uiData.dragEnd[0], this.uiData.dragEnd[1], tileIdx)
25456
+ );
25457
+ return;
25458
+ }
25293
25459
  const rect = this.canvas.getBoundingClientRect();
25294
25460
  if (e.deltaY < 0) {
25295
25461
  this.sliceScroll2D(-0.01, e.clientX - rect.left, e.clientY - rect.top);
@@ -25339,11 +25505,6 @@ var Niivue = class _Niivue {
25339
25505
  if (ext === "GZ") {
25340
25506
  ext = re.exec(fullname.slice(0, -3))[1];
25341
25507
  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}`;
25347
25508
  }
25348
25509
  return upperCase ? ext : ext.toLowerCase();
25349
25510
  }
@@ -25364,28 +25525,6 @@ var Niivue = class _Niivue {
25364
25525
  }
25365
25526
  async addVolumesFromUrl(imageOptionsArray) {
25366
25527
  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
- }
25389
25528
  const imageOptions = {
25390
25529
  url: imageItem.url,
25391
25530
  headers: imageItem.headers,
@@ -25525,40 +25664,6 @@ var Niivue = class _Niivue {
25525
25664
  this.addVolume(volume);
25526
25665
  });
25527
25666
  }
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
- }
25562
25667
  // not included in public docs
25563
25668
  dropListener(e) {
25564
25669
  e.stopPropagation();
@@ -26783,6 +26888,10 @@ var Niivue = class _Niivue {
26783
26888
  }
26784
26889
  // not included in public docs
26785
26890
  sliceScroll2D(posChange, x, y, isDelta = true) {
26891
+ if (this.opts.scrollRequiresFocus && this.canvas !== document.activeElement) {
26892
+ log.warn("Canvas element does not have focus. Scroll events will not be processed.");
26893
+ return;
26894
+ }
26786
26895
  if (this.inGraphTile(x, y)) {
26787
26896
  let vol = this.volumes[0].frame4D;
26788
26897
  if (posChange > 0) {
@@ -27230,37 +27339,6 @@ var Niivue = class _Niivue {
27230
27339
  this.document.meshes = this.meshes;
27231
27340
  return this.document.download(fileName, compress2);
27232
27341
  }
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
- }
27264
27342
  /**
27265
27343
  * load an array of volume objects
27266
27344
  * @param volumeList - the array of objects to load. each object must have a resolvable "url" property at a minimum
@@ -27280,7 +27358,27 @@ var Niivue = class _Niivue {
27280
27358
  this.volumes = [];
27281
27359
  this.gl.clearColor(0, 0, 0, 1);
27282
27360
  this.gl.clear(this.gl.COLOR_BUFFER_BIT);
27283
- await this.addVolumesFromUrl(volumeList);
27361
+ if (volumeList.length > 1) {
27362
+ await this.addVolumesFromUrl(volumeList);
27363
+ return this;
27364
+ }
27365
+ const imageOptions = {
27366
+ url: volumeList[0].url,
27367
+ headers: volumeList[0].headers,
27368
+ name: volumeList[0].name,
27369
+ colormap: volumeList[0].colormap ? volumeList[0].colormap : volumeList[0].colorMap,
27370
+ colormapNegative: volumeList[0].colormapNegative ? volumeList[0].colormapNegative : volumeList[0].colorMapNegative,
27371
+ opacity: volumeList[0].opacity,
27372
+ urlImgData: volumeList[0].urlImgData,
27373
+ cal_min: volumeList[0].cal_min,
27374
+ cal_max: volumeList[0].cal_max,
27375
+ trustCalMinMax: this.opts.trustCalMinMax,
27376
+ isManifest: volumeList[0].isManifest,
27377
+ frame4D: volumeList[0].frame4D,
27378
+ limitFrames4D: volumeList[0].limitFrames4D || this.opts.limitFrames4D,
27379
+ colorbarVisible: volumeList[0].colorbarVisible
27380
+ };
27381
+ await this.addVolumeFromUrl(imageOptions);
27284
27382
  return this;
27285
27383
  }
27286
27384
  /**
@@ -27310,29 +27408,7 @@ var Niivue = class _Niivue {
27310
27408
  */
27311
27409
  async addMeshesFromUrl(meshOptions) {
27312
27410
  const promises = meshOptions.map(async (meshItem) => {
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
- }
27411
+ const ext = this.getFileExt(meshItem.url);
27336
27412
  if (ext === "JCON" || ext === "JSON") {
27337
27413
  const response = await fetch(meshItem.url, {});
27338
27414
  const json = await response.json();
@@ -27373,7 +27449,13 @@ var Niivue = class _Niivue {
27373
27449
  this.meshes = [];
27374
27450
  this.gl.clearColor(0, 0, 0, 1);
27375
27451
  this.gl.clear(this.gl.COLOR_BUFFER_BIT);
27376
- await this.addMeshesFromUrl(meshList);
27452
+ if (meshList.length > 1) {
27453
+ await this.addMeshesFromUrl(meshList);
27454
+ this.updateGLVolume();
27455
+ this.drawScene();
27456
+ return this;
27457
+ }
27458
+ await this.addMeshFromUrl(meshList[0]);
27377
27459
  this.updateGLVolume();
27378
27460
  this.drawScene();
27379
27461
  return this;
@@ -27474,6 +27556,7 @@ var Niivue = class _Niivue {
27474
27556
  }
27475
27557
  const vx = this.back.dims[1] * this.back.dims[2] * this.back.dims[3];
27476
27558
  this.drawBitmap = new Uint8Array(vx);
27559
+ this.clickToSegmentGrowingBitmap = new Uint8Array(vx);
27477
27560
  this.drawClearAllUndoBitmaps();
27478
27561
  this.drawAddUndoBitmap();
27479
27562
  this.drawTexture = this.r8Tex(this.drawTexture, TEXTURE7_DRAW, this.back.dims, true);
@@ -27647,6 +27730,23 @@ var Niivue = class _Niivue {
27647
27730
  y = Math.min(Math.max(y, 0), dy - 1);
27648
27731
  z = Math.min(Math.max(z, 0), dz - 1);
27649
27732
  this.drawBitmap[x + y * dx + z * dx * dy] = penValue;
27733
+ const isAx = this.drawPenAxCorSag === 0;
27734
+ const isCor = this.drawPenAxCorSag === 1;
27735
+ const isSag = this.drawPenAxCorSag === 2;
27736
+ if (this.opts.penSize > 1) {
27737
+ const halfPenSize = Math.floor(this.opts.penSize / 2);
27738
+ for (let i = -halfPenSize; i <= halfPenSize; i++) {
27739
+ for (let j = -halfPenSize; j <= halfPenSize; j++) {
27740
+ if (isAx) {
27741
+ this.drawBitmap[x + i + (y + j) * dx + z * dx * dy] = penValue;
27742
+ } else if (isCor) {
27743
+ this.drawBitmap[x + i + y * dx + (z + j) * dx * dy] = penValue;
27744
+ } else if (isSag) {
27745
+ this.drawBitmap[x + (y + j) * dx + (z + i) * dx * dy] = penValue;
27746
+ }
27747
+ }
27748
+ }
27749
+ }
27650
27750
  }
27651
27751
  // not included in public docs
27652
27752
  // create line between to voxels in drawing
@@ -27728,6 +27828,98 @@ var Niivue = class _Niivue {
27728
27828
  }
27729
27829
  }
27730
27830
  }
27831
+ /**
27832
+ * Performs a 1-voxel binary dilation on a connected cluster within the drawing mask using the drawFloodFillCore function.
27833
+ *
27834
+ * @param seedXYZ - voxel index of the seed voxel in the mask array.
27835
+ * @param neighbors - Number of neighbors to consider for connectivity and dilation (6, 18, or 26).
27836
+ */
27837
+ drawingBinaryDilationWithSeed(seedXYZ, neighbors = 6) {
27838
+ try {
27839
+ let xyz2vx = function(pt) {
27840
+ return pt[0] + pt[1] * nx + pt[2] * nxy;
27841
+ };
27842
+ const mask = this.drawBitmap;
27843
+ const xDim = this.back.dims[1];
27844
+ const yDim = this.back.dims[2];
27845
+ const zDim = this.back.dims[3];
27846
+ const nx = xDim;
27847
+ const nxy = xDim * yDim;
27848
+ const totalVoxels = nxy * zDim;
27849
+ const seedIndex = xyz2vx(seedXYZ);
27850
+ if (seedIndex < 0 || seedIndex >= totalVoxels) {
27851
+ throw new Error("Seed index is out of bounds.");
27852
+ }
27853
+ const seedValue = mask[seedIndex];
27854
+ if (seedValue === 0) {
27855
+ throw new Error("Seed voxel is not part of a filled cluster.");
27856
+ }
27857
+ const img = mask.slice();
27858
+ for (let i = 0; i < totalVoxels; i++) {
27859
+ img[i] = img[i] === seedValue ? 1 : 0;
27860
+ }
27861
+ this.drawFloodFillCore(img, seedIndex, neighbors);
27862
+ const outputMask = mask.slice();
27863
+ const neighborOffsets = [];
27864
+ const offsets6 = [-nxy, nxy, -xDim, xDim, -1, 1];
27865
+ neighborOffsets.push(...offsets6);
27866
+ if (neighbors > 6) {
27867
+ neighborOffsets.push(
27868
+ -xDim - 1,
27869
+ -xDim + 1,
27870
+ xDim - 1,
27871
+ xDim + 1,
27872
+ -nxy - xDim,
27873
+ -nxy + xDim,
27874
+ -nxy - 1,
27875
+ -nxy + 1,
27876
+ nxy - xDim,
27877
+ nxy + xDim,
27878
+ nxy - 1,
27879
+ nxy + 1
27880
+ );
27881
+ }
27882
+ if (neighbors > 18) {
27883
+ neighborOffsets.push(
27884
+ -nxy - xDim - 1,
27885
+ -nxy - xDim + 1,
27886
+ -nxy + xDim - 1,
27887
+ -nxy + xDim + 1,
27888
+ nxy - xDim - 1,
27889
+ nxy - xDim + 1,
27890
+ nxy + xDim - 1,
27891
+ nxy + xDim + 1
27892
+ );
27893
+ }
27894
+ for (let idx = 0; idx < totalVoxels; idx++) {
27895
+ if (img[idx] === 2) {
27896
+ const x = idx % xDim;
27897
+ const y = Math.floor(idx % nxy / xDim);
27898
+ const z = Math.floor(idx / nxy);
27899
+ for (const offset of neighborOffsets) {
27900
+ const neighborIdx = idx + offset;
27901
+ if (neighborIdx < 0 || neighborIdx >= totalVoxels) {
27902
+ continue;
27903
+ }
27904
+ const nx2 = neighborIdx % xDim;
27905
+ const ny = Math.floor(neighborIdx % nxy / xDim);
27906
+ const nz = Math.floor(neighborIdx / nxy);
27907
+ if (Math.abs(nx2 - x) > 1 || Math.abs(ny - y) > 1 || Math.abs(nz - z) > 1) {
27908
+ continue;
27909
+ }
27910
+ if (mask[neighborIdx] === 0) {
27911
+ outputMask[neighborIdx] = seedValue;
27912
+ }
27913
+ }
27914
+ }
27915
+ }
27916
+ this.drawBitmap = outputMask;
27917
+ this.drawAddUndoBitmap();
27918
+ this.refreshDrawing(true);
27919
+ } catch (error) {
27920
+ log.error("Error in drawingBinaryDilationWithSeed:", error);
27921
+ }
27922
+ }
27731
27923
  // a voxel can be defined as having 6, 18 or 26 neighbors:
27732
27924
  // 6: neighbors share faces (distance=1)
27733
27925
  // 18: neighbors share faces (distance=1) or edges (1.4)
@@ -27809,7 +28001,7 @@ var Niivue = class _Niivue {
27809
28001
  }
27810
28002
  // not included in public docs
27811
28003
  // set all connected voxels in drawing to new color
27812
- drawFloodFill(seedXYZ, newColor = 0, growSelectedCluster = 0, forceMin = NaN, forceMax = NaN, neighbors = 6) {
28004
+ drawFloodFill(seedXYZ, newColor = 0, growSelectedCluster = 0, forceMin = NaN, forceMax = NaN, neighbors = 6, maxDistanceMM = Number.POSITIVE_INFINITY, is2D = false) {
27813
28005
  if (!this.drawBitmap) {
27814
28006
  throw new Error("drawBitmap undefined");
27815
28007
  }
@@ -27827,13 +28019,46 @@ var Niivue = class _Niivue {
27827
28019
  const nx = dims[0];
27828
28020
  const nxy = nx * dims[1];
27829
28021
  const nxyz = nxy * dims[2];
27830
- const img = this.drawBitmap.slice();
28022
+ let img = this.drawBitmap.slice();
28023
+ let drawBitmap = this.drawBitmap;
28024
+ if (this.clickToSegmentIsGrowing) {
28025
+ img = this.clickToSegmentGrowingBitmap.slice();
28026
+ drawBitmap = this.clickToSegmentGrowingBitmap;
28027
+ }
27831
28028
  if (img.length !== nxy * dims[2]) {
27832
28029
  return;
27833
28030
  }
28031
+ let constrainXYZ = -1;
28032
+ if (is2D && this.drawPenAxCorSag === 0 /* AXIAL */) {
28033
+ constrainXYZ = 2;
28034
+ } else if (is2D && this.drawPenAxCorSag === 1 /* CORONAL */) {
28035
+ constrainXYZ = 1;
28036
+ } else if (is2D && this.drawPenAxCorSag === 2 /* SAGITTAL */) {
28037
+ constrainXYZ = 0;
28038
+ }
28039
+ function vx2xyz(vx) {
28040
+ const Z = Math.floor(vx / nxy);
28041
+ const Y = Math.floor((vx - Z * nxy) / nx);
28042
+ const X = Math.floor(vx % nx);
28043
+ return [X, Y, Z];
28044
+ }
27834
28045
  function xyz2vx(pt) {
27835
28046
  return pt[0] + pt[1] * nx + pt[2] * nxy;
27836
28047
  }
28048
+ const vx2mm = (xyz) => {
28049
+ return this.vox2mm(xyz, this.back.matRAS);
28050
+ };
28051
+ const seedMM = vx2mm(seedXYZ);
28052
+ const maxDistanceMM2 = maxDistanceMM ** 2;
28053
+ function isWithinDistance(vx) {
28054
+ const xzyVox = vx2xyz(vx);
28055
+ if (constrainXYZ >= 0 && xzyVox[constrainXYZ] !== seedXYZ[constrainXYZ]) {
28056
+ return false;
28057
+ }
28058
+ const xyzMM = vx2mm(xzyVox);
28059
+ const dist2 = (xyzMM[0] - seedMM[0]) ** 2 + (xyzMM[1] - seedMM[1]) ** 2 + (xyzMM[2] - seedMM[2]) ** 2;
28060
+ return dist2 <= maxDistanceMM2;
28061
+ }
27837
28062
  const seedVx = xyz2vx(seedXYZ);
27838
28063
  const seedColor = img[seedVx];
27839
28064
  if (seedColor === newColor) {
@@ -27846,7 +28071,10 @@ var Niivue = class _Niivue {
27846
28071
  }
27847
28072
  for (let i = 1; i < nxyz; i++) {
27848
28073
  img[i] = 0;
27849
- if (this.drawBitmap[i] === seedColor) {
28074
+ if (drawBitmap[i] === seedColor) {
28075
+ if (!isWithinDistance(i)) {
28076
+ continue;
28077
+ }
27850
28078
  img[i] = 1;
27851
28079
  }
27852
28080
  }
@@ -27876,6 +28104,9 @@ var Niivue = class _Niivue {
27876
28104
  for (let i = 1; i < nxyz; i++) {
27877
28105
  img[i] = 0;
27878
28106
  if (backImg[i] >= mn && backImg[i] <= mx) {
28107
+ if (!isWithinDistance(i)) {
28108
+ continue;
28109
+ }
27879
28110
  img[i] = 1;
27880
28111
  }
27881
28112
  }
@@ -27884,11 +28115,19 @@ var Niivue = class _Niivue {
27884
28115
  }
27885
28116
  for (let i = 1; i < nxyz; i++) {
27886
28117
  if (img[i] === 2) {
27887
- this.drawBitmap[i] = newColor;
28118
+ drawBitmap[i] = newColor;
27888
28119
  }
27889
28120
  }
27890
- this.drawAddUndoBitmap();
27891
- this.refreshDrawing(false);
28121
+ if (!this.clickToSegmentIsGrowing) {
28122
+ this.drawBitmap = drawBitmap.slice();
28123
+ if (!this.opts.clickToSegment) {
28124
+ this.drawAddUndoBitmap();
28125
+ }
28126
+ this.refreshDrawing(true, this.clickToSegmentIsGrowing);
28127
+ } else {
28128
+ this.clickToSegmentGrowingBitmap = drawBitmap;
28129
+ this.refreshDrawing(true, this.clickToSegmentIsGrowing);
28130
+ }
27892
28131
  }
27893
28132
  // not included in public docs
27894
28133
  // given series of line segments, connect first and last
@@ -28048,6 +28287,7 @@ var Niivue = class _Niivue {
28048
28287
  this.drawClearAllUndoBitmaps();
28049
28288
  this.rgbaTex(this.drawTexture, TEXTURE7_DRAW, [2, 2, 2, 2], true);
28050
28289
  this.drawBitmap = null;
28290
+ this.clickToSegmentGrowingBitmap = null;
28051
28291
  this.drawScene();
28052
28292
  }
28053
28293
  /**
@@ -28056,7 +28296,7 @@ var Niivue = class _Niivue {
28056
28296
  * @example niivue.refreshDrawing();
28057
28297
  * @see {@link https://niivue.github.io/niivue/features/cactus.html | live demo usage}
28058
28298
  */
28059
- refreshDrawing(isForceRedraw = true) {
28299
+ refreshDrawing(isForceRedraw = true, useClickToSegmentBitmap = false) {
28060
28300
  if (!this.back?.dims) {
28061
28301
  throw new Error("back.dims undefined");
28062
28302
  }
@@ -28085,7 +28325,7 @@ var Niivue = class _Niivue {
28085
28325
  dims[3],
28086
28326
  this.gl.RED,
28087
28327
  this.gl.UNSIGNED_BYTE,
28088
- this.drawBitmap
28328
+ useClickToSegmentBitmap ? this.clickToSegmentGrowingBitmap : this.drawBitmap
28089
28329
  );
28090
28330
  if (isForceRedraw) {
28091
28331
  this.drawScene();
@@ -28497,6 +28737,8 @@ var Niivue = class _Niivue {
28497
28737
  gl.bindVertexArray(this.unusedVAO);
28498
28738
  this.rectShader = new Shader(gl, vertRectShader, fragRectShader);
28499
28739
  this.rectShader.use(gl);
28740
+ this.rectOutlineShader = new Shader(gl, vertRectShader, fragRectOutlineShader);
28741
+ this.rectOutlineShader.use(gl);
28500
28742
  this.lineShader = new Shader(gl, vertLineShader, fragRectShader);
28501
28743
  this.lineShader.use(gl);
28502
28744
  this.line3DShader = new Shader(gl, vertLine3DShader, fragRectShader);
@@ -28649,14 +28891,37 @@ var Niivue = class _Niivue {
28649
28891
  }
28650
28892
  /**
28651
28893
  * basic statistics for selected voxel-based image
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);
28894
+ * @param options - an object containing the following properties:
28895
+ * - layer: selects image to describe
28896
+ * - masks: optional binary images to filter voxels
28897
+ * - drawingIsMask: a boolean indicating if the drawing is used as a mask
28898
+ * - roiIsMask: a boolean indicating if the ROI is used as a mask
28899
+ * - startVox: the starting voxel coordinates
28900
+ * - endVox: the ending voxel coordinates
28901
+ * @returns numeric values to describe image or regions of images
28902
+ * @example
28903
+ * niivue.getDescriptives({
28904
+ * layer: 0,
28905
+ * masks: [],
28906
+ * drawingIsMask: true, // drawingIsMask and roiIsMask are mutually exclusive
28907
+ * roiIsMask: false,
28908
+ * startVox: [10, 20, 30], // ignored if roiIsMask is false
28909
+ * endVox: [40, 50, 60] // ignored if roiIsMask is false
28910
+ * });
28656
28911
  * @see {@link https://niivue.github.io/niivue/features/draw2.html | live demo usage}
28657
28912
  */
28658
- getDescriptives(layer = 0, masks = [], drawingIsMask = false) {
28913
+ getDescriptives(options) {
28914
+ const {
28915
+ layer = 0,
28916
+ masks = [],
28917
+ drawingIsMask = false,
28918
+ roiIsMask = false,
28919
+ startVox = [0, 0, 0],
28920
+ endVox = [0, 0, 0]
28921
+ } = options;
28922
+ let area = null;
28659
28923
  const hdr = this.volumes[layer].hdr;
28924
+ const pixDimsRAS = this.volumes[layer].pixDimsRAS;
28660
28925
  let slope = hdr.scl_slope;
28661
28926
  if (isNaN(slope)) {
28662
28927
  slope = 1;
@@ -28694,6 +28959,65 @@ var Niivue = class _Niivue {
28694
28959
  mask[i] = 0;
28695
28960
  }
28696
28961
  }
28962
+ } else if (masks.length < 1 && roiIsMask) {
28963
+ mask.fill(0);
28964
+ console.log("startVox", startVox);
28965
+ console.log("endVox", endVox);
28966
+ let constantDim = -1;
28967
+ if (startVox[0] === endVox[0]) {
28968
+ constantDim = 0;
28969
+ } else if (startVox[1] === endVox[1]) {
28970
+ constantDim = 1;
28971
+ } else if (startVox[2] === endVox[2]) {
28972
+ constantDim = 2;
28973
+ } else {
28974
+ console.error("Error: No constant dimension found.");
28975
+ return;
28976
+ }
28977
+ const dims = [0, 1, 2];
28978
+ const varDims = dims.filter((dim) => dim !== constantDim);
28979
+ const centerVox = [];
28980
+ centerVox[constantDim] = startVox[constantDim];
28981
+ centerVox[varDims[0]] = (startVox[varDims[0]] + endVox[varDims[0]]) / 2;
28982
+ centerVox[varDims[1]] = (startVox[varDims[1]] + endVox[varDims[1]]) / 2;
28983
+ const radiusX = Math.abs(endVox[varDims[0]] - startVox[varDims[0]]) / 2;
28984
+ const radiusY = Math.abs(endVox[varDims[1]] - startVox[varDims[1]]) / 2;
28985
+ const xdim = hdr.dims[1];
28986
+ const ydim = hdr.dims[2];
28987
+ const minVarDim0 = Math.max(0, Math.floor(centerVox[varDims[0]] - radiusX));
28988
+ const maxVarDim0 = Math.min(hdr.dims[varDims[0] + 1] - 1, Math.ceil(centerVox[varDims[0]] + radiusX));
28989
+ const minVarDim1 = Math.max(0, Math.floor(centerVox[varDims[1]] - radiusY));
28990
+ const maxVarDim1 = Math.min(hdr.dims[varDims[1] + 1] - 1, Math.ceil(centerVox[varDims[1]] + radiusY));
28991
+ const constDimVal = centerVox[constantDim];
28992
+ if (constDimVal < 0 || constDimVal >= hdr.dims[constantDim + 1]) {
28993
+ console.error("Error: Constant dimension value is out of bounds.");
28994
+ return;
28995
+ }
28996
+ for (let i = minVarDim0; i <= maxVarDim0; i++) {
28997
+ for (let j = minVarDim1; j <= maxVarDim1; j++) {
28998
+ const voxel = [];
28999
+ voxel[constantDim] = constDimVal;
29000
+ voxel[varDims[0]] = i;
29001
+ voxel[varDims[1]] = j;
29002
+ const di = (voxel[varDims[0]] - centerVox[varDims[0]]) / radiusX;
29003
+ const dj = (voxel[varDims[1]] - centerVox[varDims[1]]) / radiusY;
29004
+ const distSq = di * di + dj * dj;
29005
+ if (distSq <= 1) {
29006
+ const x = voxel[0];
29007
+ const y = voxel[1];
29008
+ const z = voxel[2];
29009
+ const index = z * xdim * ydim + y * xdim + x;
29010
+ mask[index] = 1;
29011
+ }
29012
+ }
29013
+ }
29014
+ const voxelArea = pixDimsRAS[varDims[0] + 1] * pixDimsRAS[varDims[1] + 1];
29015
+ const numMaskedVoxels = mask.reduce((count, value) => count + (value === 1 ? 1 : 0), 0);
29016
+ area = numMaskedVoxels * voxelArea;
29017
+ const radiusX_mm = radiusX * pixDimsRAS[varDims[0] + 1];
29018
+ const radiusY_mm = radiusY * pixDimsRAS[varDims[1] + 1];
29019
+ const areaEllipse = Math.PI * radiusX_mm * radiusY_mm;
29020
+ area = areaEllipse;
28697
29021
  }
28698
29022
  let k = 0;
28699
29023
  let M = 0;
@@ -28747,7 +29071,8 @@ var Niivue = class _Niivue {
28747
29071
  cal_min: this.volumes[layer].cal_min,
28748
29072
  cal_max: this.volumes[layer].cal_max,
28749
29073
  robust_min: this.volumes[layer].robust_min,
28750
- robust_max: this.volumes[layer].robust_max
29074
+ robust_max: this.volumes[layer].robust_max,
29075
+ area
28751
29076
  };
28752
29077
  }
28753
29078
  // not included in public docs
@@ -30225,6 +30550,29 @@ var Niivue = class _Niivue {
30225
30550
  const pos = [(x - this.graph.LTWH[0]) / this.graph.LTWH[2], (y - this.graph.LTWH[1]) / this.graph.LTWH[3]];
30226
30551
  return pos[0] > 0 && pos[1] > 0 && pos[0] <= 1 && pos[1] <= 1;
30227
30552
  }
30553
+ // update drawBitmap if it differs from clickTosegmentBitmap
30554
+ updateBitmapFromClickToSegment() {
30555
+ if (this.clickToSegmentGrowingBitmap === null) {
30556
+ return;
30557
+ }
30558
+ if (this.drawBitmap === null) {
30559
+ return;
30560
+ }
30561
+ if (this.clickToSegmentGrowingBitmap.length !== this.drawBitmap.length) {
30562
+ return;
30563
+ }
30564
+ const nvx = this.drawBitmap.length;
30565
+ for (let i = 0; i < nvx; i++) {
30566
+ this.drawBitmap[i] = this.clickToSegmentGrowingBitmap[i];
30567
+ }
30568
+ }
30569
+ sumBitmap(img) {
30570
+ let sum = 0;
30571
+ for (let i = 0; i < img.length; i++) {
30572
+ sum += img[i];
30573
+ }
30574
+ return sum;
30575
+ }
30228
30576
  // not included in public docs
30229
30577
  // handle mouse click event on canvas
30230
30578
  mouseClick(x, y, posChange = 0, isDelta = true) {
@@ -30274,7 +30622,7 @@ var Niivue = class _Niivue {
30274
30622
  if (axCorSag > 2 /* SAGITTAL */) {
30275
30623
  continue;
30276
30624
  }
30277
- const texFrac = this.screenXY2TextureFrac(x, y, i, false);
30625
+ let texFrac = this.screenXY2TextureFrac(x, y, i, false);
30278
30626
  if (texFrac[0] < 0) {
30279
30627
  continue;
30280
30628
  }
@@ -30301,31 +30649,75 @@ var Niivue = class _Niivue {
30301
30649
  this.scene.crosshairPos = vec311.clone(texFrac);
30302
30650
  }
30303
30651
  if (this.opts.drawingEnabled) {
30304
- const pt = this.frac2vox(this.scene.crosshairPos);
30305
- const ptMM = this.frac2mm(this.scene.crosshairPos);
30652
+ let pt = this.frac2vox(this.scene.crosshairPos);
30306
30653
  if (this.opts.clickToSegment) {
30307
- const radius = this.opts.clickToSegmentRadius;
30308
- const steps = this.opts.clickToSegmentSteps;
30654
+ texFrac = this.screenXY2TextureFrac(this.clickToSegmentXY[0], this.clickToSegmentXY[1], i, false);
30655
+ pt = this.frac2vox(texFrac);
30656
+ let diff = 0;
30657
+ let threshold = this.opts.clickToSegmentPercent + diff;
30658
+ if (this.uiData.mousedown) {
30659
+ const xDiff = (this.clickToSegmentXY[0] - x) / this.gl.canvas.width;
30660
+ const yDiff = (this.clickToSegmentXY[1] - y) / this.gl.canvas.height;
30661
+ diff = Math.max(Math.abs(xDiff), Math.abs(yDiff));
30662
+ threshold = this.opts.clickToSegmentPercent + diff;
30663
+ }
30664
+ let voxelIntensity = this.back.getValue(pt[0], pt[1], pt[2]);
30665
+ if (this.opts.clickToSegmentAutoIntensity) {
30666
+ if (threshold !== 0) {
30667
+ if (voxelIntensity === 0) {
30668
+ voxelIntensity = 0.01;
30669
+ }
30670
+ this.opts.clickToSegmentIntensityMax = voxelIntensity * (1 + threshold);
30671
+ this.opts.clickToSegmentIntensityMin = voxelIntensity * (1 - threshold);
30672
+ }
30673
+ if (voxelIntensity > (this.back.cal_min + this.back.cal_max) * 0.5) {
30674
+ this.opts.clickToSegmentBright = true;
30675
+ } else {
30676
+ this.opts.clickToSegmentBright = false;
30677
+ }
30678
+ }
30309
30679
  const brightOrDark = this.opts.clickToSegmentBright ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
30310
- this.drawPenFillPts = [];
30311
30680
  this.drawPenAxCorSag = axCorSag;
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
- }
30681
+ if (this.drawPenAxCorSag === 0 /* AXIAL */) {
30682
+ const pixDims = [this.back.pixDimsRAS[0], this.back.pixDimsRAS[1]];
30683
+ const minPixDim = Math.min(...pixDims);
30684
+ this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim);
30685
+ } else if (this.drawPenAxCorSag === 1 /* CORONAL */) {
30686
+ const pixDims = [this.back.pixDimsRAS[0], this.back.pixDimsRAS[2]];
30687
+ const minPixDim = Math.min(...pixDims);
30688
+ this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim);
30689
+ } else if (this.drawPenAxCorSag === 2 /* SAGITTAL */) {
30690
+ const pixDims = [this.back.pixDimsRAS[1], this.back.pixDimsRAS[2]];
30691
+ const minPixDim = Math.min(...pixDims);
30692
+ this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim);
30693
+ }
30694
+ this.drawPt(pt[0], pt[1], pt[2], this.opts.penValue);
30695
+ if (diff !== 0) {
30696
+ this.clickToSegmentIsGrowing = true;
30697
+ this.clickToSegmentGrowingBitmap = this.drawBitmap.slice();
30698
+ } else {
30699
+ this.clickToSegmentIsGrowing = false;
30325
30700
  }
30701
+ this.drawFloodFill(
30702
+ [pt[0], pt[1], pt[2]],
30703
+ 0,
30704
+ brightOrDark,
30705
+ this.opts.clickToSegmentIntensityMin,
30706
+ this.opts.clickToSegmentIntensityMax,
30707
+ this.opts.floodFillNeighbors,
30708
+ this.opts.clickToSegmentMaxDistanceMM,
30709
+ this.opts.clickToSegmentIs2D
30710
+ );
30326
30711
  this.drawScene();
30327
30712
  this.createOnLocationChange(axCorSag);
30328
- const info = this.getDescriptives(0, [], true);
30713
+ if (this.clickToSegmentIsGrowing) {
30714
+ return;
30715
+ }
30716
+ const info = this.getDescriptives({
30717
+ layer: 0,
30718
+ masks: [],
30719
+ drawingIsMask: true
30720
+ });
30329
30721
  this.onClickToSegment({ mL: info.volumeML, mm3: info.volumeMM3 });
30330
30722
  return;
30331
30723
  }
@@ -30543,20 +30935,42 @@ var Niivue = class _Niivue {
30543
30935
  if (!this.rectShader) {
30544
30936
  throw new Error("rectShader undefined");
30545
30937
  }
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);
30938
+ if (!this.opts.selectionBoxIsOutline) {
30939
+ this.rectShader.use(this.gl);
30940
+ this.gl.enable(this.gl.BLEND);
30941
+ this.gl.uniform4fv(this.rectShader.uniforms.lineColor, lineColor);
30942
+ this.gl.uniform2fv(this.rectShader.uniforms.canvasWidthHeight, [this.gl.canvas.width, this.gl.canvas.height]);
30943
+ this.gl.uniform4f(
30944
+ this.rectShader.uniforms.leftTopWidthHeight,
30945
+ leftTopWidthHeight[0],
30946
+ leftTopWidthHeight[1],
30947
+ leftTopWidthHeight[2],
30948
+ leftTopWidthHeight[3]
30949
+ );
30950
+ this.gl.bindVertexArray(this.genericVAO);
30951
+ this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
30952
+ this.gl.bindVertexArray(this.unusedVAO);
30953
+ } else {
30954
+ this.drawCircle(leftTopWidthHeight, lineColor, 0.1);
30955
+ this.rectOutlineShader.use(this.gl);
30956
+ this.gl.enable(this.gl.BLEND);
30957
+ this.gl.uniform1f(this.rectOutlineShader.uniforms.thickness, this.opts.selectionBoxLineThickness);
30958
+ this.gl.uniform4fv(this.rectOutlineShader.uniforms.lineColor, lineColor);
30959
+ this.gl.uniform2fv(this.rectOutlineShader.uniforms.canvasWidthHeight, [
30960
+ this.gl.canvas.width,
30961
+ this.gl.canvas.height
30962
+ ]);
30963
+ this.gl.uniform4f(
30964
+ this.rectOutlineShader.uniforms.leftTopWidthHeight,
30965
+ leftTopWidthHeight[0],
30966
+ leftTopWidthHeight[1],
30967
+ leftTopWidthHeight[2],
30968
+ leftTopWidthHeight[3]
30969
+ );
30970
+ this.gl.bindVertexArray(this.genericVAO);
30971
+ this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
30972
+ this.gl.bindVertexArray(this.unusedVAO);
30973
+ }
30560
30974
  }
30561
30975
  drawCircle(leftTopWidthHeight, circleColor = this.opts.fontColor, fillPercent = 1) {
30562
30976
  if (!this.circleShader) {
@@ -30582,6 +30996,10 @@ var Niivue = class _Niivue {
30582
30996
  // not included in public docs
30583
30997
  // draw a rectangle at desired location
30584
30998
  drawSelectionBox(leftTopWidthHeight) {
30999
+ if (this.opts.dragMode === 6 /* roiSelection */) {
31000
+ this.drawCircle(leftTopWidthHeight, this.opts.selectionBoxColor, 0.1);
31001
+ return;
31002
+ }
30585
31003
  this.drawRect(leftTopWidthHeight, this.opts.selectionBoxColor);
30586
31004
  }
30587
31005
  // not included in public docs
@@ -33258,10 +33676,14 @@ var Niivue = class _Niivue {
33258
33676
  width,
33259
33677
  height
33260
33678
  ]);
33679
+ return;
33261
33680
  }
33262
33681
  if (this.opts.clickToSegment) {
33263
33682
  const x = this.mousePos[0];
33264
33683
  const y = this.mousePos[1];
33684
+ if (this.inRenderTile(x, y) >= 0) {
33685
+ return;
33686
+ }
33265
33687
  const tileIdx = this.tileIndex(x, y);
33266
33688
  if (tileIdx > -1) {
33267
33689
  const fovMM = this.screenSlices[tileIdx].fovMM;