@niivue/niivue 0.45.1 → 0.47.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.47.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;
@@ -12328,10 +12368,10 @@ var NVMeshLoaders = class _NVMeshLoaders {
12328
12368
  });
12329
12369
  }
12330
12370
  const dpv32 = [];
12331
- for (let s = 0; s < dpv.length; s++) {
12371
+ for (let i2 = 0; i2 < dpv.length; i2++) {
12332
12372
  dpv32.push({
12333
- id: dpv[i].id,
12334
- vals: Float32Array.from(dpv[i].vals)
12373
+ id: dpv[i2].id,
12374
+ vals: Float32Array.from(dpv[i2].vals)
12335
12375
  });
12336
12376
  }
12337
12377
  offsetPt0[noffset++] = npt;
@@ -14445,6 +14485,7 @@ var NVMeshLoaders = class _NVMeshLoaders {
14445
14485
  plen = reader.getInt32(voxoffset += 4, isLittleEndian) - 1;
14446
14486
  break;
14447
14487
  case TAG_OLD_SURF_GEOM:
14488
+ // these don't take lengths at all
14448
14489
  case TAG_OLD_USEREALRAS:
14449
14490
  plen = 0;
14450
14491
  break;
@@ -20221,6 +20262,11 @@ var NVImage = class _NVImage {
20221
20262
  case "comments":
20222
20263
  hdr.description = items[0].substring(0, Math.min(79, items[0].length));
20223
20264
  break;
20265
+ /* case 'command_history':
20266
+ if (items[0].startsWith('dwi2tensor')) {
20267
+ isTensor = true
20268
+ }
20269
+ break */
20224
20270
  case "RepetitionTime":
20225
20271
  TR = parseFloat(items[0]);
20226
20272
  break;
@@ -20942,11 +20988,13 @@ var NVImage = class _NVImage {
20942
20988
  this.onOpacityChange(this);
20943
20989
  }
20944
20990
  }
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() {
20991
+ /**
20992
+ * set contrast/brightness to robust range (2%..98%)
20993
+ * @param vol - volume for estimate (use -1 to use estimate on all loaded volumes; use INFINITY for current volume)
20994
+ * @sets volume brightness and returns array [pct2, pct98, mnScale, mxScale]
20995
+ * @see {@link https://niivue.github.io/niivue/features/timeseries2.html | live demo usage}
20996
+ */
20997
+ calMinMax(vol = Number.POSITIVE_INFINITY) {
20950
20998
  if (!this.hdr) {
20951
20999
  throw new Error("hdr undefined");
20952
21000
  }
@@ -20957,10 +21005,20 @@ var NVImage = class _NVImage {
20957
21005
  let mx = Number.NEGATIVE_INFINITY;
20958
21006
  let nZero = 0;
20959
21007
  let nNan = 0;
20960
- const nVox = this.img.length;
21008
+ let voxEnd = this.img.length;
21009
+ let voxStart = 0;
21010
+ const nVox3D = this.hdr.dims[1] * this.hdr.dims[2] * this.hdr.dims[3];
21011
+ const nVol = Math.floor(voxEnd / nVox3D);
21012
+ if (vol >= nVol) {
21013
+ vol = this.frame4D;
21014
+ }
21015
+ if (vol >= 0 && vol < nVol) {
21016
+ voxStart = vol * nVox3D;
21017
+ voxEnd = voxStart + nVox3D;
21018
+ }
20961
21019
  const isFastCalc = this.img.constructor !== Float64Array && this.img.constructor !== Float32Array && this.ignoreZeroVoxels;
20962
21020
  if (isFastCalc) {
20963
- for (let i = 0; i < nVox; i++) {
21021
+ for (let i = voxStart; i < voxEnd; i++) {
20964
21022
  mn = Math.min(this.img[i], mn);
20965
21023
  mx = Math.max(this.img[i], mx);
20966
21024
  if (this.img[i] === 0) {
@@ -20968,7 +21026,7 @@ var NVImage = class _NVImage {
20968
21026
  }
20969
21027
  }
20970
21028
  } else {
20971
- for (let i = 0; i < nVox; i++) {
21029
+ for (let i = voxStart; i < voxEnd; i++) {
20972
21030
  if (isNaN(this.img[i])) {
20973
21031
  nNan++;
20974
21032
  continue;
@@ -21013,7 +21071,7 @@ var NVImage = class _NVImage {
21013
21071
  this.robust_max = this.cal_max;
21014
21072
  return [cmMin, cmMax, cmMin, cmMax];
21015
21073
  }
21016
- const percentZero = 100 * nZero / nVox;
21074
+ const percentZero = 100 * nZero / (voxEnd - voxStart);
21017
21075
  let isOverrideIgnoreZeroVoxels = false;
21018
21076
  if (percentZero > 60 && !this.ignoreZeroVoxels) {
21019
21077
  log.warn(`${Math.round(percentZero)}% of voxels are zero: ignoring zeros for cal_max`);
@@ -21024,7 +21082,7 @@ var NVImage = class _NVImage {
21024
21082
  nZero = 0;
21025
21083
  }
21026
21084
  nZero += nNan;
21027
- const n2pct = Math.round((nVox - nZero) * this.percentileFrac);
21085
+ const n2pct = Math.round((voxEnd - voxStart - nZero) * this.percentileFrac);
21028
21086
  if (n2pct < 1 || mn === mx) {
21029
21087
  log.debug("no variability in image intensity?");
21030
21088
  this.cal_min = mnScale;
@@ -21042,11 +21100,11 @@ var NVImage = class _NVImage {
21042
21100
  hist[i] = 0;
21043
21101
  }
21044
21102
  if (isFastCalc) {
21045
- for (let i = 0; i < nVox; i++) {
21103
+ for (let i = voxStart; i < voxEnd; i++) {
21046
21104
  hist[Math.round((this.img[i] - mn) * scl)]++;
21047
21105
  }
21048
21106
  } else if (this.ignoreZeroVoxels) {
21049
- for (let i = 0; i < nVox; i++) {
21107
+ for (let i = voxStart; i < voxEnd; i++) {
21050
21108
  if (this.img[i] === 0) {
21051
21109
  continue;
21052
21110
  }
@@ -21056,7 +21114,7 @@ var NVImage = class _NVImage {
21056
21114
  hist[Math.round((this.img[i] - mn) * scl)]++;
21057
21115
  }
21058
21116
  } else {
21059
- for (let i = 0; i < nVox; i++) {
21117
+ for (let i = voxStart; i < voxEnd; i++) {
21060
21118
  if (isNaN(this.img[i])) {
21061
21119
  continue;
21062
21120
  }
@@ -22250,12 +22308,14 @@ var DRAG_MODE = /* @__PURE__ */ ((DRAG_MODE2) => {
22250
22308
  DRAG_MODE2[DRAG_MODE2["pan"] = 3] = "pan";
22251
22309
  DRAG_MODE2[DRAG_MODE2["slicer3D"] = 4] = "slicer3D";
22252
22310
  DRAG_MODE2[DRAG_MODE2["callbackOnly"] = 5] = "callbackOnly";
22311
+ DRAG_MODE2[DRAG_MODE2["roiSelection"] = 6] = "roiSelection";
22253
22312
  return DRAG_MODE2;
22254
22313
  })(DRAG_MODE || {});
22255
22314
  var DEFAULT_OPTIONS = {
22256
22315
  textHeight: 0.06,
22257
22316
  colorbarHeight: 0.05,
22258
22317
  crosshairWidth: 1,
22318
+ crosshairWidthUnit: "voxels",
22259
22319
  crosshairGap: 0,
22260
22320
  rulerWidth: 4,
22261
22321
  show3Dcrosshair: false,
@@ -22281,6 +22341,7 @@ var DEFAULT_OPTIONS = {
22281
22341
  isRuler: false,
22282
22342
  isColorbar: false,
22283
22343
  isOrientCube: false,
22344
+ tileMargin: 0,
22284
22345
  multiplanarPadPixels: 0,
22285
22346
  // @deprecated
22286
22347
  multiplanarForceRender: false,
@@ -22293,6 +22354,8 @@ var DEFAULT_OPTIONS = {
22293
22354
  yoke3Dto2DZoom: false,
22294
22355
  isDepthPickMesh: false,
22295
22356
  isCornerOrientationText: false,
22357
+ heroImageFraction: 0,
22358
+ heroSliceType: 4 /* RENDER */,
22296
22359
  sagittalNoseLeft: false,
22297
22360
  isSliceMM: false,
22298
22361
  isV1SliceShader: false,
@@ -22319,20 +22382,49 @@ var DEFAULT_OPTIONS = {
22319
22382
  renderOverlayBlend: 1,
22320
22383
  sliceMosaicString: "",
22321
22384
  centerMosaic: false,
22385
+ penSize: 1,
22386
+ // in voxels, since all drawing is done using bitmap indices
22322
22387
  clickToSegment: false,
22323
- clickToSegmentRadius: 2,
22324
- clickToSegmentSteps: 10,
22325
- clickToSegmentBright: true
22388
+ clickToSegmentRadius: 3,
22389
+ // in mm
22390
+ clickToSegmentBright: true,
22391
+ clickToSegmentAutoIntensity: false,
22392
+ // new option, but keep clickToSegmentBright for backwards compatibility
22393
+ clickToSegmentIntensityMax: NaN,
22394
+ // NaN will use auto threshold (default flood fill behavior from before)
22395
+ clickToSegmentIntensityMin: NaN,
22396
+ // NaN will use auto threshold (default flood fill behavior from before)
22397
+ // 0 will use auto threshold (default flood fill behavior from before)
22398
+ // Take the voxel intensity at the click point and use this percentage +/- to threshold the flood fill operation.
22399
+ // If greater than 0, clickedVoxelIntensity +/- clickedVoxelIntensity * clickToSegmentPercent will be used
22400
+ // for the clickToSegmentIntensityMin and clickToSegmentIntensityMax values.
22401
+ clickToSegmentPercent: 0,
22402
+ clickToSegmentMaxDistanceMM: Number.POSITIVE_INFINITY,
22403
+ // default value is infinity for backwards compatibility with flood fill routine.
22404
+ clickToSegmentIs2D: false,
22405
+ selectionBoxLineThickness: 4,
22406
+ selectionBoxIsOutline: false,
22407
+ scrollRequiresFocus: false,
22408
+ // determines if the cavas need to be focused to scroll
22409
+ showMeasureUnits: true,
22410
+ // e.g. 20.2 vs 20.2 mm
22411
+ measureTextJustify: "center",
22412
+ // start, center, end
22413
+ measureTextColor: [1, 0, 0, 1],
22414
+ // red
22415
+ measureLineColor: [1, 0, 0, 1],
22416
+ // red
22417
+ measureTextHeight: 0.03
22326
22418
  };
22327
22419
  var INITIAL_SCENE_DATA = {
22328
22420
  gamma: 1,
22329
22421
  azimuth: 110,
22330
22422
  elevation: 10,
22331
- crosshairPos: vec38.create(),
22423
+ crosshairPos: vec38.fromValues(0.5, 0.5, 0.5),
22332
22424
  clipPlane: [0, 0, 0, 0],
22333
22425
  clipPlaneDepthAziElev: [2, 0, 0],
22334
22426
  volScaleMultiplier: 1,
22335
- pan2Dxyzmm: vec46.create(),
22427
+ pan2Dxyzmm: vec46.fromValues(0, 0, 0, 1),
22336
22428
  clipThick: 2,
22337
22429
  clipVolumeLow: [0, 0, 0],
22338
22430
  clipVolumeHigh: [1, 1, 1]
@@ -22980,8 +23072,27 @@ var NVConnectome = class _NVConnectome extends NVMesh2 {
22980
23072
  const nodes = this.nodes;
22981
23073
  if (nodes && nodes.length > 0) {
22982
23074
  const largest = nodes.reduce((a, b) => a.sizeValue > b.sizeValue ? a : b).sizeValue;
22983
- const min = this.nodeMinColor ? this.nodeMinColor : nodes.reduce((a, b) => a.colorValue < b.colorValue ? a : b).colorValue;
22984
- const max = this.nodeMaxColor ? this.nodeMaxColor : nodes.reduce((a, b) => a.colorValue > b.colorValue ? a : b).colorValue;
23075
+ let min, max;
23076
+ if (typeof this.nodeMinColor !== "undefined" && isFinite(this.nodeMinColor)) {
23077
+ min = this.nodeMinColor;
23078
+ } else {
23079
+ min = nodes[0].colorValue;
23080
+ for (let i = 1; i < nodes.length; i++) {
23081
+ if (nodes[i].colorValue < min) {
23082
+ min = nodes[i].colorValue;
23083
+ }
23084
+ }
23085
+ }
23086
+ if (typeof this.nodeMaxColor !== "undefined" && isFinite(this.nodeMaxColor)) {
23087
+ max = this.nodeMaxColor;
23088
+ } else {
23089
+ max = nodes[0].colorValue;
23090
+ for (let i = 1; i < nodes.length; i++) {
23091
+ if (nodes[i].colorValue > max) {
23092
+ max = nodes[i].colorValue;
23093
+ }
23094
+ }
23095
+ }
22985
23096
  const lut = cmapper.colormap(this.nodeColormap, this.colormapInvert);
22986
23097
  const lutNeg = cmapper.colormap(this.nodeColormapNegative, this.colormapInvert);
22987
23098
  const hasNeg = "nodeColormapNegative" in this;
@@ -23112,14 +23223,29 @@ var NVConnectome = class _NVConnectome extends NVMesh2 {
23112
23223
  let lut = cmapper.colormap(this.nodeColormap, this.colormapInvert);
23113
23224
  let lutNeg = cmapper.colormap(this.nodeColormapNegative, this.colormapInvert);
23114
23225
  let hasNeg = "nodeColormapNegative" in this;
23115
- if (this.nodeMinColor === void 0 || this.nodeMaxColor === void 0) {
23116
- throw new Error("nodeMinColor or nodeMaxColor is undefined");
23226
+ if (this.nodeMinColor === void 0) {
23227
+ this.nodeMinColor = NaN;
23228
+ }
23229
+ if (this.nodeMaxColor === void 0) {
23230
+ this.nodeMaxColor = NaN;
23117
23231
  }
23118
- if (this.edgeMin === void 0 || this.edgeMax === void 0) {
23119
- throw new Error("edgeMin or edgeMax undefined");
23232
+ if (this.edgeMin === void 0) {
23233
+ this.edgeMin = NaN;
23234
+ }
23235
+ if (this.edgeMax === void 0) {
23236
+ this.edgeMax = NaN;
23120
23237
  }
23121
23238
  let min = this.nodeMinColor;
23122
23239
  let max = this.nodeMaxColor;
23240
+ if (!isFinite(min) || !isFinite(min)) {
23241
+ const nodes2 = this.nodes;
23242
+ min = nodes2[0].colorValue;
23243
+ max = nodes2[0].colorValue;
23244
+ for (let i = 0; i < nodes2.length; i++) {
23245
+ min = Math.min(min, nodes2[i].colorValue);
23246
+ max = Math.max(max, nodes2[i].colorValue);
23247
+ }
23248
+ }
23123
23249
  const nodes = this.nodes;
23124
23250
  const nNode = nodes.length;
23125
23251
  for (let i = 0; i < nNode; i++) {
@@ -23152,35 +23278,45 @@ var NVConnectome = class _NVConnectome extends NVMesh2 {
23152
23278
  lut = cmapper.colormap(this.edgeColormap, this.colormapInvert);
23153
23279
  lutNeg = cmapper.colormap(this.edgeColormapNegative, this.colormapInvert);
23154
23280
  hasNeg = "edgeColormapNegative" in this;
23155
- min = this.edgeMin;
23156
- max = this.edgeMax;
23157
23281
  const edges = this.edges;
23158
- for (const edge of edges) {
23159
- let color = edge.colorValue;
23160
- const isNeg = hasNeg && color < 0;
23161
- if (isNeg) {
23162
- color = -color;
23163
- }
23164
- const radius = color * this.edgeScale;
23165
- if (radius <= 0) {
23166
- continue;
23167
- }
23168
- if (min < max) {
23169
- if (color < min) {
23282
+ if (edges !== void 0 && edges.length > 0) {
23283
+ min = this.edgeMin;
23284
+ max = this.edgeMax;
23285
+ if (!isFinite(min) || !isFinite(min)) {
23286
+ min = edges[0].colorValue;
23287
+ max = edges[0].colorValue;
23288
+ for (let i = 0; i < edges.length; i++) {
23289
+ min = Math.min(min, edges[i].colorValue);
23290
+ max = Math.max(max, edges[i].colorValue);
23291
+ }
23292
+ }
23293
+ for (const edge of edges) {
23294
+ let color = edge.colorValue;
23295
+ const isNeg = hasNeg && color < 0;
23296
+ if (isNeg) {
23297
+ color = -color;
23298
+ }
23299
+ const radius = color * this.edgeScale;
23300
+ if (radius <= 0) {
23170
23301
  continue;
23171
23302
  }
23172
- color = (color - min) / (max - min);
23173
- } else {
23174
- color = 1;
23175
- }
23176
- color = Math.round(Math.max(Math.min(255, color * 255))) * 4;
23177
- let rgba = [lut[color], lut[color + 1], lut[color + 2], 255];
23178
- if (isNeg) {
23179
- rgba = [lutNeg[color], lutNeg[color + 1], lutNeg[color + 2], 255];
23303
+ if (min < max) {
23304
+ if (color < min) {
23305
+ continue;
23306
+ }
23307
+ color = (color - min) / (max - min);
23308
+ } else {
23309
+ color = 1;
23310
+ }
23311
+ color = Math.round(Math.max(Math.min(255, color * 255))) * 4;
23312
+ let rgba = [lut[color], lut[color + 1], lut[color + 2], 255];
23313
+ if (isNeg) {
23314
+ rgba = [lutNeg[color], lutNeg[color + 1], lutNeg[color + 2], 255];
23315
+ }
23316
+ const pti = vec39.fromValues(nodes[edge.first].x, nodes[edge.first].y, nodes[edge.first].z);
23317
+ const ptj = vec39.fromValues(nodes[edge.second].x, nodes[edge.second].y, nodes[edge.second].z);
23318
+ NiivueObject3D.makeColoredCylinder(pts, tris, rgba255, pti, ptj, radius, rgba);
23180
23319
  }
23181
- const pti = vec39.fromValues(nodes[edge.first].x, nodes[edge.first].y, nodes[edge.first].z);
23182
- const ptj = vec39.fromValues(nodes[edge.second].x, nodes[edge.second].y, nodes[edge.second].z);
23183
- NiivueObject3D.makeColoredCylinder(pts, tris, rgba255, pti, ptj, radius, rgba);
23184
23320
  }
23185
23321
  const pts32 = new Float32Array(pts);
23186
23322
  const tris32 = new Uint32Array(tris);
@@ -24019,7 +24155,7 @@ var defaultSaveImageOptions = {
24019
24155
  isSaveDrawing: false,
24020
24156
  volumeByIndex: 0
24021
24157
  };
24022
- var Niivue = class _Niivue {
24158
+ var Niivue = class {
24023
24159
  /**
24024
24160
  * @param options - options object to set modifiable Niivue properties
24025
24161
  */
@@ -24050,6 +24186,12 @@ var Niivue = class _Niivue {
24050
24186
  // the color lookup table for drawing
24051
24187
  __publicField(this, "drawOpacity", 0.8);
24052
24188
  // opacity of drawing (default)
24189
+ __publicField(this, "clickToSegmentIsGrowing", false);
24190
+ // flag to indicate if the clickToSegment flood fill growing is in progress with left mouse down + drag
24191
+ __publicField(this, "clickToSegmentGrowingBitmap", null);
24192
+ // the bitmap of the growing flood fill
24193
+ __publicField(this, "clickToSegmentXY", [0, 0]);
24194
+ // the x,y location of the clickToSegment flood fill
24053
24195
  __publicField(this, "renderDrawAmbientOcclusion", 0.4);
24054
24196
  __publicField(this, "colorbarHeight", 0);
24055
24197
  // height in pixels, set when colorbar is drawn
@@ -24067,6 +24209,7 @@ var Niivue = class _Niivue {
24067
24209
  __publicField(this, "orientCubeShader");
24068
24210
  __publicField(this, "orientCubeShaderVAO", null);
24069
24211
  __publicField(this, "rectShader");
24212
+ __publicField(this, "rectOutlineShader");
24070
24213
  __publicField(this, "renderShader");
24071
24214
  __publicField(this, "lineShader");
24072
24215
  __publicField(this, "line3DShader");
@@ -24119,7 +24262,20 @@ var Niivue = class _Niivue {
24119
24262
  __publicField(this, "extentsMax");
24120
24263
  // ResizeObserver
24121
24264
  __publicField(this, "resizeObserver", null);
24122
- __publicField(this, "syncOpts", {});
24265
+ // syncOpts: Record<string, unknown> = {}
24266
+ __publicField(this, "syncOpts", {
24267
+ "3d": false,
24268
+ // legacy option
24269
+ "2d": false,
24270
+ // legacy option
24271
+ zoomPan: false,
24272
+ cal_min: false,
24273
+ cal_max: false,
24274
+ clipPlane: false,
24275
+ gamma: false,
24276
+ sliceType: false,
24277
+ crosshair: false
24278
+ });
24123
24279
  __publicField(this, "readyForSync", false);
24124
24280
  // UI Data
24125
24281
  __publicField(this, "uiData", {
@@ -24178,7 +24334,8 @@ var Niivue = class _Niivue {
24178
24334
  vols: [0],
24179
24335
  // e.g. timeline for background volume only, e.g. [0,2] for first and third volumes
24180
24336
  autoSizeMultiplanar: false,
24181
- normalizeValues: false
24337
+ normalizeValues: false,
24338
+ isRangeCalMinMax: false
24182
24339
  });
24183
24340
  __publicField(this, "meshShaders", [
24184
24341
  {
@@ -24578,8 +24735,16 @@ var Niivue = class _Niivue {
24578
24735
  this.canvas.style.display = "block";
24579
24736
  this.canvas.width = this.canvas.offsetWidth;
24580
24737
  this.canvas.height = this.canvas.offsetHeight;
24581
- window.addEventListener("resize", this.resizeListener.bind(this));
24582
- this.resizeObserver = new ResizeObserver(this.resizeListener.bind(this));
24738
+ window.addEventListener("resize", () => {
24739
+ requestAnimationFrame(() => {
24740
+ this.resizeListener();
24741
+ });
24742
+ });
24743
+ this.resizeObserver = new ResizeObserver(() => {
24744
+ requestAnimationFrame(() => {
24745
+ this.resizeListener();
24746
+ });
24747
+ });
24583
24748
  this.resizeObserver.observe(this.canvas.parentElement);
24584
24749
  }
24585
24750
  this.registerInteractions();
@@ -24598,6 +24763,9 @@ var Niivue = class _Niivue {
24598
24763
  * @see {@link https://niivue.github.io/niivue/features/sync.mesh.html | live demo usage}
24599
24764
  */
24600
24765
  syncWith(otherNV, syncOpts = { "2d": true, "3d": true }) {
24766
+ if (!(otherNV instanceof Array)) {
24767
+ otherNV = [otherNV];
24768
+ }
24601
24769
  this.otherNV = otherNV;
24602
24770
  this.syncOpts = { ...syncOpts };
24603
24771
  }
@@ -24613,9 +24781,55 @@ var Niivue = class _Niivue {
24613
24781
  * @see {@link https://niivue.github.io/niivue/features/sync.mesh.html | live demo usage}
24614
24782
  */
24615
24783
  broadcastTo(otherNV, syncOpts = { "2d": true, "3d": true }) {
24784
+ if (!(otherNV instanceof Array)) {
24785
+ otherNV = [otherNV];
24786
+ }
24616
24787
  this.otherNV = otherNV;
24617
24788
  this.syncOpts = syncOpts;
24618
24789
  }
24790
+ doSync3d(otherNV) {
24791
+ otherNV.scene.renderAzimuth = this.scene.renderAzimuth;
24792
+ otherNV.scene.renderElevation = this.scene.renderElevation;
24793
+ otherNV.scene.volScaleMultiplier = this.scene.volScaleMultiplier;
24794
+ }
24795
+ // both crosshair and zoomPan
24796
+ doSync2d(otherNV) {
24797
+ const thisMM = this.frac2mm(this.scene.crosshairPos);
24798
+ otherNV.scene.crosshairPos = otherNV.mm2frac(thisMM);
24799
+ otherNV.scene.pan2Dxyzmm = vec48.clone(this.scene.pan2Dxyzmm);
24800
+ }
24801
+ doSyncGamma(otherNV) {
24802
+ const thisGamma = this.scene.gamma;
24803
+ const otherGamma = otherNV.scene.gamma;
24804
+ if (thisGamma !== otherGamma) {
24805
+ otherNV.setGamma(thisGamma);
24806
+ }
24807
+ }
24808
+ doSyncZoomPan(otherNV) {
24809
+ otherNV.scene.pan2Dxyzmm = vec48.clone(this.scene.pan2Dxyzmm);
24810
+ }
24811
+ doSyncCrosshair(otherNV) {
24812
+ const thisMM = this.frac2mm(this.scene.crosshairPos);
24813
+ otherNV.scene.crosshairPos = otherNV.mm2frac(thisMM);
24814
+ }
24815
+ doSyncCalMin(otherNV) {
24816
+ if (this.volumes[0].cal_min !== otherNV.volumes[0].cal_min) {
24817
+ otherNV.volumes[0].cal_min = this.volumes[0].cal_min;
24818
+ otherNV.updateGLVolume();
24819
+ }
24820
+ }
24821
+ doSyncCalMax(otherNV) {
24822
+ if (this.volumes[0].cal_max !== otherNV.volumes[0].cal_max) {
24823
+ otherNV.volumes[0].cal_max = this.volumes[0].cal_max;
24824
+ otherNV.updateGLVolume();
24825
+ }
24826
+ }
24827
+ doSyncSliceType(otherNV) {
24828
+ otherNV.setSliceType(this.opts.sliceType);
24829
+ }
24830
+ doSyncClipPlane(otherNV) {
24831
+ otherNV.setClipPlane(this.scene.clipPlaneDepthAziElev);
24832
+ }
24619
24833
  /**
24620
24834
  * Sync the scene controls (orientation, crosshair location, etc.) from one Niivue instance to another. useful for using one canvas to drive another.
24621
24835
  * @internal
@@ -24632,46 +24846,39 @@ var Niivue = class _Niivue {
24632
24846
  if (!this.gl.canvas.matches(":focus")) {
24633
24847
  return;
24634
24848
  }
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);
24849
+ for (let i = 0; i < this.otherNV.length; i++) {
24850
+ if (this.otherNV[i] === this) {
24851
+ continue;
24852
+ }
24853
+ if (this.syncOpts.gamma) {
24854
+ this.doSyncGamma(this.otherNV[i]);
24855
+ }
24856
+ if (this.syncOpts.crosshair) {
24857
+ this.doSyncCrosshair(this.otherNV[i]);
24858
+ }
24859
+ if (this.syncOpts.zoomPan) {
24860
+ this.doSyncZoomPan(this.otherNV[i]);
24861
+ }
24862
+ if (this.syncOpts.sliceType) {
24863
+ this.doSyncSliceType(this.otherNV[i]);
24864
+ }
24865
+ if (this.syncOpts.cal_min) {
24866
+ this.doSyncCalMin(this.otherNV[i]);
24867
+ }
24868
+ if (this.syncOpts.cal_max) {
24869
+ this.doSyncCalMax(this.otherNV[i]);
24870
+ }
24871
+ if (this.syncOpts.clipPlane) {
24872
+ this.doSyncClipPlane(this.otherNV[i]);
24641
24873
  }
24642
24874
  if (this.syncOpts["2d"]) {
24643
- this.otherNV.scene.crosshairPos = this.otherNV.mm2frac(thisMM);
24644
- this.otherNV.scene.pan2Dxyzmm = vec48.clone(this.scene.pan2Dxyzmm);
24875
+ this.doSync2d(this.otherNV[i]);
24645
24876
  }
24646
24877
  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();
24878
+ this.doSync3d(this.otherNV[i]);
24674
24879
  }
24880
+ this.otherNV[i].drawScene();
24881
+ this.otherNV[i].createOnLocationChange();
24675
24882
  }
24676
24883
  }
24677
24884
  /** Not documented publicly for now
@@ -24766,6 +24973,8 @@ var Niivue = class _Niivue {
24766
24973
  this.drawPenLocation = [NaN, NaN, NaN];
24767
24974
  this.drawPenAxCorSag = -1;
24768
24975
  this.uiData.mousedown = true;
24976
+ this.setDragStart(0, 0);
24977
+ this.setDragEnd(0, 0);
24769
24978
  log.debug("mouse down");
24770
24979
  log.debug(e);
24771
24980
  const pos = this.getNoPaddingNoBorderCanvasRelativeMousePosition(e, this.gl.canvas);
@@ -24773,6 +24982,9 @@ var Niivue = class _Niivue {
24773
24982
  return;
24774
24983
  }
24775
24984
  const [x, y] = [pos.x * this.uiData.dpr, pos.y * this.uiData.dpr];
24985
+ if (this.opts.clickToSegment) {
24986
+ this.clickToSegmentXY = [x, y];
24987
+ }
24776
24988
  const label = this.getLabelAtPoint([x, y]);
24777
24989
  if (label) {
24778
24990
  if (label.onClick) {
@@ -24973,8 +25185,15 @@ var Niivue = class _Niivue {
24973
25185
  if (this.drawPenFillPts.length > 0) {
24974
25186
  this.drawPenFilled();
24975
25187
  } else if (this.drawPenAxCorSag >= 0) {
24976
- this.drawAddUndoBitmap();
25188
+ if (this.opts.clickToSegment) {
25189
+ const sumBitmap = this.sumBitmap(this.drawBitmap);
25190
+ const sumSeg = this.sumBitmap(this.clickToSegmentGrowingBitmap);
25191
+ if (sumSeg > sumBitmap) {
25192
+ this.updateBitmapFromClickToSegment();
25193
+ }
25194
+ }
24977
25195
  }
25196
+ this.drawAddUndoBitmap();
24978
25197
  this.drawPenLocation = [NaN, NaN, NaN];
24979
25198
  this.drawPenAxCorSag = -1;
24980
25199
  if (isFunction(this.onMouseUp)) {
@@ -24988,6 +25207,9 @@ var Niivue = class _Niivue {
24988
25207
  const fracStart = this.canvasPos2frac([this.uiData.dragStart[0], this.uiData.dragStart[1]]);
24989
25208
  const fracEnd = this.canvasPos2frac([this.uiData.dragEnd[0], this.uiData.dragEnd[1]]);
24990
25209
  this.generateMouseUpCallback(fracStart, fracEnd);
25210
+ if (this.opts.dragMode === 6 /* roiSelection */) {
25211
+ return;
25212
+ }
24991
25213
  if (this.opts.dragMode !== 1 /* contrast */) {
24992
25214
  return;
24993
25215
  }
@@ -25290,6 +25512,35 @@ var Niivue = class _Niivue {
25290
25512
  if (this.thumbnailVisible) {
25291
25513
  return;
25292
25514
  }
25515
+ const dragStartSum = this.uiData.dragStart.reduce((a, b) => a + b, 0);
25516
+ const dragEndSum = this.uiData.dragEnd.reduce((a, b) => a + b, 0);
25517
+ const validDrag = dragStartSum > 0 && dragEndSum > 0;
25518
+ if (this.opts.dragMode === 6 /* roiSelection */ && validDrag) {
25519
+ const delta = e.deltaY > 0 ? 1 : -1;
25520
+ if (this.uiData.dragStart[0] < this.uiData.dragEnd[0]) {
25521
+ this.uiData.dragStart[0] -= delta;
25522
+ this.uiData.dragEnd[0] += delta;
25523
+ } else {
25524
+ this.uiData.dragStart[0] += delta;
25525
+ this.uiData.dragEnd[0] -= delta;
25526
+ }
25527
+ if (this.uiData.dragStart[1] < this.uiData.dragEnd[1]) {
25528
+ this.uiData.dragStart[1] -= delta;
25529
+ this.uiData.dragEnd[1] += delta;
25530
+ } else {
25531
+ this.uiData.dragStart[1] += delta;
25532
+ this.uiData.dragEnd[1] -= delta;
25533
+ }
25534
+ this.uiData.isDragging = true;
25535
+ this.drawScene();
25536
+ this.uiData.isDragging = false;
25537
+ const tileIdx = this.tileIndex(this.uiData.dragStart[0], this.uiData.dragStart[1]);
25538
+ this.generateMouseUpCallback(
25539
+ this.screenXY2TextureFrac(this.uiData.dragStart[0], this.uiData.dragStart[1], tileIdx),
25540
+ this.screenXY2TextureFrac(this.uiData.dragEnd[0], this.uiData.dragEnd[1], tileIdx)
25541
+ );
25542
+ return;
25543
+ }
25293
25544
  const rect = this.canvas.getBoundingClientRect();
25294
25545
  if (e.deltaY < 0) {
25295
25546
  this.sliceScroll2D(-0.01, e.clientX - rect.left, e.clientY - rect.top);
@@ -25734,6 +25985,16 @@ var Niivue = class _Niivue {
25734
25985
  this.opts.isCornerOrientationText = isCornerOrientationText;
25735
25986
  this.updateGLVolume();
25736
25987
  }
25988
+ /**
25989
+ * determine proportion of screen real estate devoted to rendering in multiplanar view.
25990
+ * @param fraction - proportion of screen devoted to primary (hero) image (0 to disable)
25991
+ * @example niivue.setHeroImage(0.5)
25992
+ * @see {@link https://niivue.github.io/niivue/features/layout.html | live demo usage}
25993
+ */
25994
+ setHeroImage(fraction) {
25995
+ this.opts.heroImageFraction = fraction;
25996
+ this.drawScene();
25997
+ }
25737
25998
  /**
25738
25999
  * control whether 2D slices use radiological or neurological convention.
25739
26000
  * @param isRadiologicalConvention - new display convention
@@ -26783,6 +27044,10 @@ var Niivue = class _Niivue {
26783
27044
  }
26784
27045
  // not included in public docs
26785
27046
  sliceScroll2D(posChange, x, y, isDelta = true) {
27047
+ if (this.opts.scrollRequiresFocus && this.canvas !== document.activeElement) {
27048
+ log.warn("Canvas element does not have focus. Scroll events will not be processed.");
27049
+ return;
27050
+ }
26786
27051
  if (this.inGraphTile(x, y)) {
26787
27052
  let vol = this.volumes[0].frame4D;
26788
27053
  if (posChange > 0) {
@@ -27474,6 +27739,7 @@ var Niivue = class _Niivue {
27474
27739
  }
27475
27740
  const vx = this.back.dims[1] * this.back.dims[2] * this.back.dims[3];
27476
27741
  this.drawBitmap = new Uint8Array(vx);
27742
+ this.clickToSegmentGrowingBitmap = new Uint8Array(vx);
27477
27743
  this.drawClearAllUndoBitmaps();
27478
27744
  this.drawAddUndoBitmap();
27479
27745
  this.drawTexture = this.r8Tex(this.drawTexture, TEXTURE7_DRAW, this.back.dims, true);
@@ -27647,6 +27913,23 @@ var Niivue = class _Niivue {
27647
27913
  y = Math.min(Math.max(y, 0), dy - 1);
27648
27914
  z = Math.min(Math.max(z, 0), dz - 1);
27649
27915
  this.drawBitmap[x + y * dx + z * dx * dy] = penValue;
27916
+ const isAx = this.drawPenAxCorSag === 0;
27917
+ const isCor = this.drawPenAxCorSag === 1;
27918
+ const isSag = this.drawPenAxCorSag === 2;
27919
+ if (this.opts.penSize > 1) {
27920
+ const halfPenSize = Math.floor(this.opts.penSize / 2);
27921
+ for (let i = -halfPenSize; i <= halfPenSize; i++) {
27922
+ for (let j = -halfPenSize; j <= halfPenSize; j++) {
27923
+ if (isAx) {
27924
+ this.drawBitmap[x + i + (y + j) * dx + z * dx * dy] = penValue;
27925
+ } else if (isCor) {
27926
+ this.drawBitmap[x + i + y * dx + (z + j) * dx * dy] = penValue;
27927
+ } else if (isSag) {
27928
+ this.drawBitmap[x + (y + j) * dx + (z + i) * dx * dy] = penValue;
27929
+ }
27930
+ }
27931
+ }
27932
+ }
27650
27933
  }
27651
27934
  // not included in public docs
27652
27935
  // create line between to voxels in drawing
@@ -27728,6 +28011,98 @@ var Niivue = class _Niivue {
27728
28011
  }
27729
28012
  }
27730
28013
  }
28014
+ /**
28015
+ * Performs a 1-voxel binary dilation on a connected cluster within the drawing mask using the drawFloodFillCore function.
28016
+ *
28017
+ * @param seedXYZ - voxel index of the seed voxel in the mask array.
28018
+ * @param neighbors - Number of neighbors to consider for connectivity and dilation (6, 18, or 26).
28019
+ */
28020
+ drawingBinaryDilationWithSeed(seedXYZ, neighbors = 6) {
28021
+ try {
28022
+ let xyz2vx = function(pt) {
28023
+ return pt[0] + pt[1] * nx + pt[2] * nxy;
28024
+ };
28025
+ const mask = this.drawBitmap;
28026
+ const xDim = this.back.dims[1];
28027
+ const yDim = this.back.dims[2];
28028
+ const zDim = this.back.dims[3];
28029
+ const nx = xDim;
28030
+ const nxy = xDim * yDim;
28031
+ const totalVoxels = nxy * zDim;
28032
+ const seedIndex = xyz2vx(seedXYZ);
28033
+ if (seedIndex < 0 || seedIndex >= totalVoxels) {
28034
+ throw new Error("Seed index is out of bounds.");
28035
+ }
28036
+ const seedValue = mask[seedIndex];
28037
+ if (seedValue === 0) {
28038
+ throw new Error("Seed voxel is not part of a filled cluster.");
28039
+ }
28040
+ const img = mask.slice();
28041
+ for (let i = 0; i < totalVoxels; i++) {
28042
+ img[i] = img[i] === seedValue ? 1 : 0;
28043
+ }
28044
+ this.drawFloodFillCore(img, seedIndex, neighbors);
28045
+ const outputMask = mask.slice();
28046
+ const neighborOffsets = [];
28047
+ const offsets6 = [-nxy, nxy, -xDim, xDim, -1, 1];
28048
+ neighborOffsets.push(...offsets6);
28049
+ if (neighbors > 6) {
28050
+ neighborOffsets.push(
28051
+ -xDim - 1,
28052
+ -xDim + 1,
28053
+ xDim - 1,
28054
+ xDim + 1,
28055
+ -nxy - xDim,
28056
+ -nxy + xDim,
28057
+ -nxy - 1,
28058
+ -nxy + 1,
28059
+ nxy - xDim,
28060
+ nxy + xDim,
28061
+ nxy - 1,
28062
+ nxy + 1
28063
+ );
28064
+ }
28065
+ if (neighbors > 18) {
28066
+ neighborOffsets.push(
28067
+ -nxy - xDim - 1,
28068
+ -nxy - xDim + 1,
28069
+ -nxy + xDim - 1,
28070
+ -nxy + xDim + 1,
28071
+ nxy - xDim - 1,
28072
+ nxy - xDim + 1,
28073
+ nxy + xDim - 1,
28074
+ nxy + xDim + 1
28075
+ );
28076
+ }
28077
+ for (let idx = 0; idx < totalVoxels; idx++) {
28078
+ if (img[idx] === 2) {
28079
+ const x = idx % xDim;
28080
+ const y = Math.floor(idx % nxy / xDim);
28081
+ const z = Math.floor(idx / nxy);
28082
+ for (const offset of neighborOffsets) {
28083
+ const neighborIdx = idx + offset;
28084
+ if (neighborIdx < 0 || neighborIdx >= totalVoxels) {
28085
+ continue;
28086
+ }
28087
+ const nx2 = neighborIdx % xDim;
28088
+ const ny = Math.floor(neighborIdx % nxy / xDim);
28089
+ const nz = Math.floor(neighborIdx / nxy);
28090
+ if (Math.abs(nx2 - x) > 1 || Math.abs(ny - y) > 1 || Math.abs(nz - z) > 1) {
28091
+ continue;
28092
+ }
28093
+ if (mask[neighborIdx] === 0) {
28094
+ outputMask[neighborIdx] = seedValue;
28095
+ }
28096
+ }
28097
+ }
28098
+ }
28099
+ this.drawBitmap = outputMask;
28100
+ this.drawAddUndoBitmap();
28101
+ this.refreshDrawing(true);
28102
+ } catch (error) {
28103
+ log.error("Error in drawingBinaryDilationWithSeed:", error);
28104
+ }
28105
+ }
27731
28106
  // a voxel can be defined as having 6, 18 or 26 neighbors:
27732
28107
  // 6: neighbors share faces (distance=1)
27733
28108
  // 18: neighbors share faces (distance=1) or edges (1.4)
@@ -27809,7 +28184,7 @@ var Niivue = class _Niivue {
27809
28184
  }
27810
28185
  // not included in public docs
27811
28186
  // set all connected voxels in drawing to new color
27812
- drawFloodFill(seedXYZ, newColor = 0, growSelectedCluster = 0, forceMin = NaN, forceMax = NaN, neighbors = 6) {
28187
+ drawFloodFill(seedXYZ, newColor = 0, growSelectedCluster = 0, forceMin = NaN, forceMax = NaN, neighbors = 6, maxDistanceMM = Number.POSITIVE_INFINITY, is2D = false) {
27813
28188
  if (!this.drawBitmap) {
27814
28189
  throw new Error("drawBitmap undefined");
27815
28190
  }
@@ -27827,13 +28202,46 @@ var Niivue = class _Niivue {
27827
28202
  const nx = dims[0];
27828
28203
  const nxy = nx * dims[1];
27829
28204
  const nxyz = nxy * dims[2];
27830
- const img = this.drawBitmap.slice();
28205
+ let img = this.drawBitmap.slice();
28206
+ let drawBitmap = this.drawBitmap;
28207
+ if (this.clickToSegmentIsGrowing) {
28208
+ img = this.clickToSegmentGrowingBitmap.slice();
28209
+ drawBitmap = this.clickToSegmentGrowingBitmap;
28210
+ }
27831
28211
  if (img.length !== nxy * dims[2]) {
27832
28212
  return;
27833
28213
  }
28214
+ let constrainXYZ = -1;
28215
+ if (is2D && this.drawPenAxCorSag === 0 /* AXIAL */) {
28216
+ constrainXYZ = 2;
28217
+ } else if (is2D && this.drawPenAxCorSag === 1 /* CORONAL */) {
28218
+ constrainXYZ = 1;
28219
+ } else if (is2D && this.drawPenAxCorSag === 2 /* SAGITTAL */) {
28220
+ constrainXYZ = 0;
28221
+ }
28222
+ function vx2xyz(vx) {
28223
+ const Z = Math.floor(vx / nxy);
28224
+ const Y = Math.floor((vx - Z * nxy) / nx);
28225
+ const X = Math.floor(vx % nx);
28226
+ return [X, Y, Z];
28227
+ }
27834
28228
  function xyz2vx(pt) {
27835
28229
  return pt[0] + pt[1] * nx + pt[2] * nxy;
27836
28230
  }
28231
+ const vx2mm = (xyz) => {
28232
+ return this.vox2mm(xyz, this.back.matRAS);
28233
+ };
28234
+ const seedMM = vx2mm(seedXYZ);
28235
+ const maxDistanceMM2 = maxDistanceMM ** 2;
28236
+ function isWithinDistance(vx) {
28237
+ const xzyVox = vx2xyz(vx);
28238
+ if (constrainXYZ >= 0 && xzyVox[constrainXYZ] !== seedXYZ[constrainXYZ]) {
28239
+ return false;
28240
+ }
28241
+ const xyzMM = vx2mm(xzyVox);
28242
+ const dist2 = (xyzMM[0] - seedMM[0]) ** 2 + (xyzMM[1] - seedMM[1]) ** 2 + (xyzMM[2] - seedMM[2]) ** 2;
28243
+ return dist2 <= maxDistanceMM2;
28244
+ }
27837
28245
  const seedVx = xyz2vx(seedXYZ);
27838
28246
  const seedColor = img[seedVx];
27839
28247
  if (seedColor === newColor) {
@@ -27846,7 +28254,10 @@ var Niivue = class _Niivue {
27846
28254
  }
27847
28255
  for (let i = 1; i < nxyz; i++) {
27848
28256
  img[i] = 0;
27849
- if (this.drawBitmap[i] === seedColor) {
28257
+ if (drawBitmap[i] === seedColor) {
28258
+ if (!isWithinDistance(i)) {
28259
+ continue;
28260
+ }
27850
28261
  img[i] = 1;
27851
28262
  }
27852
28263
  }
@@ -27876,6 +28287,9 @@ var Niivue = class _Niivue {
27876
28287
  for (let i = 1; i < nxyz; i++) {
27877
28288
  img[i] = 0;
27878
28289
  if (backImg[i] >= mn && backImg[i] <= mx) {
28290
+ if (!isWithinDistance(i)) {
28291
+ continue;
28292
+ }
27879
28293
  img[i] = 1;
27880
28294
  }
27881
28295
  }
@@ -27884,11 +28298,19 @@ var Niivue = class _Niivue {
27884
28298
  }
27885
28299
  for (let i = 1; i < nxyz; i++) {
27886
28300
  if (img[i] === 2) {
27887
- this.drawBitmap[i] = newColor;
28301
+ drawBitmap[i] = newColor;
27888
28302
  }
27889
28303
  }
27890
- this.drawAddUndoBitmap();
27891
- this.refreshDrawing(false);
28304
+ if (!this.clickToSegmentIsGrowing) {
28305
+ this.drawBitmap = drawBitmap.slice();
28306
+ if (!this.opts.clickToSegment) {
28307
+ this.drawAddUndoBitmap();
28308
+ }
28309
+ this.refreshDrawing(true, this.clickToSegmentIsGrowing);
28310
+ } else {
28311
+ this.clickToSegmentGrowingBitmap = drawBitmap;
28312
+ this.refreshDrawing(true, this.clickToSegmentIsGrowing);
28313
+ }
27892
28314
  }
27893
28315
  // not included in public docs
27894
28316
  // given series of line segments, connect first and last
@@ -28048,6 +28470,7 @@ var Niivue = class _Niivue {
28048
28470
  this.drawClearAllUndoBitmaps();
28049
28471
  this.rgbaTex(this.drawTexture, TEXTURE7_DRAW, [2, 2, 2, 2], true);
28050
28472
  this.drawBitmap = null;
28473
+ this.clickToSegmentGrowingBitmap = null;
28051
28474
  this.drawScene();
28052
28475
  }
28053
28476
  /**
@@ -28056,7 +28479,7 @@ var Niivue = class _Niivue {
28056
28479
  * @example niivue.refreshDrawing();
28057
28480
  * @see {@link https://niivue.github.io/niivue/features/cactus.html | live demo usage}
28058
28481
  */
28059
- refreshDrawing(isForceRedraw = true) {
28482
+ refreshDrawing(isForceRedraw = true, useClickToSegmentBitmap = false) {
28060
28483
  if (!this.back?.dims) {
28061
28484
  throw new Error("back.dims undefined");
28062
28485
  }
@@ -28085,7 +28508,7 @@ var Niivue = class _Niivue {
28085
28508
  dims[3],
28086
28509
  this.gl.RED,
28087
28510
  this.gl.UNSIGNED_BYTE,
28088
- this.drawBitmap
28511
+ useClickToSegmentBitmap ? this.clickToSegmentGrowingBitmap : this.drawBitmap
28089
28512
  );
28090
28513
  if (isForceRedraw) {
28091
28514
  this.drawScene();
@@ -28497,6 +28920,8 @@ var Niivue = class _Niivue {
28497
28920
  gl.bindVertexArray(this.unusedVAO);
28498
28921
  this.rectShader = new Shader(gl, vertRectShader, fragRectShader);
28499
28922
  this.rectShader.use(gl);
28923
+ this.rectOutlineShader = new Shader(gl, vertRectShader, fragRectOutlineShader);
28924
+ this.rectOutlineShader.use(gl);
28500
28925
  this.lineShader = new Shader(gl, vertLineShader, fragRectShader);
28501
28926
  this.lineShader.use(gl);
28502
28927
  this.line3DShader = new Shader(gl, vertLine3DShader, fragRectShader);
@@ -28649,14 +29074,37 @@ var Niivue = class _Niivue {
28649
29074
  }
28650
29075
  /**
28651
29076
  * 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);
29077
+ * @param options - an object containing the following properties:
29078
+ * - layer: selects image to describe
29079
+ * - masks: optional binary images to filter voxels
29080
+ * - drawingIsMask: a boolean indicating if the drawing is used as a mask
29081
+ * - roiIsMask: a boolean indicating if the ROI is used as a mask
29082
+ * - startVox: the starting voxel coordinates
29083
+ * - endVox: the ending voxel coordinates
29084
+ * @returns numeric values to describe image or regions of images
29085
+ * @example
29086
+ * niivue.getDescriptives({
29087
+ * layer: 0,
29088
+ * masks: [],
29089
+ * drawingIsMask: true, // drawingIsMask and roiIsMask are mutually exclusive
29090
+ * roiIsMask: false,
29091
+ * startVox: [10, 20, 30], // ignored if roiIsMask is false
29092
+ * endVox: [40, 50, 60] // ignored if roiIsMask is false
29093
+ * });
28656
29094
  * @see {@link https://niivue.github.io/niivue/features/draw2.html | live demo usage}
28657
29095
  */
28658
- getDescriptives(layer = 0, masks = [], drawingIsMask = false) {
29096
+ getDescriptives(options) {
29097
+ const {
29098
+ layer = 0,
29099
+ masks = [],
29100
+ drawingIsMask = false,
29101
+ roiIsMask = false,
29102
+ startVox = [0, 0, 0],
29103
+ endVox = [0, 0, 0]
29104
+ } = options;
29105
+ let area = null;
28659
29106
  const hdr = this.volumes[layer].hdr;
29107
+ const pixDimsRAS = this.volumes[layer].pixDimsRAS;
28660
29108
  let slope = hdr.scl_slope;
28661
29109
  if (isNaN(slope)) {
28662
29110
  slope = 1;
@@ -28694,6 +29142,65 @@ var Niivue = class _Niivue {
28694
29142
  mask[i] = 0;
28695
29143
  }
28696
29144
  }
29145
+ } else if (masks.length < 1 && roiIsMask) {
29146
+ mask.fill(0);
29147
+ console.log("startVox", startVox);
29148
+ console.log("endVox", endVox);
29149
+ let constantDim = -1;
29150
+ if (startVox[0] === endVox[0]) {
29151
+ constantDim = 0;
29152
+ } else if (startVox[1] === endVox[1]) {
29153
+ constantDim = 1;
29154
+ } else if (startVox[2] === endVox[2]) {
29155
+ constantDim = 2;
29156
+ } else {
29157
+ console.error("Error: No constant dimension found.");
29158
+ return;
29159
+ }
29160
+ const dims = [0, 1, 2];
29161
+ const varDims = dims.filter((dim) => dim !== constantDim);
29162
+ const centerVox = [];
29163
+ centerVox[constantDim] = startVox[constantDim];
29164
+ centerVox[varDims[0]] = (startVox[varDims[0]] + endVox[varDims[0]]) / 2;
29165
+ centerVox[varDims[1]] = (startVox[varDims[1]] + endVox[varDims[1]]) / 2;
29166
+ const radiusX = Math.abs(endVox[varDims[0]] - startVox[varDims[0]]) / 2;
29167
+ const radiusY = Math.abs(endVox[varDims[1]] - startVox[varDims[1]]) / 2;
29168
+ const xdim = hdr.dims[1];
29169
+ const ydim = hdr.dims[2];
29170
+ const minVarDim0 = Math.max(0, Math.floor(centerVox[varDims[0]] - radiusX));
29171
+ const maxVarDim0 = Math.min(hdr.dims[varDims[0] + 1] - 1, Math.ceil(centerVox[varDims[0]] + radiusX));
29172
+ const minVarDim1 = Math.max(0, Math.floor(centerVox[varDims[1]] - radiusY));
29173
+ const maxVarDim1 = Math.min(hdr.dims[varDims[1] + 1] - 1, Math.ceil(centerVox[varDims[1]] + radiusY));
29174
+ const constDimVal = centerVox[constantDim];
29175
+ if (constDimVal < 0 || constDimVal >= hdr.dims[constantDim + 1]) {
29176
+ console.error("Error: Constant dimension value is out of bounds.");
29177
+ return;
29178
+ }
29179
+ for (let i = minVarDim0; i <= maxVarDim0; i++) {
29180
+ for (let j = minVarDim1; j <= maxVarDim1; j++) {
29181
+ const voxel = [];
29182
+ voxel[constantDim] = constDimVal;
29183
+ voxel[varDims[0]] = i;
29184
+ voxel[varDims[1]] = j;
29185
+ const di = (voxel[varDims[0]] - centerVox[varDims[0]]) / radiusX;
29186
+ const dj = (voxel[varDims[1]] - centerVox[varDims[1]]) / radiusY;
29187
+ const distSq = di * di + dj * dj;
29188
+ if (distSq <= 1) {
29189
+ const x = voxel[0];
29190
+ const y = voxel[1];
29191
+ const z = voxel[2];
29192
+ const index = z * xdim * ydim + y * xdim + x;
29193
+ mask[index] = 1;
29194
+ }
29195
+ }
29196
+ }
29197
+ const voxelArea = pixDimsRAS[varDims[0] + 1] * pixDimsRAS[varDims[1] + 1];
29198
+ const numMaskedVoxels = mask.reduce((count, value) => count + (value === 1 ? 1 : 0), 0);
29199
+ area = numMaskedVoxels * voxelArea;
29200
+ const radiusX_mm = radiusX * pixDimsRAS[varDims[0] + 1];
29201
+ const radiusY_mm = radiusY * pixDimsRAS[varDims[1] + 1];
29202
+ const areaEllipse = Math.PI * radiusX_mm * radiusY_mm;
29203
+ area = areaEllipse;
28697
29204
  }
28698
29205
  let k = 0;
28699
29206
  let M = 0;
@@ -28747,7 +29254,8 @@ var Niivue = class _Niivue {
28747
29254
  cal_min: this.volumes[layer].cal_min,
28748
29255
  cal_max: this.volumes[layer].cal_max,
28749
29256
  robust_min: this.volumes[layer].robust_min,
28750
- robust_max: this.volumes[layer].robust_max
29257
+ robust_max: this.volumes[layer].robust_max,
29258
+ area
28751
29259
  };
28752
29260
  }
28753
29261
  // not included in public docs
@@ -30225,6 +30733,29 @@ var Niivue = class _Niivue {
30225
30733
  const pos = [(x - this.graph.LTWH[0]) / this.graph.LTWH[2], (y - this.graph.LTWH[1]) / this.graph.LTWH[3]];
30226
30734
  return pos[0] > 0 && pos[1] > 0 && pos[0] <= 1 && pos[1] <= 1;
30227
30735
  }
30736
+ // update drawBitmap if it differs from clickTosegmentBitmap
30737
+ updateBitmapFromClickToSegment() {
30738
+ if (this.clickToSegmentGrowingBitmap === null) {
30739
+ return;
30740
+ }
30741
+ if (this.drawBitmap === null) {
30742
+ return;
30743
+ }
30744
+ if (this.clickToSegmentGrowingBitmap.length !== this.drawBitmap.length) {
30745
+ return;
30746
+ }
30747
+ const nvx = this.drawBitmap.length;
30748
+ for (let i = 0; i < nvx; i++) {
30749
+ this.drawBitmap[i] = this.clickToSegmentGrowingBitmap[i];
30750
+ }
30751
+ }
30752
+ sumBitmap(img) {
30753
+ let sum = 0;
30754
+ for (let i = 0; i < img.length; i++) {
30755
+ sum += img[i];
30756
+ }
30757
+ return sum;
30758
+ }
30228
30759
  // not included in public docs
30229
30760
  // handle mouse click event on canvas
30230
30761
  mouseClick(x, y, posChange = 0, isDelta = true) {
@@ -30274,7 +30805,7 @@ var Niivue = class _Niivue {
30274
30805
  if (axCorSag > 2 /* SAGITTAL */) {
30275
30806
  continue;
30276
30807
  }
30277
- const texFrac = this.screenXY2TextureFrac(x, y, i, false);
30808
+ let texFrac = this.screenXY2TextureFrac(x, y, i, false);
30278
30809
  if (texFrac[0] < 0) {
30279
30810
  continue;
30280
30811
  }
@@ -30301,31 +30832,75 @@ var Niivue = class _Niivue {
30301
30832
  this.scene.crosshairPos = vec311.clone(texFrac);
30302
30833
  }
30303
30834
  if (this.opts.drawingEnabled) {
30304
- const pt = this.frac2vox(this.scene.crosshairPos);
30305
- const ptMM = this.frac2mm(this.scene.crosshairPos);
30835
+ let pt = this.frac2vox(this.scene.crosshairPos);
30306
30836
  if (this.opts.clickToSegment) {
30307
- const radius = this.opts.clickToSegmentRadius;
30308
- const steps = this.opts.clickToSegmentSteps;
30837
+ texFrac = this.screenXY2TextureFrac(this.clickToSegmentXY[0], this.clickToSegmentXY[1], i, false);
30838
+ pt = this.frac2vox(texFrac);
30839
+ let diff = 0;
30840
+ let threshold = this.opts.clickToSegmentPercent + diff;
30841
+ if (this.uiData.mousedown) {
30842
+ const xDiff = (this.clickToSegmentXY[0] - x) / this.gl.canvas.width;
30843
+ const yDiff = (this.clickToSegmentXY[1] - y) / this.gl.canvas.height;
30844
+ diff = Math.max(Math.abs(xDiff), Math.abs(yDiff));
30845
+ threshold = this.opts.clickToSegmentPercent + diff;
30846
+ }
30847
+ let voxelIntensity = this.back.getValue(pt[0], pt[1], pt[2]);
30848
+ if (this.opts.clickToSegmentAutoIntensity) {
30849
+ if (threshold !== 0) {
30850
+ if (voxelIntensity === 0) {
30851
+ voxelIntensity = 0.01;
30852
+ }
30853
+ this.opts.clickToSegmentIntensityMax = voxelIntensity * (1 + threshold);
30854
+ this.opts.clickToSegmentIntensityMin = voxelIntensity * (1 - threshold);
30855
+ }
30856
+ if (voxelIntensity > (this.back.cal_min + this.back.cal_max) * 0.5) {
30857
+ this.opts.clickToSegmentBright = true;
30858
+ } else {
30859
+ this.opts.clickToSegmentBright = false;
30860
+ }
30861
+ }
30309
30862
  const brightOrDark = this.opts.clickToSegmentBright ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
30310
- this.drawPenFillPts = [];
30311
30863
  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
- }
30864
+ if (this.drawPenAxCorSag === 0 /* AXIAL */) {
30865
+ const pixDims = [this.back.pixDimsRAS[0], this.back.pixDimsRAS[1]];
30866
+ const minPixDim = Math.min(...pixDims);
30867
+ this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim);
30868
+ } else if (this.drawPenAxCorSag === 1 /* CORONAL */) {
30869
+ const pixDims = [this.back.pixDimsRAS[0], this.back.pixDimsRAS[2]];
30870
+ const minPixDim = Math.min(...pixDims);
30871
+ this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim);
30872
+ } else if (this.drawPenAxCorSag === 2 /* SAGITTAL */) {
30873
+ const pixDims = [this.back.pixDimsRAS[1], this.back.pixDimsRAS[2]];
30874
+ const minPixDim = Math.min(...pixDims);
30875
+ this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim);
30325
30876
  }
30877
+ this.drawPt(pt[0], pt[1], pt[2], this.opts.penValue);
30878
+ if (diff !== 0) {
30879
+ this.clickToSegmentIsGrowing = true;
30880
+ this.clickToSegmentGrowingBitmap = this.drawBitmap.slice();
30881
+ } else {
30882
+ this.clickToSegmentIsGrowing = false;
30883
+ }
30884
+ this.drawFloodFill(
30885
+ [pt[0], pt[1], pt[2]],
30886
+ 0,
30887
+ brightOrDark,
30888
+ this.opts.clickToSegmentIntensityMin,
30889
+ this.opts.clickToSegmentIntensityMax,
30890
+ this.opts.floodFillNeighbors,
30891
+ this.opts.clickToSegmentMaxDistanceMM,
30892
+ this.opts.clickToSegmentIs2D
30893
+ );
30326
30894
  this.drawScene();
30327
30895
  this.createOnLocationChange(axCorSag);
30328
- const info = this.getDescriptives(0, [], true);
30896
+ if (this.clickToSegmentIsGrowing) {
30897
+ return;
30898
+ }
30899
+ const info = this.getDescriptives({
30900
+ layer: 0,
30901
+ masks: [],
30902
+ drawingIsMask: true
30903
+ });
30329
30904
  this.onClickToSegment({ mL: info.volumeML, mm3: info.volumeMM3 });
30330
30905
  return;
30331
30906
  }
@@ -30385,30 +30960,39 @@ var Niivue = class _Niivue {
30385
30960
  }
30386
30961
  const frac10cm = 100 / fovMM[0];
30387
30962
  const pix10cm = frac10cm * ltwh[2];
30388
- const pixLeft = ltwh[0] + 0.5 * ltwh[2] - 0.5 * pix10cm;
30389
- const pixTop = ltwh[1] + ltwh[3] - 2 * this.opts.rulerWidth;
30963
+ const pix1cm = Math.max(Math.round(pix10cm * 0.1), 2);
30964
+ const pixLeft = Math.floor(ltwh[0] + 0.5 * ltwh[2] - 0.5 * pix10cm);
30965
+ const thick = Number(this.opts.rulerWidth);
30966
+ const pixTop = Math.floor(ltwh[1] + ltwh[3] - pix1cm) + 0.5 * thick;
30390
30967
  const startXYendXY = [pixLeft, pixTop, pixLeft + pix10cm, pixTop];
30391
- this.drawRuler10cm(startXYendXY);
30968
+ let outlineColor = [0, 0, 0, 1];
30969
+ if (this.opts.rulerColor[0] + this.opts.rulerColor[1] + this.opts.rulerColor[2] < 0.8) {
30970
+ outlineColor = [1, 1, 1, 1];
30971
+ }
30972
+ this.drawRuler10cm(startXYendXY, outlineColor, thick + 1);
30973
+ this.drawRuler10cm(startXYendXY, this.opts.rulerColor, thick);
30392
30974
  }
30393
30975
  // not included in public docs
30394
30976
  // draw 10cm ruler at desired coordinates
30395
- drawRuler10cm(startXYendXY) {
30977
+ drawRuler10cm(startXYendXY, rulerColor, rulerWidth = 1) {
30396
30978
  if (!this.lineShader) {
30397
30979
  throw new Error("lineShader undefined");
30398
30980
  }
30399
30981
  this.gl.bindVertexArray(this.genericVAO);
30400
30982
  this.lineShader.use(this.gl);
30401
- this.gl.uniform4fv(this.lineShader.uniforms.lineColor, this.opts.rulerColor);
30983
+ this.gl.uniform4fv(this.lineShader.uniforms.lineColor, rulerColor);
30402
30984
  this.gl.uniform2fv(this.lineShader.uniforms.canvasWidthHeight, [this.gl.canvas.width, this.gl.canvas.height]);
30403
- this.gl.uniform1f(this.lineShader.uniforms.thickness, this.opts.rulerWidth);
30985
+ this.gl.uniform1f(this.lineShader.uniforms.thickness, rulerWidth);
30404
30986
  this.gl.uniform4fv(this.lineShader.uniforms.startXYendXY, startXYendXY);
30405
30987
  this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
30406
30988
  const w1cm = -0.1 * (startXYendXY[0] - startXYendXY[2]);
30407
- const b = startXYendXY[1];
30408
- const t = b - 2 * this.opts.rulerWidth;
30409
- const t2 = b - 4 * this.opts.rulerWidth;
30989
+ const b = startXYendXY[1] - Math.floor(0.5 * this.opts.rulerWidth);
30990
+ const t = Math.floor(b - 0.35 * w1cm);
30991
+ const t2 = Math.floor(b - 0.7 * w1cm);
30410
30992
  for (let i = 0; i < 11; i++) {
30411
- const l = startXYendXY[0] + i * w1cm;
30993
+ let l = startXYendXY[0] + i * w1cm;
30994
+ l = Math.max(l, startXYendXY[0] + 0.5 * rulerWidth);
30995
+ l = Math.min(l, startXYendXY[2] - 0.5 * rulerWidth);
30412
30996
  const xyxy = [l, b, l, t];
30413
30997
  if (i % 5 === 0) {
30414
30998
  xyxy[3] = t2;
@@ -30486,6 +31070,24 @@ var Niivue = class _Niivue {
30486
31070
  // not included in public docs
30487
31071
  // draw line between start/end points and text to report length
30488
31072
  drawMeasurementTool(startXYendXY) {
31073
+ function extendTo(x0, y0, x1, y1, distance) {
31074
+ const x = x0 - x1;
31075
+ const y = y0 - y1;
31076
+ if (x === 0 && y === 0) {
31077
+ return {
31078
+ origin: [x1 + distance, y1],
31079
+ terminus: [x1 + distance, y1]
31080
+ };
31081
+ }
31082
+ const c = Math.sqrt(x * x + y * y);
31083
+ const dX = distance * x / c;
31084
+ const dY = distance * y / c;
31085
+ return {
31086
+ origin: [x0 + dX, y0 + dY],
31087
+ // next to start point
31088
+ terminus: [x1 - dX, y1 - dY]
31089
+ };
31090
+ }
30489
31091
  const gl = this.gl;
30490
31092
  gl.bindVertexArray(this.genericVAO);
30491
31093
  gl.depthFunc(gl.ALWAYS);
@@ -30500,9 +31102,9 @@ var Niivue = class _Niivue {
30500
31102
  gl.uniform1f(this.lineShader.uniforms.thickness, this.opts.rulerWidth);
30501
31103
  gl.uniform4fv(this.lineShader.uniforms.startXYendXY, startXYendXY);
30502
31104
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
30503
- const color = this.opts.rulerColor;
30504
- color[3] = 1;
30505
- gl.uniform4fv(this.lineShader.uniforms.lineColor, color);
31105
+ const measureLineColor = this.opts.measureLineColor;
31106
+ measureLineColor[3] = 1;
31107
+ gl.uniform4fv(this.lineShader.uniforms.lineColor, measureLineColor);
30506
31108
  const w = this.opts.rulerWidth;
30507
31109
  gl.uniform1f(this.lineShader.uniforms.thickness, w * 2);
30508
31110
  let sXYeXY = [startXYendXY[0], startXYendXY[1] - w, startXYendXY[0], startXYendXY[1] + w];
@@ -30528,8 +31130,30 @@ var Niivue = class _Niivue {
30528
31130
  if (lenMM > 99) {
30529
31131
  decimals = 0;
30530
31132
  }
30531
- const stringMM = lenMM.toFixed(decimals);
30532
- this.drawTextBetween(startXYendXY, stringMM, 1, color);
31133
+ let stringMM = lenMM.toFixed(decimals);
31134
+ if (this.opts.showMeasureUnits) {
31135
+ stringMM = `${stringMM} mm`;
31136
+ }
31137
+ let textCoords = startXYendXY;
31138
+ const [x0, y0, x1, y1] = startXYendXY;
31139
+ const { origin, terminus } = extendTo(x0, y0, x1, y1, 30);
31140
+ switch (this.opts.measureTextJustify) {
31141
+ case "start":
31142
+ textCoords = [...origin, ...origin.map((point) => point + 1)];
31143
+ break;
31144
+ case "end":
31145
+ textCoords = textCoords = [...terminus, ...terminus.map((point) => point + 1)];
31146
+ break;
31147
+ default:
31148
+ textCoords = startXYendXY;
31149
+ break;
31150
+ }
31151
+ this.drawTextBetween(
31152
+ textCoords,
31153
+ stringMM,
31154
+ this.opts.measureTextHeight / this.opts.textHeight,
31155
+ this.opts.measureTextColor
31156
+ );
30533
31157
  }
30534
31158
  gl.bindVertexArray(this.unusedVAO);
30535
31159
  }
@@ -30543,20 +31167,42 @@ var Niivue = class _Niivue {
30543
31167
  if (!this.rectShader) {
30544
31168
  throw new Error("rectShader undefined");
30545
31169
  }
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);
31170
+ if (!this.opts.selectionBoxIsOutline) {
31171
+ this.rectShader.use(this.gl);
31172
+ this.gl.enable(this.gl.BLEND);
31173
+ this.gl.uniform4fv(this.rectShader.uniforms.lineColor, lineColor);
31174
+ this.gl.uniform2fv(this.rectShader.uniforms.canvasWidthHeight, [this.gl.canvas.width, this.gl.canvas.height]);
31175
+ this.gl.uniform4f(
31176
+ this.rectShader.uniforms.leftTopWidthHeight,
31177
+ leftTopWidthHeight[0],
31178
+ leftTopWidthHeight[1],
31179
+ leftTopWidthHeight[2],
31180
+ leftTopWidthHeight[3]
31181
+ );
31182
+ this.gl.bindVertexArray(this.genericVAO);
31183
+ this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
31184
+ this.gl.bindVertexArray(this.unusedVAO);
31185
+ } else {
31186
+ this.drawCircle(leftTopWidthHeight, lineColor, 0.1);
31187
+ this.rectOutlineShader.use(this.gl);
31188
+ this.gl.enable(this.gl.BLEND);
31189
+ this.gl.uniform1f(this.rectOutlineShader.uniforms.thickness, this.opts.selectionBoxLineThickness);
31190
+ this.gl.uniform4fv(this.rectOutlineShader.uniforms.lineColor, lineColor);
31191
+ this.gl.uniform2fv(this.rectOutlineShader.uniforms.canvasWidthHeight, [
31192
+ this.gl.canvas.width,
31193
+ this.gl.canvas.height
31194
+ ]);
31195
+ this.gl.uniform4f(
31196
+ this.rectOutlineShader.uniforms.leftTopWidthHeight,
31197
+ leftTopWidthHeight[0],
31198
+ leftTopWidthHeight[1],
31199
+ leftTopWidthHeight[2],
31200
+ leftTopWidthHeight[3]
31201
+ );
31202
+ this.gl.bindVertexArray(this.genericVAO);
31203
+ this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
31204
+ this.gl.bindVertexArray(this.unusedVAO);
31205
+ }
30560
31206
  }
30561
31207
  drawCircle(leftTopWidthHeight, circleColor = this.opts.fontColor, fillPercent = 1) {
30562
31208
  if (!this.circleShader) {
@@ -30582,6 +31228,10 @@ var Niivue = class _Niivue {
30582
31228
  // not included in public docs
30583
31229
  // draw a rectangle at desired location
30584
31230
  drawSelectionBox(leftTopWidthHeight) {
31231
+ if (this.opts.dragMode === 6 /* roiSelection */) {
31232
+ this.drawCircle(leftTopWidthHeight, this.opts.selectionBoxColor, 0.1);
31233
+ return;
31234
+ }
30585
31235
  this.drawRect(leftTopWidthHeight, this.opts.selectionBoxColor);
30586
31236
  }
30587
31237
  // not included in public docs
@@ -31361,6 +32011,18 @@ var Niivue = class _Niivue {
31361
32011
  }
31362
32012
  draw2D(leftTopWidthHeight, axCorSag, customMM = NaN, imageWidthHeight = [NaN, NaN]) {
31363
32013
  const padLeftTop = [NaN, NaN];
32014
+ if (imageWidthHeight[0] === Infinity) {
32015
+ const volScale = this.sliceScale().volScale;
32016
+ let scale = this.scaleSlice(volScale[0], volScale[1], [0, 0], [leftTopWidthHeight[2], leftTopWidthHeight[3]]);
32017
+ if (axCorSag === 1 /* CORONAL */) {
32018
+ scale = this.scaleSlice(volScale[0], volScale[2], [0, 0], [leftTopWidthHeight[2], leftTopWidthHeight[3]]);
32019
+ }
32020
+ if (axCorSag === 2 /* SAGITTAL */) {
32021
+ scale = this.scaleSlice(volScale[1], volScale[2], [0, 0], [leftTopWidthHeight[2], leftTopWidthHeight[3]]);
32022
+ }
32023
+ imageWidthHeight[0] = scale[2];
32024
+ imageWidthHeight[1] = scale[3];
32025
+ }
31364
32026
  if (isNaN(imageWidthHeight[0])) {
31365
32027
  this.draw2DMain(leftTopWidthHeight, axCorSag, customMM);
31366
32028
  } else {
@@ -31572,10 +32234,10 @@ var Niivue = class _Niivue {
31572
32234
  if (graph.opacity <= 0 || graph.LTWH[2] <= 5 || graph.LTWH[3] <= 5) {
31573
32235
  return;
31574
32236
  }
31575
- if (graph.LTWH[0] + graph.LTWH[2] > this.gl.canvas.width) {
32237
+ if (Math.floor(graph.LTWH[0] + graph.LTWH[2]) > this.gl.canvas.width) {
31576
32238
  return;
31577
32239
  }
31578
- if (graph.LTWH[1] + graph.LTWH[3] > this.gl.canvas.height) {
32240
+ if (Math.floor(graph.LTWH[1] + graph.LTWH[3]) > this.gl.canvas.height) {
31579
32241
  return;
31580
32242
  }
31581
32243
  graph.backColor = [0.15, 0.15, 0.15, graph.opacity];
@@ -31645,6 +32307,12 @@ var Niivue = class _Niivue {
31645
32307
  mx = Math.max(v, mx);
31646
32308
  }
31647
32309
  }
32310
+ const volMn = this.volumes[vols[0]].cal_min;
32311
+ const volMx = this.volumes[vols[0]].cal_max;
32312
+ if (graph.isRangeCalMinMax && volMn < volMx && isFinite(volMn) && isFinite(volMx)) {
32313
+ mn = volMn;
32314
+ mx = volMx;
32315
+ }
31648
32316
  if (graph.normalizeValues && mx > mn) {
31649
32317
  const range = mx - mn;
31650
32318
  for (let j = 0; j < graph.lines.length; j++) {
@@ -32400,22 +33068,28 @@ var Niivue = class _Niivue {
32400
33068
  }
32401
33069
  const gl = this.gl;
32402
33070
  const mm = this.frac2mm(this.scene.crosshairPos, 0, isSliceMM);
33071
+ let radius = 1;
33072
+ const [mn, mx, range] = this.sceneExtentsMinMax(isSliceMM);
33073
+ if (this.volumes.length > 0) {
33074
+ if (!this.back) {
33075
+ throw new Error("back undefined");
33076
+ }
33077
+ radius = 0.5 * Math.min(Math.min(this.back.pixDims[1], this.back.pixDims[2]), this.back.pixDims[3]);
33078
+ } else if (range[0] < 50 || range[0] > 1e3) {
33079
+ radius = range[0] * 0.02;
33080
+ }
33081
+ radius *= this.opts.crosshairWidth;
33082
+ if (this.opts?.crosshairWidthUnit === "percent") {
33083
+ radius = range[0] * this.opts.crosshairWidth * 0.5 * 0.01;
33084
+ }
33085
+ if (this.opts?.crosshairWidthUnit === "mm") {
33086
+ radius = this.opts.crosshairWidth * 0.5;
33087
+ }
32403
33088
  if (this.crosshairs3D === null || this.crosshairs3D.mm[0] !== mm[0] || this.crosshairs3D.mm[1] !== mm[1] || this.crosshairs3D.mm[2] !== mm[2]) {
32404
33089
  if (this.crosshairs3D !== null) {
32405
33090
  gl.deleteBuffer(this.crosshairs3D.indexBuffer);
32406
33091
  gl.deleteBuffer(this.crosshairs3D.vertexBuffer);
32407
33092
  }
32408
- const [mn, mx, range] = this.sceneExtentsMinMax(isSliceMM);
32409
- let radius = 1;
32410
- if (this.volumes.length > 0) {
32411
- if (!this.back) {
32412
- throw new Error("back undefined");
32413
- }
32414
- radius = 0.5 * Math.min(Math.min(this.back.pixDims[1], this.back.pixDims[2]), this.back.pixDims[3]);
32415
- } else if (range[0] < 50 || range[0] > 1e3) {
32416
- radius = range[0] * 0.02;
32417
- }
32418
- radius *= this.opts.crosshairWidth;
32419
33093
  this.crosshairs3D = NiivueObject3D.generateCrosshairs(this.gl, 1, mm, mn, mx, radius, 20, this.opts.crosshairGap);
32420
33094
  this.crosshairs3D.mm = mm;
32421
33095
  }
@@ -32434,6 +33108,7 @@ var Niivue = class _Niivue {
32434
33108
  );
32435
33109
  }
32436
33110
  gl.uniformMatrix4fv(crosshairsShader.uniforms.mvpMtx, false, mvpMtx);
33111
+ gl.disable(gl.CULL_FACE);
32437
33112
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.crosshairs3D.indexBuffer);
32438
33113
  gl.enable(gl.DEPTH_TEST);
32439
33114
  const color = [...this.opts.crosshairColor];
@@ -32584,9 +33259,9 @@ var Niivue = class _Niivue {
32584
33259
  }
32585
33260
  // not included in public docs
32586
33261
  // note: we also have a "sliceScale" method, which could be confusing
32587
- scaleSlice(w, h, widthPadPixels = 0, heightPadPixels = 0) {
32588
- const canvasW = this.effectiveCanvasWidth() - widthPadPixels;
32589
- const canvasH = this.effectiveCanvasHeight() - heightPadPixels;
33262
+ scaleSlice(w, h, padPixelsWH = [0, 0], canvasWH = [0, 0]) {
33263
+ const canvasW = canvasWH[0] === 0 ? this.effectiveCanvasWidth() - padPixelsWH[0] : canvasWH[0] - padPixelsWH[0];
33264
+ const canvasH = canvasWH[1] === 0 ? this.effectiveCanvasHeight() - padPixelsWH[1] : canvasWH[1] - padPixelsWH[1];
32590
33265
  let scalePix = canvasW / w;
32591
33266
  if (h * scalePix > canvasH) {
32592
33267
  scalePix = canvasH / h;
@@ -33035,7 +33710,6 @@ var Niivue = class _Niivue {
33035
33710
  if (this.volumes.length === 0 || typeof this.volumes[0].dims === "undefined") {
33036
33711
  if (this.meshes.length > 0) {
33037
33712
  this.screenSlices = [];
33038
- this.opts.sliceType = 4 /* RENDER */;
33039
33713
  this.draw3D();
33040
33714
  if (this.opts.isColorbar) {
33041
33715
  this.drawColorbar();
@@ -33079,6 +33753,8 @@ var Niivue = class _Niivue {
33079
33753
  if (this.sliceMosaicString.length > 0) {
33080
33754
  this.drawMosaic(this.sliceMosaicString);
33081
33755
  } else {
33756
+ const heroImageWH = [0, 0];
33757
+ let isHeroImage = false;
33082
33758
  this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
33083
33759
  this.screenSlices = [];
33084
33760
  if (this.opts.sliceType === 0 /* AXIAL */) {
@@ -33088,6 +33764,9 @@ var Niivue = class _Niivue {
33088
33764
  } else if (this.opts.sliceType === 2 /* SAGITTAL */) {
33089
33765
  this.draw2D([0, 0, 0, 0], 2);
33090
33766
  } else {
33767
+ let padPixelsWH = function(cols, rows) {
33768
+ return [(cols - 1) * pad + cols * innerPad, (rows - 1) * pad + rows * innerPad];
33769
+ };
33091
33770
  let isShowRender = false;
33092
33771
  if (this.opts.multiplanarForceRender) {
33093
33772
  isShowRender = true;
@@ -33114,20 +33793,42 @@ var Niivue = class _Niivue {
33114
33793
  log.debug("multiplanarPadPixels must be numeric");
33115
33794
  }
33116
33795
  const pad = parseFloat(`${this.opts.multiplanarPadPixels}`);
33117
- const ltwh2x2 = this.scaleSlice(volScale[0] + volScale[1], volScale[1] + volScale[2], pad * 1, pad * 1);
33796
+ let innerPad = this.opts.tileMargin;
33797
+ if (innerPad < 0) {
33798
+ innerPad = 2 * (2 + Math.ceil(Math.max(this.opts.textHeight, 0.01) * Math.min(this.gl.canvas.height, this.gl.canvas.width)));
33799
+ }
33800
+ let canvasWH = [this.effectiveCanvasWidth(), this.effectiveCanvasHeight()];
33801
+ if (this.opts.heroImageFraction > 0 && this.opts.heroImageFraction < 1) {
33802
+ isShowRender = false;
33803
+ isHeroImage = true;
33804
+ if (canvasWH[0] > canvasWH[1] && this.opts.multiplanarLayout !== 3 /* ROW */) {
33805
+ heroImageWH[0] = canvasWH[0] * this.opts.heroImageFraction;
33806
+ } else {
33807
+ heroImageWH[1] = canvasWH[1] * this.opts.heroImageFraction;
33808
+ }
33809
+ canvasWH = [canvasWH[0] - heroImageWH[0], canvasWH[1] - heroImageWH[1]];
33810
+ }
33811
+ const ltwh2x2 = this.scaleSlice(
33812
+ volScale[0] + volScale[1],
33813
+ volScale[1] + volScale[2],
33814
+ padPixelsWH(2, 2),
33815
+ canvasWH
33816
+ );
33118
33817
  const mx = Math.max(Math.max(volScale[1], volScale[2]), volScale[0]);
33119
33818
  const ltwh3x1 = this.scaleSlice(
33120
33819
  volScale[0] + volScale[0] + volScale[1],
33121
33820
  Math.max(volScale[1], volScale[2]),
33122
- pad * 2
33821
+ padPixelsWH(3, 1),
33822
+ canvasWH
33123
33823
  );
33124
33824
  const ltwh4x1 = this.scaleSlice(
33125
33825
  volScale[0] + volScale[0] + volScale[1] + mx,
33126
33826
  Math.max(volScale[1], volScale[2]),
33127
- pad * 3
33827
+ padPixelsWH(4, 1),
33828
+ canvasWH
33128
33829
  );
33129
- const ltwh1x3 = this.scaleSlice(mx, volScale[1] + volScale[2] + volScale[2], 0, pad * 2);
33130
- const ltwh1x4 = this.scaleSlice(mx, volScale[1] + volScale[2] + volScale[2] + mx, 0, pad * 3);
33830
+ const ltwh1x3 = this.scaleSlice(mx, volScale[1] + volScale[2] + volScale[2], padPixelsWH(1, 3), canvasWH);
33831
+ const ltwh1x4 = this.scaleSlice(mx, volScale[1] + volScale[2] + volScale[2] + mx, padPixelsWH(1, 4), canvasWH);
33131
33832
  let isDraw3D = !isDrawPenDown && (maxVols < 2 || !isDrawGraph);
33132
33833
  let isDrawColumn = false;
33133
33834
  let isDrawGrid = false;
@@ -33150,22 +33851,39 @@ var Niivue = class _Niivue {
33150
33851
  let ltwh = ltwh2x2;
33151
33852
  if (isDrawColumn) {
33152
33853
  ltwh = ltwh1x3;
33153
- if (isShowRender || this.opts.multiplanarShowRender === 2 /* AUTO */ && ltwh1x4[4] >= ltwh1x3[4]) {
33854
+ if (!isHeroImage && (isShowRender || this.opts.multiplanarShowRender === 2 /* AUTO */ && ltwh1x4[4] >= ltwh1x3[4])) {
33154
33855
  ltwh = ltwh1x4;
33155
33856
  } else {
33156
33857
  isDraw3D = false;
33157
33858
  }
33158
33859
  } else if (isDrawRow) {
33159
33860
  ltwh = ltwh3x1;
33160
- if (isShowRender || this.opts.multiplanarShowRender === 2 /* AUTO */ && ltwh4x1[4] >= ltwh3x1[4]) {
33861
+ if (!isHeroImage && (isShowRender || this.opts.multiplanarShowRender === 2 /* AUTO */ && ltwh4x1[4] >= ltwh3x1[4])) {
33161
33862
  ltwh = ltwh4x1;
33162
33863
  } else {
33163
33864
  isDraw3D = false;
33164
33865
  }
33165
33866
  }
33166
- const sX = volScale[0] * ltwh[4];
33167
- const sY = volScale[1] * ltwh[4];
33168
- const sZ = volScale[2] * ltwh[4];
33867
+ if (isHeroImage) {
33868
+ const heroW = heroImageWH[0] === 0 ? this.effectiveCanvasWidth() : heroImageWH[0];
33869
+ const heroH = heroImageWH[1] === 0 ? this.effectiveCanvasHeight() : heroImageWH[1];
33870
+ if (this.opts?.heroSliceType === 0 /* AXIAL */ || this.opts?.heroSliceType === 1 /* CORONAL */ || this.opts?.heroSliceType === 2 /* SAGITTAL */) {
33871
+ this.draw2D([0, 0, heroW, heroH], this.opts.heroSliceType, NaN, [Infinity, Infinity]);
33872
+ } else {
33873
+ const ltwh2 = ltwh.slice();
33874
+ const canvasW = this.effectiveCanvasWidth();
33875
+ if (heroW === canvasW) {
33876
+ ltwh2[0] = 0;
33877
+ }
33878
+ this.draw3D([ltwh2[0], 0, heroW, heroH]);
33879
+ }
33880
+ ltwh[0] += heroImageWH[0];
33881
+ ltwh[1] += heroImageWH[1];
33882
+ isDraw3D = false;
33883
+ }
33884
+ const sX = volScale[0] * ltwh[4] + innerPad;
33885
+ const sY = volScale[1] * ltwh[4] + innerPad;
33886
+ const sZ = volScale[2] * ltwh[4] + innerPad;
33169
33887
  const actualX = actualScale[0] * ltwh[4];
33170
33888
  const actualY = actualScale[1] * ltwh[4];
33171
33889
  const actualZ = actualScale[2] * ltwh[4];
@@ -33192,6 +33910,9 @@ var Niivue = class _Niivue {
33192
33910
  if (this.opts.multiplanarShowRender === 2 /* AUTO */) {
33193
33911
  isDraw3D = true;
33194
33912
  }
33913
+ if (isHeroImage) {
33914
+ isDraw3D = false;
33915
+ }
33195
33916
  this.draw2D([ltwh[0], ltwh[1] + sZ + pad, sX, sY], 0, NaN, [actualX, actualY]);
33196
33917
  this.draw2D([ltwh[0], ltwh[1], sX, sZ], 1, NaN, [actualX, actualZ]);
33197
33918
  this.draw2D([ltwh[0] + sX + pad, ltwh[1], sY, sZ], 2, NaN, [actualY, actualZ]);
@@ -33258,10 +33979,14 @@ var Niivue = class _Niivue {
33258
33979
  width,
33259
33980
  height
33260
33981
  ]);
33982
+ return;
33261
33983
  }
33262
33984
  if (this.opts.clickToSegment) {
33263
33985
  const x = this.mousePos[0];
33264
33986
  const y = this.mousePos[1];
33987
+ if (this.inRenderTile(x, y) >= 0) {
33988
+ return;
33989
+ }
33265
33990
  const tileIdx = this.tileIndex(x, y);
33266
33991
  if (tileIdx > -1) {
33267
33992
  const fovMM = this.screenSlices[tileIdx].fovMM;