@niivue/niivue 0.45.1 → 0.46.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/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.46.0";
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;
@@ -20942,11 +20982,13 @@ var NVImage = class _NVImage {
20942
20982
  this.onOpacityChange(this);
20943
20983
  }
20944
20984
  }
20945
- // not included in public docs
20946
- // given an overlayItem and its img TypedArray, calculate 2% and 98% display range if needed
20947
- // clone FSL robust_range estimates https://github.com/rordenlab/niimath/blob/331758459140db59290a794350d0ff3ad4c37b67/src/core32.c#L1215
20948
- // ToDo: convert to web assembly, this is slow in JavaScript
20949
- calMinMax() {
20985
+ /**
20986
+ * set contrast/brightness to robust range (2%..98%)
20987
+ * @param vol - volume for estimate (use -1 to use estimate on all loaded volumes; use INFINITY for current volume)
20988
+ * @sets volume brightness and returns array [pct2, pct98, mnScale, mxScale]
20989
+ * @see {@link https://niivue.github.io/niivue/features/timeseries2.html | live demo usage}
20990
+ */
20991
+ calMinMax(vol = Number.POSITIVE_INFINITY) {
20950
20992
  if (!this.hdr) {
20951
20993
  throw new Error("hdr undefined");
20952
20994
  }
@@ -20957,10 +20999,20 @@ var NVImage = class _NVImage {
20957
20999
  let mx = Number.NEGATIVE_INFINITY;
20958
21000
  let nZero = 0;
20959
21001
  let nNan = 0;
20960
- const nVox = this.img.length;
21002
+ let voxEnd = this.img.length;
21003
+ let voxStart = 0;
21004
+ const nVox3D = this.hdr.dims[1] * this.hdr.dims[2] * this.hdr.dims[3];
21005
+ const nVol = Math.floor(voxEnd / nVox3D);
21006
+ if (vol >= nVol) {
21007
+ vol = this.frame4D;
21008
+ }
21009
+ if (vol >= 0 && vol < nVol) {
21010
+ voxStart = vol * nVox3D;
21011
+ voxEnd = voxStart + nVox3D;
21012
+ }
20961
21013
  const isFastCalc = this.img.constructor !== Float64Array && this.img.constructor !== Float32Array && this.ignoreZeroVoxels;
20962
21014
  if (isFastCalc) {
20963
- for (let i = 0; i < nVox; i++) {
21015
+ for (let i = voxStart; i < voxEnd; i++) {
20964
21016
  mn = Math.min(this.img[i], mn);
20965
21017
  mx = Math.max(this.img[i], mx);
20966
21018
  if (this.img[i] === 0) {
@@ -20968,7 +21020,7 @@ var NVImage = class _NVImage {
20968
21020
  }
20969
21021
  }
20970
21022
  } else {
20971
- for (let i = 0; i < nVox; i++) {
21023
+ for (let i = voxStart; i < voxEnd; i++) {
20972
21024
  if (isNaN(this.img[i])) {
20973
21025
  nNan++;
20974
21026
  continue;
@@ -21013,7 +21065,7 @@ var NVImage = class _NVImage {
21013
21065
  this.robust_max = this.cal_max;
21014
21066
  return [cmMin, cmMax, cmMin, cmMax];
21015
21067
  }
21016
- const percentZero = 100 * nZero / nVox;
21068
+ const percentZero = 100 * nZero / (voxEnd - voxStart);
21017
21069
  let isOverrideIgnoreZeroVoxels = false;
21018
21070
  if (percentZero > 60 && !this.ignoreZeroVoxels) {
21019
21071
  log.warn(`${Math.round(percentZero)}% of voxels are zero: ignoring zeros for cal_max`);
@@ -21024,7 +21076,7 @@ var NVImage = class _NVImage {
21024
21076
  nZero = 0;
21025
21077
  }
21026
21078
  nZero += nNan;
21027
- const n2pct = Math.round((nVox - nZero) * this.percentileFrac);
21079
+ const n2pct = Math.round((voxEnd - voxStart - nZero) * this.percentileFrac);
21028
21080
  if (n2pct < 1 || mn === mx) {
21029
21081
  log.debug("no variability in image intensity?");
21030
21082
  this.cal_min = mnScale;
@@ -21042,11 +21094,11 @@ var NVImage = class _NVImage {
21042
21094
  hist[i] = 0;
21043
21095
  }
21044
21096
  if (isFastCalc) {
21045
- for (let i = 0; i < nVox; i++) {
21097
+ for (let i = voxStart; i < voxEnd; i++) {
21046
21098
  hist[Math.round((this.img[i] - mn) * scl)]++;
21047
21099
  }
21048
21100
  } else if (this.ignoreZeroVoxels) {
21049
- for (let i = 0; i < nVox; i++) {
21101
+ for (let i = voxStart; i < voxEnd; i++) {
21050
21102
  if (this.img[i] === 0) {
21051
21103
  continue;
21052
21104
  }
@@ -21056,7 +21108,7 @@ var NVImage = class _NVImage {
21056
21108
  hist[Math.round((this.img[i] - mn) * scl)]++;
21057
21109
  }
21058
21110
  } else {
21059
- for (let i = 0; i < nVox; i++) {
21111
+ for (let i = voxStart; i < voxEnd; i++) {
21060
21112
  if (isNaN(this.img[i])) {
21061
21113
  continue;
21062
21114
  }
@@ -22250,6 +22302,7 @@ var DRAG_MODE = /* @__PURE__ */ ((DRAG_MODE2) => {
22250
22302
  DRAG_MODE2[DRAG_MODE2["pan"] = 3] = "pan";
22251
22303
  DRAG_MODE2[DRAG_MODE2["slicer3D"] = 4] = "slicer3D";
22252
22304
  DRAG_MODE2[DRAG_MODE2["callbackOnly"] = 5] = "callbackOnly";
22305
+ DRAG_MODE2[DRAG_MODE2["roiSelection"] = 6] = "roiSelection";
22253
22306
  return DRAG_MODE2;
22254
22307
  })(DRAG_MODE || {});
22255
22308
  var DEFAULT_OPTIONS = {
@@ -22319,20 +22372,40 @@ var DEFAULT_OPTIONS = {
22319
22372
  renderOverlayBlend: 1,
22320
22373
  sliceMosaicString: "",
22321
22374
  centerMosaic: false,
22375
+ penSize: 1,
22376
+ // in voxels, since all drawing is done using bitmap indices
22322
22377
  clickToSegment: false,
22323
- clickToSegmentRadius: 2,
22324
- clickToSegmentSteps: 10,
22325
- clickToSegmentBright: true
22378
+ clickToSegmentRadius: 3,
22379
+ // in mm
22380
+ clickToSegmentBright: true,
22381
+ clickToSegmentAutoIntensity: false,
22382
+ // new option, but keep clickToSegmentBright for backwards compatibility
22383
+ clickToSegmentIntensityMax: NaN,
22384
+ // NaN will use auto threshold (default flood fill behavior from before)
22385
+ clickToSegmentIntensityMin: NaN,
22386
+ // NaN will use auto threshold (default flood fill behavior from before)
22387
+ // 0 will use auto threshold (default flood fill behavior from before)
22388
+ // Take the voxel intensity at the click point and use this percentage +/- to threshold the flood fill operation.
22389
+ // If greater than 0, clickedVoxelIntensity +/- clickedVoxelIntensity * clickToSegmentPercent will be used
22390
+ // for the clickToSegmentIntensityMin and clickToSegmentIntensityMax values.
22391
+ clickToSegmentPercent: 0,
22392
+ clickToSegmentMaxDistanceMM: Number.POSITIVE_INFINITY,
22393
+ // default value is infinity for backwards compatibility with flood fill routine.
22394
+ clickToSegmentIs2D: false,
22395
+ selectionBoxLineThickness: 4,
22396
+ selectionBoxIsOutline: false,
22397
+ scrollRequiresFocus: false
22398
+ // determines if the cavas need to be focused to scroll
22326
22399
  };
22327
22400
  var INITIAL_SCENE_DATA = {
22328
22401
  gamma: 1,
22329
22402
  azimuth: 110,
22330
22403
  elevation: 10,
22331
- crosshairPos: vec38.create(),
22404
+ crosshairPos: vec38.fromValues(0.5, 0.5, 0.5),
22332
22405
  clipPlane: [0, 0, 0, 0],
22333
22406
  clipPlaneDepthAziElev: [2, 0, 0],
22334
22407
  volScaleMultiplier: 1,
22335
- pan2Dxyzmm: vec46.create(),
22408
+ pan2Dxyzmm: vec46.fromValues(0, 0, 0, 1),
22336
22409
  clipThick: 2,
22337
22410
  clipVolumeLow: [0, 0, 0],
22338
22411
  clipVolumeHigh: [1, 1, 1]
@@ -24019,7 +24092,7 @@ var defaultSaveImageOptions = {
24019
24092
  isSaveDrawing: false,
24020
24093
  volumeByIndex: 0
24021
24094
  };
24022
- var Niivue = class _Niivue {
24095
+ var Niivue = class {
24023
24096
  /**
24024
24097
  * @param options - options object to set modifiable Niivue properties
24025
24098
  */
@@ -24050,6 +24123,12 @@ var Niivue = class _Niivue {
24050
24123
  // the color lookup table for drawing
24051
24124
  __publicField(this, "drawOpacity", 0.8);
24052
24125
  // opacity of drawing (default)
24126
+ __publicField(this, "clickToSegmentIsGrowing", false);
24127
+ // flag to indicate if the clickToSegment flood fill growing is in progress with left mouse down + drag
24128
+ __publicField(this, "clickToSegmentGrowingBitmap", null);
24129
+ // the bitmap of the growing flood fill
24130
+ __publicField(this, "clickToSegmentXY", [0, 0]);
24131
+ // the x,y location of the clickToSegment flood fill
24053
24132
  __publicField(this, "renderDrawAmbientOcclusion", 0.4);
24054
24133
  __publicField(this, "colorbarHeight", 0);
24055
24134
  // height in pixels, set when colorbar is drawn
@@ -24067,6 +24146,7 @@ var Niivue = class _Niivue {
24067
24146
  __publicField(this, "orientCubeShader");
24068
24147
  __publicField(this, "orientCubeShaderVAO", null);
24069
24148
  __publicField(this, "rectShader");
24149
+ __publicField(this, "rectOutlineShader");
24070
24150
  __publicField(this, "renderShader");
24071
24151
  __publicField(this, "lineShader");
24072
24152
  __publicField(this, "line3DShader");
@@ -24119,7 +24199,20 @@ var Niivue = class _Niivue {
24119
24199
  __publicField(this, "extentsMax");
24120
24200
  // ResizeObserver
24121
24201
  __publicField(this, "resizeObserver", null);
24122
- __publicField(this, "syncOpts", {});
24202
+ // syncOpts: Record<string, unknown> = {}
24203
+ __publicField(this, "syncOpts", {
24204
+ "3d": false,
24205
+ // legacy option
24206
+ "2d": false,
24207
+ // legacy option
24208
+ zoomPan: false,
24209
+ cal_min: false,
24210
+ cal_max: false,
24211
+ clipPlane: false,
24212
+ gamma: false,
24213
+ sliceType: false,
24214
+ crosshair: false
24215
+ });
24123
24216
  __publicField(this, "readyForSync", false);
24124
24217
  // UI Data
24125
24218
  __publicField(this, "uiData", {
@@ -24178,7 +24271,8 @@ var Niivue = class _Niivue {
24178
24271
  vols: [0],
24179
24272
  // e.g. timeline for background volume only, e.g. [0,2] for first and third volumes
24180
24273
  autoSizeMultiplanar: false,
24181
- normalizeValues: false
24274
+ normalizeValues: false,
24275
+ isRangeCalMinMax: false
24182
24276
  });
24183
24277
  __publicField(this, "meshShaders", [
24184
24278
  {
@@ -24578,8 +24672,16 @@ var Niivue = class _Niivue {
24578
24672
  this.canvas.style.display = "block";
24579
24673
  this.canvas.width = this.canvas.offsetWidth;
24580
24674
  this.canvas.height = this.canvas.offsetHeight;
24581
- window.addEventListener("resize", this.resizeListener.bind(this));
24582
- this.resizeObserver = new ResizeObserver(this.resizeListener.bind(this));
24675
+ window.addEventListener("resize", () => {
24676
+ requestAnimationFrame(() => {
24677
+ this.resizeListener();
24678
+ });
24679
+ });
24680
+ this.resizeObserver = new ResizeObserver(() => {
24681
+ requestAnimationFrame(() => {
24682
+ this.resizeListener();
24683
+ });
24684
+ });
24583
24685
  this.resizeObserver.observe(this.canvas.parentElement);
24584
24686
  }
24585
24687
  this.registerInteractions();
@@ -24598,6 +24700,9 @@ var Niivue = class _Niivue {
24598
24700
  * @see {@link https://niivue.github.io/niivue/features/sync.mesh.html | live demo usage}
24599
24701
  */
24600
24702
  syncWith(otherNV, syncOpts = { "2d": true, "3d": true }) {
24703
+ if (!(otherNV instanceof Array)) {
24704
+ otherNV = [otherNV];
24705
+ }
24601
24706
  this.otherNV = otherNV;
24602
24707
  this.syncOpts = { ...syncOpts };
24603
24708
  }
@@ -24613,9 +24718,55 @@ var Niivue = class _Niivue {
24613
24718
  * @see {@link https://niivue.github.io/niivue/features/sync.mesh.html | live demo usage}
24614
24719
  */
24615
24720
  broadcastTo(otherNV, syncOpts = { "2d": true, "3d": true }) {
24721
+ if (!(otherNV instanceof Array)) {
24722
+ otherNV = [otherNV];
24723
+ }
24616
24724
  this.otherNV = otherNV;
24617
24725
  this.syncOpts = syncOpts;
24618
24726
  }
24727
+ doSync3d(otherNV) {
24728
+ otherNV.scene.renderAzimuth = this.scene.renderAzimuth;
24729
+ otherNV.scene.renderElevation = this.scene.renderElevation;
24730
+ otherNV.scene.volScaleMultiplier = this.scene.volScaleMultiplier;
24731
+ }
24732
+ // both crosshair and zoomPan
24733
+ doSync2d(otherNV) {
24734
+ const thisMM = this.frac2mm(this.scene.crosshairPos);
24735
+ otherNV.scene.crosshairPos = otherNV.mm2frac(thisMM);
24736
+ otherNV.scene.pan2Dxyzmm = vec48.clone(this.scene.pan2Dxyzmm);
24737
+ }
24738
+ doSyncGamma(otherNV) {
24739
+ const thisGamma = this.scene.gamma;
24740
+ const otherGamma = otherNV.scene.gamma;
24741
+ if (thisGamma !== otherGamma) {
24742
+ otherNV.setGamma(thisGamma);
24743
+ }
24744
+ }
24745
+ doSyncZoomPan(otherNV) {
24746
+ otherNV.scene.pan2Dxyzmm = vec48.clone(this.scene.pan2Dxyzmm);
24747
+ }
24748
+ doSyncCrosshair(otherNV) {
24749
+ const thisMM = this.frac2mm(this.scene.crosshairPos);
24750
+ otherNV.scene.crosshairPos = otherNV.mm2frac(thisMM);
24751
+ }
24752
+ doSyncCalMin(otherNV) {
24753
+ if (this.volumes[0].cal_min !== otherNV.volumes[0].cal_min) {
24754
+ otherNV.volumes[0].cal_min = this.volumes[0].cal_min;
24755
+ otherNV.updateGLVolume();
24756
+ }
24757
+ }
24758
+ doSyncCalMax(otherNV) {
24759
+ if (this.volumes[0].cal_max !== otherNV.volumes[0].cal_max) {
24760
+ otherNV.volumes[0].cal_max = this.volumes[0].cal_max;
24761
+ otherNV.updateGLVolume();
24762
+ }
24763
+ }
24764
+ doSyncSliceType(otherNV) {
24765
+ otherNV.setSliceType(this.opts.sliceType);
24766
+ }
24767
+ doSyncClipPlane(otherNV) {
24768
+ otherNV.setClipPlane(this.scene.clipPlaneDepthAziElev);
24769
+ }
24619
24770
  /**
24620
24771
  * Sync the scene controls (orientation, crosshair location, etc.) from one Niivue instance to another. useful for using one canvas to drive another.
24621
24772
  * @internal
@@ -24632,46 +24783,39 @@ var Niivue = class _Niivue {
24632
24783
  if (!this.gl.canvas.matches(":focus")) {
24633
24784
  return;
24634
24785
  }
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);
24786
+ for (let i = 0; i < this.otherNV.length; i++) {
24787
+ if (this.otherNV[i] === this) {
24788
+ continue;
24789
+ }
24790
+ if (this.syncOpts.gamma) {
24791
+ this.doSyncGamma(this.otherNV[i]);
24792
+ }
24793
+ if (this.syncOpts.crosshair) {
24794
+ this.doSyncCrosshair(this.otherNV[i]);
24795
+ }
24796
+ if (this.syncOpts.zoomPan) {
24797
+ this.doSyncZoomPan(this.otherNV[i]);
24798
+ }
24799
+ if (this.syncOpts.sliceType) {
24800
+ this.doSyncSliceType(this.otherNV[i]);
24801
+ }
24802
+ if (this.syncOpts.cal_min) {
24803
+ this.doSyncCalMin(this.otherNV[i]);
24804
+ }
24805
+ if (this.syncOpts.cal_max) {
24806
+ this.doSyncCalMax(this.otherNV[i]);
24807
+ }
24808
+ if (this.syncOpts.clipPlane) {
24809
+ this.doSyncClipPlane(this.otherNV[i]);
24641
24810
  }
24642
24811
  if (this.syncOpts["2d"]) {
24643
- this.otherNV.scene.crosshairPos = this.otherNV.mm2frac(thisMM);
24644
- this.otherNV.scene.pan2Dxyzmm = vec48.clone(this.scene.pan2Dxyzmm);
24812
+ this.doSync2d(this.otherNV[i]);
24645
24813
  }
24646
24814
  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();
24815
+ this.doSync3d(this.otherNV[i]);
24674
24816
  }
24817
+ this.otherNV[i].drawScene();
24818
+ this.otherNV[i].createOnLocationChange();
24675
24819
  }
24676
24820
  }
24677
24821
  /** Not documented publicly for now
@@ -24766,6 +24910,8 @@ var Niivue = class _Niivue {
24766
24910
  this.drawPenLocation = [NaN, NaN, NaN];
24767
24911
  this.drawPenAxCorSag = -1;
24768
24912
  this.uiData.mousedown = true;
24913
+ this.setDragStart(0, 0);
24914
+ this.setDragEnd(0, 0);
24769
24915
  log.debug("mouse down");
24770
24916
  log.debug(e);
24771
24917
  const pos = this.getNoPaddingNoBorderCanvasRelativeMousePosition(e, this.gl.canvas);
@@ -24773,6 +24919,9 @@ var Niivue = class _Niivue {
24773
24919
  return;
24774
24920
  }
24775
24921
  const [x, y] = [pos.x * this.uiData.dpr, pos.y * this.uiData.dpr];
24922
+ if (this.opts.clickToSegment) {
24923
+ this.clickToSegmentXY = [x, y];
24924
+ }
24776
24925
  const label = this.getLabelAtPoint([x, y]);
24777
24926
  if (label) {
24778
24927
  if (label.onClick) {
@@ -24973,8 +25122,15 @@ var Niivue = class _Niivue {
24973
25122
  if (this.drawPenFillPts.length > 0) {
24974
25123
  this.drawPenFilled();
24975
25124
  } else if (this.drawPenAxCorSag >= 0) {
24976
- this.drawAddUndoBitmap();
25125
+ if (this.opts.clickToSegment) {
25126
+ const sumBitmap = this.sumBitmap(this.drawBitmap);
25127
+ const sumSeg = this.sumBitmap(this.clickToSegmentGrowingBitmap);
25128
+ if (sumSeg > sumBitmap) {
25129
+ this.updateBitmapFromClickToSegment();
25130
+ }
25131
+ }
24977
25132
  }
25133
+ this.drawAddUndoBitmap();
24978
25134
  this.drawPenLocation = [NaN, NaN, NaN];
24979
25135
  this.drawPenAxCorSag = -1;
24980
25136
  if (isFunction(this.onMouseUp)) {
@@ -24988,6 +25144,9 @@ var Niivue = class _Niivue {
24988
25144
  const fracStart = this.canvasPos2frac([this.uiData.dragStart[0], this.uiData.dragStart[1]]);
24989
25145
  const fracEnd = this.canvasPos2frac([this.uiData.dragEnd[0], this.uiData.dragEnd[1]]);
24990
25146
  this.generateMouseUpCallback(fracStart, fracEnd);
25147
+ if (this.opts.dragMode === 6 /* roiSelection */) {
25148
+ return;
25149
+ }
24991
25150
  if (this.opts.dragMode !== 1 /* contrast */) {
24992
25151
  return;
24993
25152
  }
@@ -25290,6 +25449,35 @@ var Niivue = class _Niivue {
25290
25449
  if (this.thumbnailVisible) {
25291
25450
  return;
25292
25451
  }
25452
+ const dragStartSum = this.uiData.dragStart.reduce((a, b) => a + b, 0);
25453
+ const dragEndSum = this.uiData.dragEnd.reduce((a, b) => a + b, 0);
25454
+ const validDrag = dragStartSum > 0 && dragEndSum > 0;
25455
+ if (this.opts.dragMode === 6 /* roiSelection */ && validDrag) {
25456
+ const delta = e.deltaY > 0 ? 1 : -1;
25457
+ if (this.uiData.dragStart[0] < this.uiData.dragEnd[0]) {
25458
+ this.uiData.dragStart[0] -= delta;
25459
+ this.uiData.dragEnd[0] += delta;
25460
+ } else {
25461
+ this.uiData.dragStart[0] += delta;
25462
+ this.uiData.dragEnd[0] -= delta;
25463
+ }
25464
+ if (this.uiData.dragStart[1] < this.uiData.dragEnd[1]) {
25465
+ this.uiData.dragStart[1] -= delta;
25466
+ this.uiData.dragEnd[1] += delta;
25467
+ } else {
25468
+ this.uiData.dragStart[1] += delta;
25469
+ this.uiData.dragEnd[1] -= delta;
25470
+ }
25471
+ this.uiData.isDragging = true;
25472
+ this.drawScene();
25473
+ this.uiData.isDragging = false;
25474
+ const tileIdx = this.tileIndex(this.uiData.dragStart[0], this.uiData.dragStart[1]);
25475
+ this.generateMouseUpCallback(
25476
+ this.screenXY2TextureFrac(this.uiData.dragStart[0], this.uiData.dragStart[1], tileIdx),
25477
+ this.screenXY2TextureFrac(this.uiData.dragEnd[0], this.uiData.dragEnd[1], tileIdx)
25478
+ );
25479
+ return;
25480
+ }
25293
25481
  const rect = this.canvas.getBoundingClientRect();
25294
25482
  if (e.deltaY < 0) {
25295
25483
  this.sliceScroll2D(-0.01, e.clientX - rect.left, e.clientY - rect.top);
@@ -26783,6 +26971,10 @@ var Niivue = class _Niivue {
26783
26971
  }
26784
26972
  // not included in public docs
26785
26973
  sliceScroll2D(posChange, x, y, isDelta = true) {
26974
+ if (this.opts.scrollRequiresFocus && this.canvas !== document.activeElement) {
26975
+ log.warn("Canvas element does not have focus. Scroll events will not be processed.");
26976
+ return;
26977
+ }
26786
26978
  if (this.inGraphTile(x, y)) {
26787
26979
  let vol = this.volumes[0].frame4D;
26788
26980
  if (posChange > 0) {
@@ -27474,6 +27666,7 @@ var Niivue = class _Niivue {
27474
27666
  }
27475
27667
  const vx = this.back.dims[1] * this.back.dims[2] * this.back.dims[3];
27476
27668
  this.drawBitmap = new Uint8Array(vx);
27669
+ this.clickToSegmentGrowingBitmap = new Uint8Array(vx);
27477
27670
  this.drawClearAllUndoBitmaps();
27478
27671
  this.drawAddUndoBitmap();
27479
27672
  this.drawTexture = this.r8Tex(this.drawTexture, TEXTURE7_DRAW, this.back.dims, true);
@@ -27647,6 +27840,23 @@ var Niivue = class _Niivue {
27647
27840
  y = Math.min(Math.max(y, 0), dy - 1);
27648
27841
  z = Math.min(Math.max(z, 0), dz - 1);
27649
27842
  this.drawBitmap[x + y * dx + z * dx * dy] = penValue;
27843
+ const isAx = this.drawPenAxCorSag === 0;
27844
+ const isCor = this.drawPenAxCorSag === 1;
27845
+ const isSag = this.drawPenAxCorSag === 2;
27846
+ if (this.opts.penSize > 1) {
27847
+ const halfPenSize = Math.floor(this.opts.penSize / 2);
27848
+ for (let i = -halfPenSize; i <= halfPenSize; i++) {
27849
+ for (let j = -halfPenSize; j <= halfPenSize; j++) {
27850
+ if (isAx) {
27851
+ this.drawBitmap[x + i + (y + j) * dx + z * dx * dy] = penValue;
27852
+ } else if (isCor) {
27853
+ this.drawBitmap[x + i + y * dx + (z + j) * dx * dy] = penValue;
27854
+ } else if (isSag) {
27855
+ this.drawBitmap[x + (y + j) * dx + (z + i) * dx * dy] = penValue;
27856
+ }
27857
+ }
27858
+ }
27859
+ }
27650
27860
  }
27651
27861
  // not included in public docs
27652
27862
  // create line between to voxels in drawing
@@ -27728,6 +27938,98 @@ var Niivue = class _Niivue {
27728
27938
  }
27729
27939
  }
27730
27940
  }
27941
+ /**
27942
+ * Performs a 1-voxel binary dilation on a connected cluster within the drawing mask using the drawFloodFillCore function.
27943
+ *
27944
+ * @param seedXYZ - voxel index of the seed voxel in the mask array.
27945
+ * @param neighbors - Number of neighbors to consider for connectivity and dilation (6, 18, or 26).
27946
+ */
27947
+ drawingBinaryDilationWithSeed(seedXYZ, neighbors = 6) {
27948
+ try {
27949
+ let xyz2vx = function(pt) {
27950
+ return pt[0] + pt[1] * nx + pt[2] * nxy;
27951
+ };
27952
+ const mask = this.drawBitmap;
27953
+ const xDim = this.back.dims[1];
27954
+ const yDim = this.back.dims[2];
27955
+ const zDim = this.back.dims[3];
27956
+ const nx = xDim;
27957
+ const nxy = xDim * yDim;
27958
+ const totalVoxels = nxy * zDim;
27959
+ const seedIndex = xyz2vx(seedXYZ);
27960
+ if (seedIndex < 0 || seedIndex >= totalVoxels) {
27961
+ throw new Error("Seed index is out of bounds.");
27962
+ }
27963
+ const seedValue = mask[seedIndex];
27964
+ if (seedValue === 0) {
27965
+ throw new Error("Seed voxel is not part of a filled cluster.");
27966
+ }
27967
+ const img = mask.slice();
27968
+ for (let i = 0; i < totalVoxels; i++) {
27969
+ img[i] = img[i] === seedValue ? 1 : 0;
27970
+ }
27971
+ this.drawFloodFillCore(img, seedIndex, neighbors);
27972
+ const outputMask = mask.slice();
27973
+ const neighborOffsets = [];
27974
+ const offsets6 = [-nxy, nxy, -xDim, xDim, -1, 1];
27975
+ neighborOffsets.push(...offsets6);
27976
+ if (neighbors > 6) {
27977
+ neighborOffsets.push(
27978
+ -xDim - 1,
27979
+ -xDim + 1,
27980
+ xDim - 1,
27981
+ xDim + 1,
27982
+ -nxy - xDim,
27983
+ -nxy + xDim,
27984
+ -nxy - 1,
27985
+ -nxy + 1,
27986
+ nxy - xDim,
27987
+ nxy + xDim,
27988
+ nxy - 1,
27989
+ nxy + 1
27990
+ );
27991
+ }
27992
+ if (neighbors > 18) {
27993
+ neighborOffsets.push(
27994
+ -nxy - xDim - 1,
27995
+ -nxy - xDim + 1,
27996
+ -nxy + xDim - 1,
27997
+ -nxy + xDim + 1,
27998
+ nxy - xDim - 1,
27999
+ nxy - xDim + 1,
28000
+ nxy + xDim - 1,
28001
+ nxy + xDim + 1
28002
+ );
28003
+ }
28004
+ for (let idx = 0; idx < totalVoxels; idx++) {
28005
+ if (img[idx] === 2) {
28006
+ const x = idx % xDim;
28007
+ const y = Math.floor(idx % nxy / xDim);
28008
+ const z = Math.floor(idx / nxy);
28009
+ for (const offset of neighborOffsets) {
28010
+ const neighborIdx = idx + offset;
28011
+ if (neighborIdx < 0 || neighborIdx >= totalVoxels) {
28012
+ continue;
28013
+ }
28014
+ const nx2 = neighborIdx % xDim;
28015
+ const ny = Math.floor(neighborIdx % nxy / xDim);
28016
+ const nz = Math.floor(neighborIdx / nxy);
28017
+ if (Math.abs(nx2 - x) > 1 || Math.abs(ny - y) > 1 || Math.abs(nz - z) > 1) {
28018
+ continue;
28019
+ }
28020
+ if (mask[neighborIdx] === 0) {
28021
+ outputMask[neighborIdx] = seedValue;
28022
+ }
28023
+ }
28024
+ }
28025
+ }
28026
+ this.drawBitmap = outputMask;
28027
+ this.drawAddUndoBitmap();
28028
+ this.refreshDrawing(true);
28029
+ } catch (error) {
28030
+ log.error("Error in drawingBinaryDilationWithSeed:", error);
28031
+ }
28032
+ }
27731
28033
  // a voxel can be defined as having 6, 18 or 26 neighbors:
27732
28034
  // 6: neighbors share faces (distance=1)
27733
28035
  // 18: neighbors share faces (distance=1) or edges (1.4)
@@ -27809,7 +28111,7 @@ var Niivue = class _Niivue {
27809
28111
  }
27810
28112
  // not included in public docs
27811
28113
  // set all connected voxels in drawing to new color
27812
- drawFloodFill(seedXYZ, newColor = 0, growSelectedCluster = 0, forceMin = NaN, forceMax = NaN, neighbors = 6) {
28114
+ drawFloodFill(seedXYZ, newColor = 0, growSelectedCluster = 0, forceMin = NaN, forceMax = NaN, neighbors = 6, maxDistanceMM = Number.POSITIVE_INFINITY, is2D = false) {
27813
28115
  if (!this.drawBitmap) {
27814
28116
  throw new Error("drawBitmap undefined");
27815
28117
  }
@@ -27827,13 +28129,46 @@ var Niivue = class _Niivue {
27827
28129
  const nx = dims[0];
27828
28130
  const nxy = nx * dims[1];
27829
28131
  const nxyz = nxy * dims[2];
27830
- const img = this.drawBitmap.slice();
28132
+ let img = this.drawBitmap.slice();
28133
+ let drawBitmap = this.drawBitmap;
28134
+ if (this.clickToSegmentIsGrowing) {
28135
+ img = this.clickToSegmentGrowingBitmap.slice();
28136
+ drawBitmap = this.clickToSegmentGrowingBitmap;
28137
+ }
27831
28138
  if (img.length !== nxy * dims[2]) {
27832
28139
  return;
27833
28140
  }
28141
+ let constrainXYZ = -1;
28142
+ if (is2D && this.drawPenAxCorSag === 0 /* AXIAL */) {
28143
+ constrainXYZ = 2;
28144
+ } else if (is2D && this.drawPenAxCorSag === 1 /* CORONAL */) {
28145
+ constrainXYZ = 1;
28146
+ } else if (is2D && this.drawPenAxCorSag === 2 /* SAGITTAL */) {
28147
+ constrainXYZ = 0;
28148
+ }
28149
+ function vx2xyz(vx) {
28150
+ const Z = Math.floor(vx / nxy);
28151
+ const Y = Math.floor((vx - Z * nxy) / nx);
28152
+ const X = Math.floor(vx % nx);
28153
+ return [X, Y, Z];
28154
+ }
27834
28155
  function xyz2vx(pt) {
27835
28156
  return pt[0] + pt[1] * nx + pt[2] * nxy;
27836
28157
  }
28158
+ const vx2mm = (xyz) => {
28159
+ return this.vox2mm(xyz, this.back.matRAS);
28160
+ };
28161
+ const seedMM = vx2mm(seedXYZ);
28162
+ const maxDistanceMM2 = maxDistanceMM ** 2;
28163
+ function isWithinDistance(vx) {
28164
+ const xzyVox = vx2xyz(vx);
28165
+ if (constrainXYZ >= 0 && xzyVox[constrainXYZ] !== seedXYZ[constrainXYZ]) {
28166
+ return false;
28167
+ }
28168
+ const xyzMM = vx2mm(xzyVox);
28169
+ const dist2 = (xyzMM[0] - seedMM[0]) ** 2 + (xyzMM[1] - seedMM[1]) ** 2 + (xyzMM[2] - seedMM[2]) ** 2;
28170
+ return dist2 <= maxDistanceMM2;
28171
+ }
27837
28172
  const seedVx = xyz2vx(seedXYZ);
27838
28173
  const seedColor = img[seedVx];
27839
28174
  if (seedColor === newColor) {
@@ -27846,7 +28181,10 @@ var Niivue = class _Niivue {
27846
28181
  }
27847
28182
  for (let i = 1; i < nxyz; i++) {
27848
28183
  img[i] = 0;
27849
- if (this.drawBitmap[i] === seedColor) {
28184
+ if (drawBitmap[i] === seedColor) {
28185
+ if (!isWithinDistance(i)) {
28186
+ continue;
28187
+ }
27850
28188
  img[i] = 1;
27851
28189
  }
27852
28190
  }
@@ -27876,6 +28214,9 @@ var Niivue = class _Niivue {
27876
28214
  for (let i = 1; i < nxyz; i++) {
27877
28215
  img[i] = 0;
27878
28216
  if (backImg[i] >= mn && backImg[i] <= mx) {
28217
+ if (!isWithinDistance(i)) {
28218
+ continue;
28219
+ }
27879
28220
  img[i] = 1;
27880
28221
  }
27881
28222
  }
@@ -27884,11 +28225,19 @@ var Niivue = class _Niivue {
27884
28225
  }
27885
28226
  for (let i = 1; i < nxyz; i++) {
27886
28227
  if (img[i] === 2) {
27887
- this.drawBitmap[i] = newColor;
28228
+ drawBitmap[i] = newColor;
27888
28229
  }
27889
28230
  }
27890
- this.drawAddUndoBitmap();
27891
- this.refreshDrawing(false);
28231
+ if (!this.clickToSegmentIsGrowing) {
28232
+ this.drawBitmap = drawBitmap.slice();
28233
+ if (!this.opts.clickToSegment) {
28234
+ this.drawAddUndoBitmap();
28235
+ }
28236
+ this.refreshDrawing(true, this.clickToSegmentIsGrowing);
28237
+ } else {
28238
+ this.clickToSegmentGrowingBitmap = drawBitmap;
28239
+ this.refreshDrawing(true, this.clickToSegmentIsGrowing);
28240
+ }
27892
28241
  }
27893
28242
  // not included in public docs
27894
28243
  // given series of line segments, connect first and last
@@ -28048,6 +28397,7 @@ var Niivue = class _Niivue {
28048
28397
  this.drawClearAllUndoBitmaps();
28049
28398
  this.rgbaTex(this.drawTexture, TEXTURE7_DRAW, [2, 2, 2, 2], true);
28050
28399
  this.drawBitmap = null;
28400
+ this.clickToSegmentGrowingBitmap = null;
28051
28401
  this.drawScene();
28052
28402
  }
28053
28403
  /**
@@ -28056,7 +28406,7 @@ var Niivue = class _Niivue {
28056
28406
  * @example niivue.refreshDrawing();
28057
28407
  * @see {@link https://niivue.github.io/niivue/features/cactus.html | live demo usage}
28058
28408
  */
28059
- refreshDrawing(isForceRedraw = true) {
28409
+ refreshDrawing(isForceRedraw = true, useClickToSegmentBitmap = false) {
28060
28410
  if (!this.back?.dims) {
28061
28411
  throw new Error("back.dims undefined");
28062
28412
  }
@@ -28085,7 +28435,7 @@ var Niivue = class _Niivue {
28085
28435
  dims[3],
28086
28436
  this.gl.RED,
28087
28437
  this.gl.UNSIGNED_BYTE,
28088
- this.drawBitmap
28438
+ useClickToSegmentBitmap ? this.clickToSegmentGrowingBitmap : this.drawBitmap
28089
28439
  );
28090
28440
  if (isForceRedraw) {
28091
28441
  this.drawScene();
@@ -28497,6 +28847,8 @@ var Niivue = class _Niivue {
28497
28847
  gl.bindVertexArray(this.unusedVAO);
28498
28848
  this.rectShader = new Shader(gl, vertRectShader, fragRectShader);
28499
28849
  this.rectShader.use(gl);
28850
+ this.rectOutlineShader = new Shader(gl, vertRectShader, fragRectOutlineShader);
28851
+ this.rectOutlineShader.use(gl);
28500
28852
  this.lineShader = new Shader(gl, vertLineShader, fragRectShader);
28501
28853
  this.lineShader.use(gl);
28502
28854
  this.line3DShader = new Shader(gl, vertLine3DShader, fragRectShader);
@@ -28649,14 +29001,37 @@ var Niivue = class _Niivue {
28649
29001
  }
28650
29002
  /**
28651
29003
  * 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);
29004
+ * @param options - an object containing the following properties:
29005
+ * - layer: selects image to describe
29006
+ * - masks: optional binary images to filter voxels
29007
+ * - drawingIsMask: a boolean indicating if the drawing is used as a mask
29008
+ * - roiIsMask: a boolean indicating if the ROI is used as a mask
29009
+ * - startVox: the starting voxel coordinates
29010
+ * - endVox: the ending voxel coordinates
29011
+ * @returns numeric values to describe image or regions of images
29012
+ * @example
29013
+ * niivue.getDescriptives({
29014
+ * layer: 0,
29015
+ * masks: [],
29016
+ * drawingIsMask: true, // drawingIsMask and roiIsMask are mutually exclusive
29017
+ * roiIsMask: false,
29018
+ * startVox: [10, 20, 30], // ignored if roiIsMask is false
29019
+ * endVox: [40, 50, 60] // ignored if roiIsMask is false
29020
+ * });
28656
29021
  * @see {@link https://niivue.github.io/niivue/features/draw2.html | live demo usage}
28657
29022
  */
28658
- getDescriptives(layer = 0, masks = [], drawingIsMask = false) {
29023
+ getDescriptives(options) {
29024
+ const {
29025
+ layer = 0,
29026
+ masks = [],
29027
+ drawingIsMask = false,
29028
+ roiIsMask = false,
29029
+ startVox = [0, 0, 0],
29030
+ endVox = [0, 0, 0]
29031
+ } = options;
29032
+ let area = null;
28659
29033
  const hdr = this.volumes[layer].hdr;
29034
+ const pixDimsRAS = this.volumes[layer].pixDimsRAS;
28660
29035
  let slope = hdr.scl_slope;
28661
29036
  if (isNaN(slope)) {
28662
29037
  slope = 1;
@@ -28694,6 +29069,65 @@ var Niivue = class _Niivue {
28694
29069
  mask[i] = 0;
28695
29070
  }
28696
29071
  }
29072
+ } else if (masks.length < 1 && roiIsMask) {
29073
+ mask.fill(0);
29074
+ console.log("startVox", startVox);
29075
+ console.log("endVox", endVox);
29076
+ let constantDim = -1;
29077
+ if (startVox[0] === endVox[0]) {
29078
+ constantDim = 0;
29079
+ } else if (startVox[1] === endVox[1]) {
29080
+ constantDim = 1;
29081
+ } else if (startVox[2] === endVox[2]) {
29082
+ constantDim = 2;
29083
+ } else {
29084
+ console.error("Error: No constant dimension found.");
29085
+ return;
29086
+ }
29087
+ const dims = [0, 1, 2];
29088
+ const varDims = dims.filter((dim) => dim !== constantDim);
29089
+ const centerVox = [];
29090
+ centerVox[constantDim] = startVox[constantDim];
29091
+ centerVox[varDims[0]] = (startVox[varDims[0]] + endVox[varDims[0]]) / 2;
29092
+ centerVox[varDims[1]] = (startVox[varDims[1]] + endVox[varDims[1]]) / 2;
29093
+ const radiusX = Math.abs(endVox[varDims[0]] - startVox[varDims[0]]) / 2;
29094
+ const radiusY = Math.abs(endVox[varDims[1]] - startVox[varDims[1]]) / 2;
29095
+ const xdim = hdr.dims[1];
29096
+ const ydim = hdr.dims[2];
29097
+ const minVarDim0 = Math.max(0, Math.floor(centerVox[varDims[0]] - radiusX));
29098
+ const maxVarDim0 = Math.min(hdr.dims[varDims[0] + 1] - 1, Math.ceil(centerVox[varDims[0]] + radiusX));
29099
+ const minVarDim1 = Math.max(0, Math.floor(centerVox[varDims[1]] - radiusY));
29100
+ const maxVarDim1 = Math.min(hdr.dims[varDims[1] + 1] - 1, Math.ceil(centerVox[varDims[1]] + radiusY));
29101
+ const constDimVal = centerVox[constantDim];
29102
+ if (constDimVal < 0 || constDimVal >= hdr.dims[constantDim + 1]) {
29103
+ console.error("Error: Constant dimension value is out of bounds.");
29104
+ return;
29105
+ }
29106
+ for (let i = minVarDim0; i <= maxVarDim0; i++) {
29107
+ for (let j = minVarDim1; j <= maxVarDim1; j++) {
29108
+ const voxel = [];
29109
+ voxel[constantDim] = constDimVal;
29110
+ voxel[varDims[0]] = i;
29111
+ voxel[varDims[1]] = j;
29112
+ const di = (voxel[varDims[0]] - centerVox[varDims[0]]) / radiusX;
29113
+ const dj = (voxel[varDims[1]] - centerVox[varDims[1]]) / radiusY;
29114
+ const distSq = di * di + dj * dj;
29115
+ if (distSq <= 1) {
29116
+ const x = voxel[0];
29117
+ const y = voxel[1];
29118
+ const z = voxel[2];
29119
+ const index = z * xdim * ydim + y * xdim + x;
29120
+ mask[index] = 1;
29121
+ }
29122
+ }
29123
+ }
29124
+ const voxelArea = pixDimsRAS[varDims[0] + 1] * pixDimsRAS[varDims[1] + 1];
29125
+ const numMaskedVoxels = mask.reduce((count, value) => count + (value === 1 ? 1 : 0), 0);
29126
+ area = numMaskedVoxels * voxelArea;
29127
+ const radiusX_mm = radiusX * pixDimsRAS[varDims[0] + 1];
29128
+ const radiusY_mm = radiusY * pixDimsRAS[varDims[1] + 1];
29129
+ const areaEllipse = Math.PI * radiusX_mm * radiusY_mm;
29130
+ area = areaEllipse;
28697
29131
  }
28698
29132
  let k = 0;
28699
29133
  let M = 0;
@@ -28747,7 +29181,8 @@ var Niivue = class _Niivue {
28747
29181
  cal_min: this.volumes[layer].cal_min,
28748
29182
  cal_max: this.volumes[layer].cal_max,
28749
29183
  robust_min: this.volumes[layer].robust_min,
28750
- robust_max: this.volumes[layer].robust_max
29184
+ robust_max: this.volumes[layer].robust_max,
29185
+ area
28751
29186
  };
28752
29187
  }
28753
29188
  // not included in public docs
@@ -30225,6 +30660,29 @@ var Niivue = class _Niivue {
30225
30660
  const pos = [(x - this.graph.LTWH[0]) / this.graph.LTWH[2], (y - this.graph.LTWH[1]) / this.graph.LTWH[3]];
30226
30661
  return pos[0] > 0 && pos[1] > 0 && pos[0] <= 1 && pos[1] <= 1;
30227
30662
  }
30663
+ // update drawBitmap if it differs from clickTosegmentBitmap
30664
+ updateBitmapFromClickToSegment() {
30665
+ if (this.clickToSegmentGrowingBitmap === null) {
30666
+ return;
30667
+ }
30668
+ if (this.drawBitmap === null) {
30669
+ return;
30670
+ }
30671
+ if (this.clickToSegmentGrowingBitmap.length !== this.drawBitmap.length) {
30672
+ return;
30673
+ }
30674
+ const nvx = this.drawBitmap.length;
30675
+ for (let i = 0; i < nvx; i++) {
30676
+ this.drawBitmap[i] = this.clickToSegmentGrowingBitmap[i];
30677
+ }
30678
+ }
30679
+ sumBitmap(img) {
30680
+ let sum = 0;
30681
+ for (let i = 0; i < img.length; i++) {
30682
+ sum += img[i];
30683
+ }
30684
+ return sum;
30685
+ }
30228
30686
  // not included in public docs
30229
30687
  // handle mouse click event on canvas
30230
30688
  mouseClick(x, y, posChange = 0, isDelta = true) {
@@ -30274,7 +30732,7 @@ var Niivue = class _Niivue {
30274
30732
  if (axCorSag > 2 /* SAGITTAL */) {
30275
30733
  continue;
30276
30734
  }
30277
- const texFrac = this.screenXY2TextureFrac(x, y, i, false);
30735
+ let texFrac = this.screenXY2TextureFrac(x, y, i, false);
30278
30736
  if (texFrac[0] < 0) {
30279
30737
  continue;
30280
30738
  }
@@ -30301,31 +30759,75 @@ var Niivue = class _Niivue {
30301
30759
  this.scene.crosshairPos = vec311.clone(texFrac);
30302
30760
  }
30303
30761
  if (this.opts.drawingEnabled) {
30304
- const pt = this.frac2vox(this.scene.crosshairPos);
30305
- const ptMM = this.frac2mm(this.scene.crosshairPos);
30762
+ let pt = this.frac2vox(this.scene.crosshairPos);
30306
30763
  if (this.opts.clickToSegment) {
30307
- const radius = this.opts.clickToSegmentRadius;
30308
- const steps = this.opts.clickToSegmentSteps;
30764
+ texFrac = this.screenXY2TextureFrac(this.clickToSegmentXY[0], this.clickToSegmentXY[1], i, false);
30765
+ pt = this.frac2vox(texFrac);
30766
+ let diff = 0;
30767
+ let threshold = this.opts.clickToSegmentPercent + diff;
30768
+ if (this.uiData.mousedown) {
30769
+ const xDiff = (this.clickToSegmentXY[0] - x) / this.gl.canvas.width;
30770
+ const yDiff = (this.clickToSegmentXY[1] - y) / this.gl.canvas.height;
30771
+ diff = Math.max(Math.abs(xDiff), Math.abs(yDiff));
30772
+ threshold = this.opts.clickToSegmentPercent + diff;
30773
+ }
30774
+ let voxelIntensity = this.back.getValue(pt[0], pt[1], pt[2]);
30775
+ if (this.opts.clickToSegmentAutoIntensity) {
30776
+ if (threshold !== 0) {
30777
+ if (voxelIntensity === 0) {
30778
+ voxelIntensity = 0.01;
30779
+ }
30780
+ this.opts.clickToSegmentIntensityMax = voxelIntensity * (1 + threshold);
30781
+ this.opts.clickToSegmentIntensityMin = voxelIntensity * (1 - threshold);
30782
+ }
30783
+ if (voxelIntensity > (this.back.cal_min + this.back.cal_max) * 0.5) {
30784
+ this.opts.clickToSegmentBright = true;
30785
+ } else {
30786
+ this.opts.clickToSegmentBright = false;
30787
+ }
30788
+ }
30309
30789
  const brightOrDark = this.opts.clickToSegmentBright ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
30310
- this.drawPenFillPts = [];
30311
30790
  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
- }
30791
+ if (this.drawPenAxCorSag === 0 /* AXIAL */) {
30792
+ const pixDims = [this.back.pixDimsRAS[0], this.back.pixDimsRAS[1]];
30793
+ const minPixDim = Math.min(...pixDims);
30794
+ this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim);
30795
+ } else if (this.drawPenAxCorSag === 1 /* CORONAL */) {
30796
+ const pixDims = [this.back.pixDimsRAS[0], this.back.pixDimsRAS[2]];
30797
+ const minPixDim = Math.min(...pixDims);
30798
+ this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim);
30799
+ } else if (this.drawPenAxCorSag === 2 /* SAGITTAL */) {
30800
+ const pixDims = [this.back.pixDimsRAS[1], this.back.pixDimsRAS[2]];
30801
+ const minPixDim = Math.min(...pixDims);
30802
+ this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim);
30325
30803
  }
30804
+ this.drawPt(pt[0], pt[1], pt[2], this.opts.penValue);
30805
+ if (diff !== 0) {
30806
+ this.clickToSegmentIsGrowing = true;
30807
+ this.clickToSegmentGrowingBitmap = this.drawBitmap.slice();
30808
+ } else {
30809
+ this.clickToSegmentIsGrowing = false;
30810
+ }
30811
+ this.drawFloodFill(
30812
+ [pt[0], pt[1], pt[2]],
30813
+ 0,
30814
+ brightOrDark,
30815
+ this.opts.clickToSegmentIntensityMin,
30816
+ this.opts.clickToSegmentIntensityMax,
30817
+ this.opts.floodFillNeighbors,
30818
+ this.opts.clickToSegmentMaxDistanceMM,
30819
+ this.opts.clickToSegmentIs2D
30820
+ );
30326
30821
  this.drawScene();
30327
30822
  this.createOnLocationChange(axCorSag);
30328
- const info = this.getDescriptives(0, [], true);
30823
+ if (this.clickToSegmentIsGrowing) {
30824
+ return;
30825
+ }
30826
+ const info = this.getDescriptives({
30827
+ layer: 0,
30828
+ masks: [],
30829
+ drawingIsMask: true
30830
+ });
30329
30831
  this.onClickToSegment({ mL: info.volumeML, mm3: info.volumeMM3 });
30330
30832
  return;
30331
30833
  }
@@ -30543,20 +31045,42 @@ var Niivue = class _Niivue {
30543
31045
  if (!this.rectShader) {
30544
31046
  throw new Error("rectShader undefined");
30545
31047
  }
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);
31048
+ if (!this.opts.selectionBoxIsOutline) {
31049
+ this.rectShader.use(this.gl);
31050
+ this.gl.enable(this.gl.BLEND);
31051
+ this.gl.uniform4fv(this.rectShader.uniforms.lineColor, lineColor);
31052
+ this.gl.uniform2fv(this.rectShader.uniforms.canvasWidthHeight, [this.gl.canvas.width, this.gl.canvas.height]);
31053
+ this.gl.uniform4f(
31054
+ this.rectShader.uniforms.leftTopWidthHeight,
31055
+ leftTopWidthHeight[0],
31056
+ leftTopWidthHeight[1],
31057
+ leftTopWidthHeight[2],
31058
+ leftTopWidthHeight[3]
31059
+ );
31060
+ this.gl.bindVertexArray(this.genericVAO);
31061
+ this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
31062
+ this.gl.bindVertexArray(this.unusedVAO);
31063
+ } else {
31064
+ this.drawCircle(leftTopWidthHeight, lineColor, 0.1);
31065
+ this.rectOutlineShader.use(this.gl);
31066
+ this.gl.enable(this.gl.BLEND);
31067
+ this.gl.uniform1f(this.rectOutlineShader.uniforms.thickness, this.opts.selectionBoxLineThickness);
31068
+ this.gl.uniform4fv(this.rectOutlineShader.uniforms.lineColor, lineColor);
31069
+ this.gl.uniform2fv(this.rectOutlineShader.uniforms.canvasWidthHeight, [
31070
+ this.gl.canvas.width,
31071
+ this.gl.canvas.height
31072
+ ]);
31073
+ this.gl.uniform4f(
31074
+ this.rectOutlineShader.uniforms.leftTopWidthHeight,
31075
+ leftTopWidthHeight[0],
31076
+ leftTopWidthHeight[1],
31077
+ leftTopWidthHeight[2],
31078
+ leftTopWidthHeight[3]
31079
+ );
31080
+ this.gl.bindVertexArray(this.genericVAO);
31081
+ this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
31082
+ this.gl.bindVertexArray(this.unusedVAO);
31083
+ }
30560
31084
  }
30561
31085
  drawCircle(leftTopWidthHeight, circleColor = this.opts.fontColor, fillPercent = 1) {
30562
31086
  if (!this.circleShader) {
@@ -30582,6 +31106,10 @@ var Niivue = class _Niivue {
30582
31106
  // not included in public docs
30583
31107
  // draw a rectangle at desired location
30584
31108
  drawSelectionBox(leftTopWidthHeight) {
31109
+ if (this.opts.dragMode === 6 /* roiSelection */) {
31110
+ this.drawCircle(leftTopWidthHeight, this.opts.selectionBoxColor, 0.1);
31111
+ return;
31112
+ }
30585
31113
  this.drawRect(leftTopWidthHeight, this.opts.selectionBoxColor);
30586
31114
  }
30587
31115
  // not included in public docs
@@ -31572,10 +32100,10 @@ var Niivue = class _Niivue {
31572
32100
  if (graph.opacity <= 0 || graph.LTWH[2] <= 5 || graph.LTWH[3] <= 5) {
31573
32101
  return;
31574
32102
  }
31575
- if (graph.LTWH[0] + graph.LTWH[2] > this.gl.canvas.width) {
32103
+ if (Math.floor(graph.LTWH[0] + graph.LTWH[2]) > this.gl.canvas.width) {
31576
32104
  return;
31577
32105
  }
31578
- if (graph.LTWH[1] + graph.LTWH[3] > this.gl.canvas.height) {
32106
+ if (Math.floor(graph.LTWH[1] + graph.LTWH[3]) > this.gl.canvas.height) {
31579
32107
  return;
31580
32108
  }
31581
32109
  graph.backColor = [0.15, 0.15, 0.15, graph.opacity];
@@ -31645,6 +32173,12 @@ var Niivue = class _Niivue {
31645
32173
  mx = Math.max(v, mx);
31646
32174
  }
31647
32175
  }
32176
+ const volMn = this.volumes[vols[0]].cal_min;
32177
+ const volMx = this.volumes[vols[0]].cal_max;
32178
+ if (graph.isRangeCalMinMax && volMn < volMx && isFinite(volMn) && isFinite(volMx)) {
32179
+ mn = volMn;
32180
+ mx = volMx;
32181
+ }
31648
32182
  if (graph.normalizeValues && mx > mn) {
31649
32183
  const range = mx - mn;
31650
32184
  for (let j = 0; j < graph.lines.length; j++) {
@@ -33258,10 +33792,14 @@ var Niivue = class _Niivue {
33258
33792
  width,
33259
33793
  height
33260
33794
  ]);
33795
+ return;
33261
33796
  }
33262
33797
  if (this.opts.clickToSegment) {
33263
33798
  const x = this.mousePos[0];
33264
33799
  const y = this.mousePos[1];
33800
+ if (this.inRenderTile(x, y) >= 0) {
33801
+ return;
33802
+ }
33265
33803
  const tileIdx = this.tileIndex(x, y);
33266
33804
  if (tileIdx > -1) {
33267
33805
  const fovMM = this.screenSlices[tileIdx].fovMM;