@niivue/niivue 0.44.1 → 0.44.2-dev.1

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
@@ -4,16 +4,13 @@ var __export = (target, all) => {
4
4
  for (var name in all)
5
5
  __defProp(target, name, { get: all[name], enumerable: true });
6
6
  };
7
- var __publicField = (obj, key, value) => {
8
- __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
9
- return value;
10
- };
7
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
11
8
 
12
9
  // src/niivue/index.ts
13
10
  import { mat4 as mat46, vec2, vec3 as vec311, vec4 as vec48 } from "gl-matrix";
14
11
 
15
12
  // package.json
16
- var version = "0.44.1";
13
+ var version = "0.44.2-dev.1";
17
14
 
18
15
  // src/logger.ts
19
16
  var _Log = class _Log {
@@ -809,6 +806,46 @@ out vec4 color;
809
806
  void main() {
810
807
  color = lineColor;
811
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
+ }`;
812
849
  var vertColorbarShader = `#version 300 es
813
850
  #line 490
814
851
  layout(location=0) in vec3 pos;
@@ -1271,11 +1308,11 @@ uniform mat4 mvpMtx;
1271
1308
  uniform mat4 normMtx;
1272
1309
  out vec4 vClr;
1273
1310
  out vec3 vN;
1274
- out vec4 vP;
1311
+ out vec4 vPc;
1275
1312
  void main(void) {
1276
1313
  vec3 lightPosition = vec3(0.0, 0.0, -10.0);
1277
- vP = vec4(pos, 1.0);
1278
- gl_Position = mvpMtx * vP;
1314
+ vPc = mvpMtx * vec4(pos, 1.0);
1315
+ gl_Position = vPc;
1279
1316
  vN = normalize((normMtx * vec4(norm.xyz,1.0)).xyz);
1280
1317
  //vV = -vec3(modelMtx*vec4(pos,1.0));
1281
1318
  vClr = clr;
@@ -1420,6 +1457,45 @@ void main() {
1420
1457
  s *= specularWhite;
1421
1458
  color = vec4(d3 + s3 + s, opacity);
1422
1459
  }`;
1460
+ var fragMeshShaderCrevice = `#version 300 es
1461
+ precision highp int;
1462
+ precision highp float;
1463
+ uniform float opacity;
1464
+ in vec4 vClr;
1465
+ in vec3 vN;
1466
+ in vec4 vPc;
1467
+ out vec4 color;
1468
+ void main() {
1469
+ vec3 n = normalize(vN);
1470
+ // Compute curvature
1471
+ vec3 dx = dFdx(n);
1472
+ vec3 dy = dFdy(n);
1473
+ vec3 xneg = n - dx;
1474
+ vec3 xpos = n + dx;
1475
+ vec3 yneg = n - dy;
1476
+ vec3 ypos = n + dy;
1477
+ float depth = length(vPc.xyz);
1478
+ float curv = (cross(xneg, xpos).y - cross(yneg, ypos).x) / depth;
1479
+ //at this stage 0.5 for flat, with valleys dark and ridges bright
1480
+ curv = 1.0 - (curv + 0.5);
1481
+ //clamp
1482
+ curv = min(max(curv, 0.0), 1.0);
1483
+ // easing function
1484
+ curv = pow(curv, 0.5);
1485
+ //modulate ambient and diffuse with curvature
1486
+ vec3 r = vec3(0.0, 0.0, 1.0); //rayDir: for orthographic projections moving in Z direction (no need for normal matrix)
1487
+ float ambient = 0.6;
1488
+ float diffuse = 0.6;
1489
+ float specular = 0.2;
1490
+ float shininess = 10.0;
1491
+ vec3 lightPosition = vec3(0.0, 10.0, -2.0);
1492
+ vec3 l = normalize(lightPosition);
1493
+ float lightNormDot = dot(n, l);
1494
+ vec3 a = vClr.rgb * ambient * curv;
1495
+ vec3 d = max(lightNormDot, 0.0) * vClr.rgb * diffuse;
1496
+ float s = specular * pow(max(dot(reflect(l, n), r), 0.0), shininess);
1497
+ color = vec4(a + d + s, opacity);
1498
+ }`;
1423
1499
  var fragMeshShader = `#version 300 es
1424
1500
  precision highp int;
1425
1501
  precision highp float;
@@ -12357,6 +12433,12 @@ var NVMeshLoaders = class _NVMeshLoaders {
12357
12433
  while (lines[pos].length < 1) {
12358
12434
  pos++;
12359
12435
  }
12436
+ if (lines[pos].startsWith("METADATA")) {
12437
+ while (lines[pos].length > 1) {
12438
+ pos++;
12439
+ }
12440
+ pos++;
12441
+ }
12360
12442
  items = lines[pos].trim().split(/\s+/);
12361
12443
  pos++;
12362
12444
  if (items[0].includes("LINES")) {
@@ -12535,7 +12617,7 @@ var NVMeshLoaders = class _NVMeshLoaders {
12535
12617
  layer.values = obj;
12536
12618
  }
12537
12619
  }
12538
- } else if (ext === "CRV" || ext === "CURV") {
12620
+ } else if (ext === "CRV" || ext === "CURV" || ext === "THICKNESS" || ext === "AREA") {
12539
12621
  layer.values = _NVMeshLoaders.readCURV(buffer, n_vert);
12540
12622
  layer.isTransparentBelowCalMin = false;
12541
12623
  } else if (ext === "GII") {
@@ -12560,6 +12642,9 @@ var NVMeshLoaders = class _NVMeshLoaders {
12560
12642
  layer.values = _NVMeshLoaders.readSMP(buffer, n_vert);
12561
12643
  } else if (ext === "STC") {
12562
12644
  layer.values = _NVMeshLoaders.readSTC(buffer, n_vert);
12645
+ } else if (_NVMeshLoaders.isCurv(buffer)) {
12646
+ layer.values = _NVMeshLoaders.readCURV(buffer, n_vert);
12647
+ layer.isTransparentBelowCalMin = false;
12563
12648
  } else {
12564
12649
  log.warn("Unknown layer overlay format " + name);
12565
12650
  return layer;
@@ -12733,6 +12818,17 @@ var NVMeshLoaders = class _NVMeshLoaders {
12733
12818
  return f32;
12734
12819
  }
12735
12820
  // readSTC()
12821
+ static isCurv(buffer) {
12822
+ const view = new DataView(buffer);
12823
+ const sig0 = view.getUint8(0);
12824
+ const sig1 = view.getUint8(1);
12825
+ const sig2 = view.getUint8(2);
12826
+ if (sig0 !== 255 || sig1 !== 255 || sig2 !== 255) {
12827
+ utiltiesLogger.debug("Unable to recognize file type: does not appear to be FreeSurfer format.");
12828
+ return false;
12829
+ }
12830
+ return true;
12831
+ }
12736
12832
  // read freesurfer curv big-endian format
12737
12833
  // https://github.com/bonilhamusclab/MRIcroS/blob/master/%2BfileUtils/%2Bpial/readPial.m
12738
12834
  // http://www.grahamwideman.com/gw/brain/fs/surfacefileformats.htm
@@ -18369,76 +18465,90 @@ function getBestTransform(imageDirections, voxelDimensions, imagePosition) {
18369
18465
  }
18370
18466
  return m;
18371
18467
  }
18372
- function str2Buffer(str) {
18468
+ function str2Buffer(str, maxLen = 80) {
18469
+ str = str.replace(/[`$]/g, "");
18373
18470
  const bytes = [];
18374
- for (let i = 0; i < str.length; i++) {
18471
+ const len = Math.min(maxLen, str.length);
18472
+ for (let i = 0; i < len; i++) {
18375
18473
  const char = str.charCodeAt(i);
18376
18474
  bytes.push(char & 255);
18377
18475
  }
18378
18476
  return bytes;
18379
18477
  }
18380
- function hdrToArrayBuffer(hdr, isDrawing8 = false) {
18478
+ function hdrToArrayBuffer(hdr, isDrawing8 = false, isInputEndian = false) {
18381
18479
  const SHORT_SIZE = 2;
18382
18480
  const FLOAT32_SIZE = 4;
18481
+ let isLittleEndian = true;
18482
+ if (isInputEndian) {
18483
+ isLittleEndian = hdr.littleEndian;
18484
+ }
18383
18485
  const byteArray = new Uint8Array(348);
18384
18486
  const view = new DataView(byteArray.buffer);
18385
- view.setInt32(0, 348, hdr.littleEndian);
18487
+ view.setInt32(0, 348, isLittleEndian);
18488
+ view.setUint8(38, 114);
18386
18489
  view.setUint8(39, hdr.dim_info);
18387
18490
  for (let i = 0; i < 8; i++) {
18388
- view.setUint16(40 + SHORT_SIZE * i, hdr.dims[i], hdr.littleEndian);
18491
+ view.setUint16(40 + SHORT_SIZE * i, hdr.dims[i], isLittleEndian);
18389
18492
  }
18390
- view.setFloat32(56, hdr.intent_p1, hdr.littleEndian);
18391
- view.setFloat32(60, hdr.intent_p2, hdr.littleEndian);
18392
- view.setFloat32(64, hdr.intent_p3, hdr.littleEndian);
18393
- view.setInt16(68, hdr.intent_code, hdr.littleEndian);
18493
+ view.setFloat32(56, hdr.intent_p1, isLittleEndian);
18494
+ view.setFloat32(60, hdr.intent_p2, isLittleEndian);
18495
+ view.setFloat32(64, hdr.intent_p3, isLittleEndian);
18496
+ view.setInt16(68, hdr.intent_code, isLittleEndian);
18394
18497
  if (isDrawing8) {
18395
- view.setInt16(70, 2, hdr.littleEndian);
18396
- view.setInt16(72, 8, hdr.littleEndian);
18498
+ view.setInt16(70, 2, isLittleEndian);
18499
+ view.setInt16(72, 8, isLittleEndian);
18397
18500
  } else {
18398
- view.setInt16(70, hdr.datatypeCode, hdr.littleEndian);
18399
- view.setInt16(72, hdr.numBitsPerVoxel, hdr.littleEndian);
18501
+ view.setInt16(70, hdr.datatypeCode, isLittleEndian);
18502
+ view.setInt16(72, hdr.numBitsPerVoxel, isLittleEndian);
18400
18503
  }
18401
- view.setInt16(74, hdr.slice_start, hdr.littleEndian);
18504
+ view.setInt16(74, hdr.slice_start, isLittleEndian);
18402
18505
  for (let i = 0; i < 8; i++) {
18403
- view.setFloat32(76 + FLOAT32_SIZE * i, hdr.pixDims[i], hdr.littleEndian);
18506
+ view.setFloat32(76 + FLOAT32_SIZE * i, hdr.pixDims[i], isLittleEndian);
18404
18507
  }
18405
18508
  if (isDrawing8) {
18406
- view.setFloat32(108, 352, hdr.littleEndian);
18407
- view.setFloat32(112, 1, hdr.littleEndian);
18408
- view.setFloat32(116, 0, hdr.littleEndian);
18509
+ view.setFloat32(108, 352, isLittleEndian);
18510
+ view.setFloat32(112, 1, isLittleEndian);
18511
+ view.setFloat32(116, 0, isLittleEndian);
18409
18512
  } else {
18410
- view.setFloat32(108, hdr.vox_offset, hdr.littleEndian);
18411
- view.setFloat32(112, hdr.scl_slope, hdr.littleEndian);
18412
- view.setFloat32(116, hdr.scl_inter, hdr.littleEndian);
18513
+ view.setFloat32(108, 352, isLittleEndian);
18514
+ view.setFloat32(112, hdr.scl_slope, isLittleEndian);
18515
+ view.setFloat32(116, hdr.scl_inter, isLittleEndian);
18413
18516
  }
18414
- view.setInt16(120, hdr.slice_end, hdr.littleEndian);
18517
+ view.setInt16(120, hdr.slice_end, isLittleEndian);
18415
18518
  view.setUint8(122, hdr.slice_code);
18416
- view.setUint8(123, hdr.xyzt_units);
18519
+ if (hdr.xyzt_units === 0) {
18520
+ view.setUint8(123, 10);
18521
+ } else {
18522
+ view.setUint8(123, hdr.xyzt_units);
18523
+ }
18417
18524
  if (isDrawing8) {
18418
- view.setFloat32(124, 0, hdr.littleEndian);
18419
- view.setFloat32(128, 0, hdr.littleEndian);
18525
+ view.setFloat32(124, 0, isLittleEndian);
18526
+ view.setFloat32(128, 0, isLittleEndian);
18420
18527
  } else {
18421
- view.setFloat32(124, hdr.cal_max, hdr.littleEndian);
18422
- view.setFloat32(128, hdr.cal_min, hdr.littleEndian);
18528
+ view.setFloat32(124, hdr.cal_max, isLittleEndian);
18529
+ view.setFloat32(128, hdr.cal_min, isLittleEndian);
18423
18530
  }
18424
- view.setFloat32(132, hdr.slice_duration, hdr.littleEndian);
18425
- view.setFloat32(136, hdr.toffset, hdr.littleEndian);
18531
+ view.setFloat32(132, hdr.slice_duration, isLittleEndian);
18532
+ view.setFloat32(136, hdr.toffset, isLittleEndian);
18426
18533
  byteArray.set(str2Buffer(hdr.description), 148);
18427
18534
  byteArray.set(str2Buffer(hdr.aux_file), 228);
18428
- view.setInt16(252, hdr.qform_code, hdr.littleEndian);
18429
- view.setInt16(254, hdr.sform_code, hdr.littleEndian);
18430
- view.setFloat32(256, hdr.quatern_b, hdr.littleEndian);
18431
- view.setFloat32(260, hdr.quatern_c, hdr.littleEndian);
18432
- view.setFloat32(264, hdr.quatern_d, hdr.littleEndian);
18433
- view.setFloat32(268, hdr.qoffset_x, hdr.littleEndian);
18434
- view.setFloat32(272, hdr.qoffset_y, hdr.littleEndian);
18435
- view.setFloat32(276, hdr.qoffset_z, hdr.littleEndian);
18535
+ view.setInt16(252, hdr.qform_code, isLittleEndian);
18536
+ if (hdr.sform_code < 1 || hdr.sform_code < 1) {
18537
+ view.setInt16(254, 1, isLittleEndian);
18538
+ } else {
18539
+ view.setInt16(254, hdr.sform_code, isLittleEndian);
18540
+ }
18541
+ view.setFloat32(256, hdr.quatern_b, isLittleEndian);
18542
+ view.setFloat32(260, hdr.quatern_c, isLittleEndian);
18543
+ view.setFloat32(264, hdr.quatern_d, isLittleEndian);
18544
+ view.setFloat32(268, hdr.qoffset_x, isLittleEndian);
18545
+ view.setFloat32(272, hdr.qoffset_y, isLittleEndian);
18546
+ view.setFloat32(276, hdr.qoffset_z, isLittleEndian);
18436
18547
  const flattened = hdr.affine.flat();
18437
18548
  for (let i = 0; i < 12; i++) {
18438
- view.setFloat32(280 + FLOAT32_SIZE * i, flattened[i], hdr.littleEndian);
18549
+ view.setFloat32(280 + FLOAT32_SIZE * i, flattened[i], isLittleEndian);
18439
18550
  }
18440
- byteArray.set(str2Buffer(hdr.intent_name), 328);
18441
- byteArray.set(str2Buffer(hdr.magic), 344);
18551
+ view.setInt32(344, 3222382, true);
18442
18552
  return byteArray;
18443
18553
  }
18444
18554
  function getExtents(positions, forceOriginInVolume = true) {
@@ -19982,7 +20092,7 @@ var NVImage = class _NVImage {
19982
20092
  hdr.datatypeCode = 64 /* DT_FLOAT64 */;
19983
20093
  break;
19984
20094
  default:
19985
- throw new Error("Unsupported NRRD data type: " + items[0]);
20095
+ throw new Error("Unsupported MHA data type: " + items[0]);
19986
20096
  }
19987
20097
  }
19988
20098
  if (line.startsWith("ObjectType") && !items[0].includes("Image")) {
@@ -20004,6 +20114,9 @@ var NVImage = class _NVImage {
20004
20114
  [mat33[2], mat33[5], mat33[8], offset[2]],
20005
20115
  [0, 0, 0, 1]
20006
20116
  ];
20117
+ while (bytes[pos] === 10) {
20118
+ pos++;
20119
+ }
20007
20120
  hdr.vox_offset = pos;
20008
20121
  if (isDetached && pairedImgData) {
20009
20122
  if (isGz) {
@@ -22177,6 +22290,7 @@ var DRAG_MODE = /* @__PURE__ */ ((DRAG_MODE2) => {
22177
22290
  DRAG_MODE2[DRAG_MODE2["pan"] = 3] = "pan";
22178
22291
  DRAG_MODE2[DRAG_MODE2["slicer3D"] = 4] = "slicer3D";
22179
22292
  DRAG_MODE2[DRAG_MODE2["callbackOnly"] = 5] = "callbackOnly";
22293
+ DRAG_MODE2[DRAG_MODE2["roiSelection"] = 6] = "roiSelection";
22180
22294
  return DRAG_MODE2;
22181
22295
  })(DRAG_MODE || {});
22182
22296
  var DEFAULT_OPTIONS = {
@@ -22211,6 +22325,7 @@ var DEFAULT_OPTIONS = {
22211
22325
  multiplanarPadPixels: 0,
22212
22326
  // @deprecated
22213
22327
  multiplanarForceRender: false,
22328
+ multiplanarEqualSize: false,
22214
22329
  multiplanarShowRender: 2 /* AUTO */,
22215
22330
  // auto is the same behaviour as multiplanarForceRender: false
22216
22331
  isRadiologicalConvention: false,
@@ -22245,12 +22360,30 @@ var DEFAULT_OPTIONS = {
22245
22360
  renderOverlayBlend: 1,
22246
22361
  sliceMosaicString: "",
22247
22362
  centerMosaic: false,
22363
+ penSize: 1,
22364
+ // in voxels, since all drawing is done using bitmap indices
22248
22365
  clickToSegment: false,
22249
- clickToSegmentRadius: 2,
22250
- clickToSegmentSteps: 10,
22251
- clickToSegmentBright: true
22366
+ clickToSegmentRadius: 3,
22367
+ // in mm
22368
+ clickToSegmentBright: true,
22369
+ clickToSegmentAutoIntensity: false,
22370
+ // new option, but keep clickToSegmentBright for backwards compatibility
22371
+ clickToSegmentIntensityMax: NaN,
22372
+ // NaN will use auto threshold (default flood fill behavior from before)
22373
+ clickToSegmentIntensityMin: NaN,
22374
+ // NaN will use auto threshold (default flood fill behavior from before)
22375
+ // 0 will use auto threshold (default flood fill behavior from before)
22376
+ // Take the voxel intensity at the click point and use this percentage +/- to threshold the flood fill operation.
22377
+ // If greater than 0, clickedVoxelIntensity +/- clickedVoxelIntensity * clickToSegmentPercent will be used
22378
+ // for the clickToSegmentIntensityMin and clickToSegmentIntensityMax values.
22379
+ clickToSegmentPercent: 0,
22380
+ clickToSegmentMaxDistanceMM: Number.POSITIVE_INFINITY,
22381
+ // default value is infinity for backwards compatibility with flood fill routine.
22382
+ selectionBoxLineThickness: 4,
22383
+ selectionBoxIsOutline: false
22252
22384
  };
22253
22385
  var INITIAL_SCENE_DATA = {
22386
+ gamma: 1,
22254
22387
  azimuth: 110,
22255
22388
  elevation: 10,
22256
22389
  crosshairPos: vec38.create(),
@@ -22489,6 +22622,9 @@ var NVDocument = class _NVDocument {
22489
22622
  data.opts.meshThicknessOn2D = "infinity";
22490
22623
  }
22491
22624
  data.labels = [...this.data.labels];
22625
+ for (const label of data.labels) {
22626
+ delete label.onClick;
22627
+ }
22492
22628
  data.customData = this.customData;
22493
22629
  if (this.volumes.length) {
22494
22630
  let imageOptions = this.imageOptionsArray[0];
@@ -22744,6 +22880,19 @@ var LabelLineTerminator = /* @__PURE__ */ ((LabelLineTerminator2) => {
22744
22880
  LabelLineTerminator2["RING"] = "ring";
22745
22881
  return LabelLineTerminator2;
22746
22882
  })(LabelLineTerminator || {});
22883
+ var LabelAnchorPoint = /* @__PURE__ */ ((LabelAnchorPoint2) => {
22884
+ LabelAnchorPoint2[LabelAnchorPoint2["NONE"] = 0 /* NONE */] = "NONE";
22885
+ LabelAnchorPoint2[LabelAnchorPoint2["TOPLEFT"] = 9] = "TOPLEFT";
22886
+ LabelAnchorPoint2[LabelAnchorPoint2["TOPCENTER"] = 10] = "TOPCENTER";
22887
+ LabelAnchorPoint2[LabelAnchorPoint2["TOPRIGHT"] = 12] = "TOPRIGHT";
22888
+ LabelAnchorPoint2[LabelAnchorPoint2["MIDDLELEFT"] = 17] = "MIDDLELEFT";
22889
+ LabelAnchorPoint2[LabelAnchorPoint2["MIDDLECENTER"] = 18] = "MIDDLECENTER";
22890
+ LabelAnchorPoint2[LabelAnchorPoint2["MIDDLERIGHT"] = 20] = "MIDDLERIGHT";
22891
+ LabelAnchorPoint2[LabelAnchorPoint2["BOTTOMLEFT"] = 33] = "BOTTOMLEFT";
22892
+ LabelAnchorPoint2[LabelAnchorPoint2["BOTTOMCENTER"] = 34] = "BOTTOMCENTER";
22893
+ LabelAnchorPoint2[LabelAnchorPoint2["BOTTOMRIGHT"] = 36] = "BOTTOMRIGHT";
22894
+ return LabelAnchorPoint2;
22895
+ })(LabelAnchorPoint || {});
22747
22896
  var NVLabel3DStyle = class {
22748
22897
  /**
22749
22898
  * @param textColor - Color of text
@@ -22781,13 +22930,17 @@ var NVLabel3D = class {
22781
22930
  * @param style - The style of the label
22782
22931
  * @param points - An array of points label for label lines
22783
22932
  */
22784
- constructor(text, style, points) {
22933
+ constructor(text, style, points, anchor, onClick) {
22785
22934
  __publicField(this, "text");
22786
22935
  __publicField(this, "style");
22787
22936
  __publicField(this, "points");
22937
+ __publicField(this, "anchor");
22938
+ __publicField(this, "onClick");
22788
22939
  this.text = text;
22789
22940
  this.style = style;
22790
22941
  this.points = points;
22942
+ this.anchor = anchor || 0 /* NONE */;
22943
+ this.onClick = onClick;
22791
22944
  }
22792
22945
  };
22793
22946
 
@@ -23924,7 +24077,7 @@ var defaultSaveImageOptions = {
23924
24077
  isSaveDrawing: false,
23925
24078
  volumeByIndex: 0
23926
24079
  };
23927
- var Niivue = class _Niivue {
24080
+ var Niivue = class {
23928
24081
  /**
23929
24082
  * @param options - options object to set modifiable Niivue properties
23930
24083
  */
@@ -23971,6 +24124,7 @@ var Niivue = class _Niivue {
23971
24124
  __publicField(this, "orientCubeShader");
23972
24125
  __publicField(this, "orientCubeShaderVAO", null);
23973
24126
  __publicField(this, "rectShader");
24127
+ __publicField(this, "rectOutlineShader");
23974
24128
  __publicField(this, "renderShader");
23975
24129
  __publicField(this, "lineShader");
23976
24130
  __publicField(this, "line3DShader");
@@ -24023,7 +24177,20 @@ var Niivue = class _Niivue {
24023
24177
  __publicField(this, "extentsMax");
24024
24178
  // ResizeObserver
24025
24179
  __publicField(this, "resizeObserver", null);
24026
- __publicField(this, "syncOpts", {});
24180
+ // syncOpts: Record<string, unknown> = {}
24181
+ __publicField(this, "syncOpts", {
24182
+ "3d": false,
24183
+ // legacy option
24184
+ "2d": false,
24185
+ // legacy option
24186
+ zoomPan: false,
24187
+ cal_min: false,
24188
+ cal_max: false,
24189
+ clipPlane: false,
24190
+ gamma: false,
24191
+ sliceType: false,
24192
+ crosshair: false
24193
+ });
24027
24194
  __publicField(this, "readyForSync", false);
24028
24195
  // UI Data
24029
24196
  __publicField(this, "uiData", {
@@ -24101,6 +24268,10 @@ var Niivue = class _Niivue {
24101
24268
  Name: "Hemispheric",
24102
24269
  Frag: fragMeshHemiShader
24103
24270
  },
24271
+ {
24272
+ Name: "Crevice",
24273
+ Frag: fragMeshShaderCrevice
24274
+ },
24104
24275
  {
24105
24276
  Name: "Edge",
24106
24277
  Frag: fragMeshEdgeShader
@@ -24498,6 +24669,9 @@ var Niivue = class _Niivue {
24498
24669
  * @see {@link https://niivue.github.io/niivue/features/sync.mesh.html | live demo usage}
24499
24670
  */
24500
24671
  syncWith(otherNV, syncOpts = { "2d": true, "3d": true }) {
24672
+ if (!(otherNV instanceof Array)) {
24673
+ otherNV = [otherNV];
24674
+ }
24501
24675
  this.otherNV = otherNV;
24502
24676
  this.syncOpts = { ...syncOpts };
24503
24677
  }
@@ -24513,9 +24687,55 @@ var Niivue = class _Niivue {
24513
24687
  * @see {@link https://niivue.github.io/niivue/features/sync.mesh.html | live demo usage}
24514
24688
  */
24515
24689
  broadcastTo(otherNV, syncOpts = { "2d": true, "3d": true }) {
24690
+ if (!(otherNV instanceof Array)) {
24691
+ otherNV = [otherNV];
24692
+ }
24516
24693
  this.otherNV = otherNV;
24517
24694
  this.syncOpts = syncOpts;
24518
24695
  }
24696
+ doSync3d(otherNV) {
24697
+ otherNV.scene.renderAzimuth = this.scene.renderAzimuth;
24698
+ otherNV.scene.renderElevation = this.scene.renderElevation;
24699
+ otherNV.scene.volScaleMultiplier = this.scene.volScaleMultiplier;
24700
+ }
24701
+ // both crosshair and zoomPan
24702
+ doSync2d(otherNV) {
24703
+ const thisMM = this.frac2mm(this.scene.crosshairPos);
24704
+ otherNV.scene.crosshairPos = otherNV.mm2frac(thisMM);
24705
+ otherNV.scene.pan2Dxyzmm = vec48.clone(this.scene.pan2Dxyzmm);
24706
+ }
24707
+ doSyncGamma(otherNV) {
24708
+ const thisGamma = this.scene.gamma;
24709
+ const otherGamma = otherNV.scene.gamma;
24710
+ if (thisGamma !== otherGamma) {
24711
+ otherNV.setGamma(thisGamma);
24712
+ }
24713
+ }
24714
+ doSyncZoomPan(otherNV) {
24715
+ otherNV.scene.pan2Dxyzmm = vec48.clone(this.scene.pan2Dxyzmm);
24716
+ }
24717
+ doSyncCrosshair(otherNV) {
24718
+ const thisMM = this.frac2mm(this.scene.crosshairPos);
24719
+ otherNV.scene.crosshairPos = otherNV.mm2frac(thisMM);
24720
+ }
24721
+ doSyncCalMin(otherNV) {
24722
+ if (this.volumes[0].cal_min !== otherNV.volumes[0].cal_min) {
24723
+ otherNV.volumes[0].cal_min = this.volumes[0].cal_min;
24724
+ otherNV.updateGLVolume();
24725
+ }
24726
+ }
24727
+ doSyncCalMax(otherNV) {
24728
+ if (this.volumes[0].cal_max !== otherNV.volumes[0].cal_max) {
24729
+ otherNV.volumes[0].cal_max = this.volumes[0].cal_max;
24730
+ otherNV.updateGLVolume();
24731
+ }
24732
+ }
24733
+ doSyncSliceType(otherNV) {
24734
+ otherNV.setSliceType(this.opts.sliceType);
24735
+ }
24736
+ doSyncClipPlane(otherNV) {
24737
+ otherNV.setClipPlane(this.scene.clipPlaneDepthAziElev);
24738
+ }
24519
24739
  /**
24520
24740
  * Sync the scene controls (orientation, crosshair location, etc.) from one Niivue instance to another. useful for using one canvas to drive another.
24521
24741
  * @internal
@@ -24532,36 +24752,39 @@ var Niivue = class _Niivue {
24532
24752
  if (!this.gl.canvas.matches(":focus")) {
24533
24753
  return;
24534
24754
  }
24535
- const thisMM = this.frac2mm(this.scene.crosshairPos);
24536
- if (this.otherNV instanceof _Niivue) {
24755
+ for (let i = 0; i < this.otherNV.length; i++) {
24756
+ if (this.otherNV[i] === this) {
24757
+ continue;
24758
+ }
24759
+ if (this.syncOpts.gamma) {
24760
+ this.doSyncGamma(this.otherNV[i]);
24761
+ }
24762
+ if (this.syncOpts.crosshair) {
24763
+ this.doSyncCrosshair(this.otherNV[i]);
24764
+ }
24765
+ if (this.syncOpts.zoomPan) {
24766
+ this.doSyncZoomPan(this.otherNV[i]);
24767
+ }
24768
+ if (this.syncOpts.sliceType) {
24769
+ this.doSyncSliceType(this.otherNV[i]);
24770
+ }
24771
+ if (this.syncOpts.cal_min) {
24772
+ this.doSyncCalMin(this.otherNV[i]);
24773
+ }
24774
+ if (this.syncOpts.cal_max) {
24775
+ this.doSyncCalMax(this.otherNV[i]);
24776
+ }
24777
+ if (this.syncOpts.clipPlane) {
24778
+ this.doSyncClipPlane(this.otherNV[i]);
24779
+ }
24537
24780
  if (this.syncOpts["2d"]) {
24538
- this.otherNV.scene.crosshairPos = this.otherNV.mm2frac(thisMM);
24539
- this.otherNV.scene.pan2Dxyzmm = vec48.clone(this.scene.pan2Dxyzmm);
24781
+ this.doSync2d(this.otherNV[i]);
24540
24782
  }
24541
24783
  if (this.syncOpts["3d"]) {
24542
- this.otherNV.scene.renderAzimuth = this.scene.renderAzimuth;
24543
- this.otherNV.scene.renderElevation = this.scene.renderElevation;
24544
- this.otherNV.scene.volScaleMultiplier = this.scene.volScaleMultiplier;
24545
- }
24546
- this.otherNV.drawScene();
24547
- this.otherNV.createOnLocationChange();
24548
- } else if (Array.isArray(this.otherNV)) {
24549
- for (let i = 0; i < this.otherNV.length; i++) {
24550
- if (this.otherNV[i] === this) {
24551
- continue;
24552
- }
24553
- if (this.syncOpts["2d"]) {
24554
- this.otherNV[i].scene.crosshairPos = this.otherNV[i].mm2frac(thisMM);
24555
- this.otherNV[i].scene.pan2Dxyzmm = vec48.clone(this.scene.pan2Dxyzmm);
24556
- }
24557
- if (this.syncOpts["3d"]) {
24558
- this.otherNV[i].scene.renderAzimuth = this.scene.renderAzimuth;
24559
- this.otherNV[i].scene.renderElevation = this.scene.renderElevation;
24560
- this.otherNV[i].scene.volScaleMultiplier = this.scene.volScaleMultiplier;
24561
- }
24562
- this.otherNV[i].drawScene();
24563
- this.otherNV[i].createOnLocationChange();
24784
+ this.doSync3d(this.otherNV[i]);
24564
24785
  }
24786
+ this.otherNV[i].drawScene();
24787
+ this.otherNV[i].createOnLocationChange();
24565
24788
  }
24566
24789
  }
24567
24790
  /** Not documented publicly for now
@@ -24665,6 +24888,10 @@ var Niivue = class _Niivue {
24665
24888
  const [x, y] = [pos.x * this.uiData.dpr, pos.y * this.uiData.dpr];
24666
24889
  const label = this.getLabelAtPoint([x, y]);
24667
24890
  if (label) {
24891
+ if (label.onClick) {
24892
+ label.onClick(label);
24893
+ return;
24894
+ }
24668
24895
  for (const mesh of this.meshes) {
24669
24896
  if (mesh.type !== "connectome" /* CONNECTOME */) {
24670
24897
  continue;
@@ -24874,6 +25101,9 @@ var Niivue = class _Niivue {
24874
25101
  const fracStart = this.canvasPos2frac([this.uiData.dragStart[0], this.uiData.dragStart[1]]);
24875
25102
  const fracEnd = this.canvasPos2frac([this.uiData.dragEnd[0], this.uiData.dragEnd[1]]);
24876
25103
  this.generateMouseUpCallback(fracStart, fracEnd);
25104
+ if (this.opts.dragMode === 6 /* roiSelection */) {
25105
+ return;
25106
+ }
24877
25107
  if (this.opts.dragMode !== 1 /* contrast */) {
24878
25108
  return;
24879
25109
  }
@@ -24979,6 +25209,15 @@ var Niivue = class _Niivue {
24979
25209
  this.drawScene();
24980
25210
  this.uiData.prevX = this.uiData.currX;
24981
25211
  this.uiData.prevY = this.uiData.currY;
25212
+ } else if (!this.uiData.mousedown && this.opts.clickToSegment) {
25213
+ const pos = this.getNoPaddingNoBorderCanvasRelativeMousePosition(e, this.gl.canvas);
25214
+ if (!pos) {
25215
+ return;
25216
+ }
25217
+ const x = pos.x * this.uiData.dpr;
25218
+ const y = pos.y * this.uiData.dpr;
25219
+ this.mousePos = [x, y];
25220
+ this.drawScene();
24982
25221
  }
24983
25222
  }
24984
25223
  // not included in public docs
@@ -25503,7 +25742,6 @@ var Niivue = class _Niivue {
25503
25742
  this.loadDrawing(volume);
25504
25743
  } else {
25505
25744
  this.addVolume(volume);
25506
- this.setDrawingEnabled(true);
25507
25745
  }
25508
25746
  }).catch((e2) => {
25509
25747
  throw e2;
@@ -27437,6 +27675,23 @@ var Niivue = class _Niivue {
27437
27675
  y = Math.min(Math.max(y, 0), dy - 1);
27438
27676
  z = Math.min(Math.max(z, 0), dz - 1);
27439
27677
  this.drawBitmap[x + y * dx + z * dx * dy] = penValue;
27678
+ const isAx = this.drawPenAxCorSag === 0;
27679
+ const isCor = this.drawPenAxCorSag === 1;
27680
+ const isSag = this.drawPenAxCorSag === 2;
27681
+ if (this.opts.penSize > 1) {
27682
+ const halfPenSize = Math.floor(this.opts.penSize / 2);
27683
+ for (let i = -halfPenSize; i <= halfPenSize; i++) {
27684
+ for (let j = -halfPenSize; j <= halfPenSize; j++) {
27685
+ if (isAx) {
27686
+ this.drawBitmap[x + i + (y + j) * dx + z * dx * dy] = penValue;
27687
+ } else if (isCor) {
27688
+ this.drawBitmap[x + i + y * dx + (z + j) * dx * dy] = penValue;
27689
+ } else if (isSag) {
27690
+ this.drawBitmap[x + (y + j) * dx + (z + i) * dx * dy] = penValue;
27691
+ }
27692
+ }
27693
+ }
27694
+ }
27440
27695
  }
27441
27696
  // not included in public docs
27442
27697
  // create line between to voxels in drawing
@@ -27518,6 +27773,98 @@ var Niivue = class _Niivue {
27518
27773
  }
27519
27774
  }
27520
27775
  }
27776
+ /**
27777
+ * Performs a 1-voxel binary dilation on a connected cluster within the drawing mask using the drawFloodFillCore function.
27778
+ *
27779
+ * @param seedXYZ - voxel index of the seed voxel in the mask array.
27780
+ * @param neighbors - Number of neighbors to consider for connectivity and dilation (6, 18, or 26).
27781
+ */
27782
+ drawingBinaryDilationWithSeed(seedXYZ, neighbors = 6) {
27783
+ try {
27784
+ let xyz2vx = function(pt) {
27785
+ return pt[0] + pt[1] * nx + pt[2] * nxy;
27786
+ };
27787
+ const mask = this.drawBitmap;
27788
+ const xDim = this.back.dims[1];
27789
+ const yDim = this.back.dims[2];
27790
+ const zDim = this.back.dims[3];
27791
+ const nx = xDim;
27792
+ const nxy = xDim * yDim;
27793
+ const totalVoxels = nxy * zDim;
27794
+ const seedIndex = xyz2vx(seedXYZ);
27795
+ if (seedIndex < 0 || seedIndex >= totalVoxels) {
27796
+ throw new Error("Seed index is out of bounds.");
27797
+ }
27798
+ const seedValue = mask[seedIndex];
27799
+ if (seedValue === 0) {
27800
+ throw new Error("Seed voxel is not part of a filled cluster.");
27801
+ }
27802
+ const img = mask.slice();
27803
+ for (let i = 0; i < totalVoxels; i++) {
27804
+ img[i] = img[i] === seedValue ? 1 : 0;
27805
+ }
27806
+ this.drawFloodFillCore(img, seedIndex, neighbors);
27807
+ const outputMask = mask.slice();
27808
+ const neighborOffsets = [];
27809
+ const offsets6 = [-nxy, nxy, -xDim, xDim, -1, 1];
27810
+ neighborOffsets.push(...offsets6);
27811
+ if (neighbors > 6) {
27812
+ neighborOffsets.push(
27813
+ -xDim - 1,
27814
+ -xDim + 1,
27815
+ xDim - 1,
27816
+ xDim + 1,
27817
+ -nxy - xDim,
27818
+ -nxy + xDim,
27819
+ -nxy - 1,
27820
+ -nxy + 1,
27821
+ nxy - xDim,
27822
+ nxy + xDim,
27823
+ nxy - 1,
27824
+ nxy + 1
27825
+ );
27826
+ }
27827
+ if (neighbors > 18) {
27828
+ neighborOffsets.push(
27829
+ -nxy - xDim - 1,
27830
+ -nxy - xDim + 1,
27831
+ -nxy + xDim - 1,
27832
+ -nxy + xDim + 1,
27833
+ nxy - xDim - 1,
27834
+ nxy - xDim + 1,
27835
+ nxy + xDim - 1,
27836
+ nxy + xDim + 1
27837
+ );
27838
+ }
27839
+ for (let idx = 0; idx < totalVoxels; idx++) {
27840
+ if (img[idx] === 2) {
27841
+ const x = idx % xDim;
27842
+ const y = Math.floor(idx % nxy / xDim);
27843
+ const z = Math.floor(idx / nxy);
27844
+ for (const offset of neighborOffsets) {
27845
+ const neighborIdx = idx + offset;
27846
+ if (neighborIdx < 0 || neighborIdx >= totalVoxels) {
27847
+ continue;
27848
+ }
27849
+ const nx2 = neighborIdx % xDim;
27850
+ const ny = Math.floor(neighborIdx % nxy / xDim);
27851
+ const nz = Math.floor(neighborIdx / nxy);
27852
+ if (Math.abs(nx2 - x) > 1 || Math.abs(ny - y) > 1 || Math.abs(nz - z) > 1) {
27853
+ continue;
27854
+ }
27855
+ if (mask[neighborIdx] === 0) {
27856
+ outputMask[neighborIdx] = seedValue;
27857
+ }
27858
+ }
27859
+ }
27860
+ }
27861
+ this.drawBitmap = outputMask;
27862
+ this.drawAddUndoBitmap();
27863
+ this.refreshDrawing(true);
27864
+ } catch (error) {
27865
+ log.error("Error in drawingBinaryDilationWithSeed:", error);
27866
+ }
27867
+ }
27521
27868
  // a voxel can be defined as having 6, 18 or 26 neighbors:
27522
27869
  // 6: neighbors share faces (distance=1)
27523
27870
  // 18: neighbors share faces (distance=1) or edges (1.4)
@@ -27599,7 +27946,7 @@ var Niivue = class _Niivue {
27599
27946
  }
27600
27947
  // not included in public docs
27601
27948
  // set all connected voxels in drawing to new color
27602
- drawFloodFill(seedXYZ, newColor = 0, growSelectedCluster = 0, forceMin = NaN, forceMax = NaN, neighbors = 6) {
27949
+ drawFloodFill(seedXYZ, newColor = 0, growSelectedCluster = 0, forceMin = NaN, forceMax = NaN, neighbors = 6, maxDistanceMM = Number.POSITIVE_INFINITY) {
27603
27950
  if (!this.drawBitmap) {
27604
27951
  throw new Error("drawBitmap undefined");
27605
27952
  }
@@ -27621,9 +27968,25 @@ var Niivue = class _Niivue {
27621
27968
  if (img.length !== nxy * dims[2]) {
27622
27969
  return;
27623
27970
  }
27971
+ function vx2xyz(vx) {
27972
+ const Z = Math.floor(vx / nxy);
27973
+ const Y = Math.floor((vx - Z * nxy) / nx);
27974
+ const X = Math.floor(vx % nx);
27975
+ return [X, Y, Z];
27976
+ }
27624
27977
  function xyz2vx(pt) {
27625
27978
  return pt[0] + pt[1] * nx + pt[2] * nxy;
27626
27979
  }
27980
+ const vx2mm = (xyz) => {
27981
+ return this.vox2mm(xyz, this.back.matRAS);
27982
+ };
27983
+ const seedMM = vx2mm(seedXYZ);
27984
+ const maxDistanceMM2 = maxDistanceMM ** 2;
27985
+ function isWithinDistance(vx) {
27986
+ const xyzMM = vx2mm(vx2xyz(vx));
27987
+ const dist2 = (xyzMM[0] - seedMM[0]) ** 2 + (xyzMM[1] - seedMM[1]) ** 2 + (xyzMM[2] - seedMM[2]) ** 2;
27988
+ return dist2 <= maxDistanceMM2;
27989
+ }
27627
27990
  const seedVx = xyz2vx(seedXYZ);
27628
27991
  const seedColor = img[seedVx];
27629
27992
  if (seedColor === newColor) {
@@ -27637,6 +28000,9 @@ var Niivue = class _Niivue {
27637
28000
  for (let i = 1; i < nxyz; i++) {
27638
28001
  img[i] = 0;
27639
28002
  if (this.drawBitmap[i] === seedColor) {
28003
+ if (!isWithinDistance(i)) {
28004
+ continue;
28005
+ }
27640
28006
  img[i] = 1;
27641
28007
  }
27642
28008
  }
@@ -27666,6 +28032,9 @@ var Niivue = class _Niivue {
27666
28032
  for (let i = 1; i < nxyz; i++) {
27667
28033
  img[i] = 0;
27668
28034
  if (backImg[i] >= mn && backImg[i] <= mx) {
28035
+ if (!isWithinDistance(i)) {
28036
+ continue;
28037
+ }
27669
28038
  img[i] = 1;
27670
28039
  }
27671
28040
  }
@@ -28287,6 +28656,8 @@ var Niivue = class _Niivue {
28287
28656
  gl.bindVertexArray(this.unusedVAO);
28288
28657
  this.rectShader = new Shader(gl, vertRectShader, fragRectShader);
28289
28658
  this.rectShader.use(gl);
28659
+ this.rectOutlineShader = new Shader(gl, vertRectShader, fragRectOutlineShader);
28660
+ this.rectOutlineShader.use(gl);
28290
28661
  this.lineShader = new Shader(gl, vertLineShader, fragRectShader);
28291
28662
  this.lineShader.use(gl);
28292
28663
  this.line3DShader = new Shader(gl, vertLine3DShader, fragRectShader);
@@ -28439,14 +28810,37 @@ var Niivue = class _Niivue {
28439
28810
  }
28440
28811
  /**
28441
28812
  * basic statistics for selected voxel-based image
28442
- * @param layer - selects image to describe
28443
- * @param masks - are optional binary images to filter voxles
28444
- * @returns numeric values to describe image
28445
- * @example niivue.getDescriptives(0);
28813
+ * @param options - an object containing the following properties:
28814
+ * - layer: selects image to describe
28815
+ * - masks: optional binary images to filter voxels
28816
+ * - drawingIsMask: a boolean indicating if the drawing is used as a mask
28817
+ * - roiIsMask: a boolean indicating if the ROI is used as a mask
28818
+ * - startVox: the starting voxel coordinates
28819
+ * - endVox: the ending voxel coordinates
28820
+ * @returns numeric values to describe image or regions of images
28821
+ * @example
28822
+ * niivue.getDescriptives({
28823
+ * layer: 0,
28824
+ * masks: [],
28825
+ * drawingIsMask: true, // drawingIsMask and roiIsMask are mutually exclusive
28826
+ * roiIsMask: false,
28827
+ * startVox: [10, 20, 30], // ignored if roiIsMask is false
28828
+ * endVox: [40, 50, 60] // ignored if roiIsMask is false
28829
+ * });
28446
28830
  * @see {@link https://niivue.github.io/niivue/features/draw2.html | live demo usage}
28447
28831
  */
28448
- getDescriptives(layer = 0, masks = [], drawingIsMask = false) {
28832
+ getDescriptives(options) {
28833
+ const {
28834
+ layer = 0,
28835
+ masks = [],
28836
+ drawingIsMask = false,
28837
+ roiIsMask = false,
28838
+ startVox = [0, 0, 0],
28839
+ endVox = [0, 0, 0]
28840
+ } = options;
28841
+ let area = null;
28449
28842
  const hdr = this.volumes[layer].hdr;
28843
+ const pixDimsRAS = this.volumes[layer].pixDimsRAS;
28450
28844
  let slope = hdr.scl_slope;
28451
28845
  if (isNaN(slope)) {
28452
28846
  slope = 1;
@@ -28484,6 +28878,37 @@ var Niivue = class _Niivue {
28484
28878
  mask[i] = 0;
28485
28879
  }
28486
28880
  }
28881
+ } else if (masks.length < 1 && roiIsMask) {
28882
+ mask.fill(0);
28883
+ console.log("startVox", startVox);
28884
+ console.log("endVox", endVox);
28885
+ const xrange = this.calculateMinMaxVoxIdx([startVox[0], endVox[0]]);
28886
+ const yrange = this.calculateMinMaxVoxIdx([startVox[1], endVox[1]]);
28887
+ const zrange = this.calculateMinMaxVoxIdx([startVox[2], endVox[2]]);
28888
+ if (startVox[0] - endVox[0] === 0) {
28889
+ xrange[1] = startVox[0] + 1;
28890
+ } else if (startVox[1] - endVox[1] === 0) {
28891
+ yrange[1] = startVox[1] + 1;
28892
+ } else if (startVox[2] - endVox[2] === 0) {
28893
+ zrange[1] = startVox[2] + 1;
28894
+ }
28895
+ const xdim = hdr.dims[1];
28896
+ const ydim = hdr.dims[2];
28897
+ for (let z = zrange[0]; z < zrange[1]; z++) {
28898
+ const zi = z * xdim * ydim;
28899
+ for (let y = yrange[0]; y < yrange[1]; y++) {
28900
+ const yi = y * xdim;
28901
+ for (let x = xrange[0]; x < xrange[1]; x++) {
28902
+ const index = zi + yi + x;
28903
+ mask[index] = 1;
28904
+ }
28905
+ }
28906
+ }
28907
+ const xmm = Math.abs(pixDimsRAS[1] * (endVox[0] - startVox[0]));
28908
+ const ymm = Math.abs(pixDimsRAS[2] * (endVox[1] - startVox[1]));
28909
+ const zmm = Math.abs(pixDimsRAS[3] * (endVox[2] - startVox[2]));
28910
+ const mms = [xmm, ymm, zmm];
28911
+ area = mms.filter((m) => m !== 0).reduce((a, b) => a * b, 1);
28487
28912
  }
28488
28913
  let k = 0;
28489
28914
  let M = 0;
@@ -28537,7 +28962,8 @@ var Niivue = class _Niivue {
28537
28962
  cal_min: this.volumes[layer].cal_min,
28538
28963
  cal_max: this.volumes[layer].cal_max,
28539
28964
  robust_min: this.volumes[layer].robust_min,
28540
- robust_max: this.volumes[layer].robust_max
28965
+ robust_max: this.volumes[layer].robust_max,
28966
+ area
28541
28967
  };
28542
28968
  }
28543
28969
  // not included in public docs
@@ -29728,6 +30154,7 @@ var Niivue = class _Niivue {
29728
30154
  * @see {@link https://niivue.github.io/niivue/features/colormaps.html | live demo usage}
29729
30155
  */
29730
30156
  setGamma(gamma = 1) {
30157
+ this.scene.gamma = gamma;
29731
30158
  cmapper.gamma = gamma;
29732
30159
  this.updateGLVolume();
29733
30160
  }
@@ -30092,27 +30519,50 @@ var Niivue = class _Niivue {
30092
30519
  if (this.opts.drawingEnabled) {
30093
30520
  const pt = this.frac2vox(this.scene.crosshairPos);
30094
30521
  if (this.opts.clickToSegment) {
30095
- const radius = this.opts.clickToSegmentRadius;
30096
- const steps = this.opts.clickToSegmentSteps;
30522
+ const voxelIntensity = this.back.getValue(pt[0], pt[1], pt[2]);
30523
+ if (this.opts.clickToSegmentPercent > 0 && this.opts.clickToSegmentPercent <= 1) {
30524
+ this.opts.clickToSegmentIntensityMax = voxelIntensity * (1 + this.opts.clickToSegmentPercent);
30525
+ this.opts.clickToSegmentIntensityMin = voxelIntensity * (1 - this.opts.clickToSegmentPercent);
30526
+ }
30527
+ if (this.opts.clickToSegmentAutoIntensity) {
30528
+ if (voxelIntensity > (this.back.cal_min + this.back.cal_max) * 0.5) {
30529
+ this.opts.clickToSegmentBright = true;
30530
+ } else {
30531
+ this.opts.clickToSegmentBright = false;
30532
+ }
30533
+ }
30097
30534
  const brightOrDark = this.opts.clickToSegmentBright ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
30098
- this.drawPenFillPts = [];
30099
30535
  this.drawPenAxCorSag = axCorSag;
30100
- for (let i2 = 1; i2 <= steps; i2++) {
30101
- const angle = i2 / steps * 2 * Math.PI;
30102
- let xVox = pt[0] + radius * Math.cos(angle);
30103
- let yVox = pt[1] + radius * Math.sin(angle);
30104
- const zVox = pt[2];
30105
- xVox = Math.round(xVox);
30106
- yVox = Math.round(yVox);
30107
- this.drawPt(xVox, yVox, zVox, this.opts.penValue);
30108
- this.drawPenFillPts.push([xVox, yVox, pt[2]]);
30109
- if (i2 === steps) {
30110
- this.drawFloodFill([xVox, yVox, pt[2]], 0, brightOrDark, NaN, NaN, this.opts.floodFillNeighbors);
30111
- }
30536
+ if (this.drawPenAxCorSag === 0 /* AXIAL */) {
30537
+ const pixDims = [this.back.pixDimsRAS[0], this.back.pixDimsRAS[1]];
30538
+ const minPixDim = Math.min(...pixDims);
30539
+ this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim);
30540
+ } else if (this.drawPenAxCorSag === 1 /* CORONAL */) {
30541
+ const pixDims = [this.back.pixDimsRAS[0], this.back.pixDimsRAS[2]];
30542
+ const minPixDim = Math.min(...pixDims);
30543
+ this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim);
30544
+ } else if (this.drawPenAxCorSag === 2 /* SAGITTAL */) {
30545
+ const pixDims = [this.back.pixDimsRAS[1], this.back.pixDimsRAS[2]];
30546
+ const minPixDim = Math.min(...pixDims);
30547
+ this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim);
30112
30548
  }
30549
+ this.drawPt(pt[0], pt[1], pt[2], this.opts.penValue);
30550
+ this.drawFloodFill(
30551
+ [pt[0], pt[1], pt[2]],
30552
+ 0,
30553
+ brightOrDark,
30554
+ this.opts.clickToSegmentIntensityMin,
30555
+ this.opts.clickToSegmentIntensityMax,
30556
+ this.opts.floodFillNeighbors,
30557
+ this.opts.clickToSegmentMaxDistanceMM
30558
+ );
30113
30559
  this.drawScene();
30114
30560
  this.createOnLocationChange(axCorSag);
30115
- const info = this.getDescriptives(0, [], true);
30561
+ const info = this.getDescriptives({
30562
+ layer: 0,
30563
+ masks: [],
30564
+ drawingIsMask: true
30565
+ });
30116
30566
  this.onClickToSegment({ mL: info.volumeML, mm3: info.volumeMM3 });
30117
30567
  return;
30118
30568
  }
@@ -30330,20 +30780,41 @@ var Niivue = class _Niivue {
30330
30780
  if (!this.rectShader) {
30331
30781
  throw new Error("rectShader undefined");
30332
30782
  }
30333
- this.rectShader.use(this.gl);
30334
- this.gl.enable(this.gl.BLEND);
30335
- this.gl.uniform4fv(this.rectShader.uniforms.lineColor, lineColor);
30336
- this.gl.uniform2fv(this.rectShader.uniforms.canvasWidthHeight, [this.gl.canvas.width, this.gl.canvas.height]);
30337
- this.gl.uniform4f(
30338
- this.rectShader.uniforms.leftTopWidthHeight,
30339
- leftTopWidthHeight[0],
30340
- leftTopWidthHeight[1],
30341
- leftTopWidthHeight[2],
30342
- leftTopWidthHeight[3]
30343
- );
30344
- this.gl.bindVertexArray(this.genericVAO);
30345
- this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
30346
- this.gl.bindVertexArray(this.unusedVAO);
30783
+ if (!this.opts.selectionBoxIsOutline) {
30784
+ this.rectShader.use(this.gl);
30785
+ this.gl.enable(this.gl.BLEND);
30786
+ this.gl.uniform4fv(this.rectShader.uniforms.lineColor, lineColor);
30787
+ this.gl.uniform2fv(this.rectShader.uniforms.canvasWidthHeight, [this.gl.canvas.width, this.gl.canvas.height]);
30788
+ this.gl.uniform4f(
30789
+ this.rectShader.uniforms.leftTopWidthHeight,
30790
+ leftTopWidthHeight[0],
30791
+ leftTopWidthHeight[1],
30792
+ leftTopWidthHeight[2],
30793
+ leftTopWidthHeight[3]
30794
+ );
30795
+ this.gl.bindVertexArray(this.genericVAO);
30796
+ this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
30797
+ this.gl.bindVertexArray(this.unusedVAO);
30798
+ } else {
30799
+ this.rectOutlineShader.use(this.gl);
30800
+ this.gl.enable(this.gl.BLEND);
30801
+ this.gl.uniform1f(this.rectOutlineShader.uniforms.thickness, this.opts.selectionBoxLineThickness);
30802
+ this.gl.uniform4fv(this.rectOutlineShader.uniforms.lineColor, lineColor);
30803
+ this.gl.uniform2fv(this.rectOutlineShader.uniforms.canvasWidthHeight, [
30804
+ this.gl.canvas.width,
30805
+ this.gl.canvas.height
30806
+ ]);
30807
+ this.gl.uniform4f(
30808
+ this.rectOutlineShader.uniforms.leftTopWidthHeight,
30809
+ leftTopWidthHeight[0],
30810
+ leftTopWidthHeight[1],
30811
+ leftTopWidthHeight[2],
30812
+ leftTopWidthHeight[3]
30813
+ );
30814
+ this.gl.bindVertexArray(this.genericVAO);
30815
+ this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
30816
+ this.gl.bindVertexArray(this.unusedVAO);
30817
+ }
30347
30818
  }
30348
30819
  drawCircle(leftTopWidthHeight, circleColor = this.opts.fontColor, fillPercent = 1) {
30349
30820
  if (!this.circleShader) {
@@ -30387,9 +30858,21 @@ var Niivue = class _Niivue {
30387
30858
  const labels = [...this.document.labels, ...definedMeshLabels];
30388
30859
  return labels;
30389
30860
  }
30861
+ getConnectomeLabels() {
30862
+ const connectomes = this.meshes.filter((m) => m.type === "connectome" /* CONNECTOME */);
30863
+ const meshNodes = connectomes.flatMap((m) => m.nodes);
30864
+ const meshLabels = meshNodes.map((n) => n.label);
30865
+ const definedMeshLabels = meshLabels.filter((l) => l !== void 0);
30866
+ const nonAnchoredLabels = this.document.labels.filter((l) => l.anchor == null || l.anchor === 0 /* NONE */);
30867
+ const nonAnchoredLabelSet = new Set(definedMeshLabels);
30868
+ for (const label of nonAnchoredLabels) {
30869
+ nonAnchoredLabelSet.add(label);
30870
+ }
30871
+ return Array.from(nonAnchoredLabelSet);
30872
+ }
30390
30873
  getBulletMarginWidth() {
30391
30874
  let bulletMargin = 0;
30392
- const labels = this.getAllLabels();
30875
+ const labels = this.getConnectomeLabels();
30393
30876
  if (labels.length === 0) {
30394
30877
  return 0;
30395
30878
  }
@@ -30406,7 +30889,7 @@ var Niivue = class _Niivue {
30406
30889
  return bulletMargin;
30407
30890
  }
30408
30891
  getLegendPanelWidth() {
30409
- const labels = this.getAllLabels();
30892
+ const labels = this.getConnectomeLabels();
30410
30893
  if (!this.opts.showLegend || labels.length === 0) {
30411
30894
  return 0;
30412
30895
  }
@@ -30432,7 +30915,7 @@ var Niivue = class _Niivue {
30432
30915
  return width;
30433
30916
  }
30434
30917
  getLegendPanelHeight() {
30435
- const labels = this.getAllLabels();
30918
+ const labels = this.getConnectomeLabels();
30436
30919
  let height = 0;
30437
30920
  const scale = 1;
30438
30921
  const verticalMargin = this.opts.textHeight * this.gl.canvas.height * scale;
@@ -30874,7 +31357,7 @@ var Niivue = class _Niivue {
30874
31357
  }
30875
31358
  // not included in public docs
30876
31359
  // show text labels for L/R, A/P, I/S dimensions
30877
- drawSliceOrientationText(leftTopWidthHeight, axCorSag) {
31360
+ drawSliceOrientationText(leftTopWidthHeight, axCorSag, padLeftTop = [NaN, NaN]) {
30878
31361
  this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
30879
31362
  let topText = "S";
30880
31363
  if (axCorSag === 0 /* AXIAL */) {
@@ -30888,8 +31371,32 @@ var Niivue = class _Niivue {
30888
31371
  this.drawTextRightBelow([leftTopWidthHeight[0], leftTopWidthHeight[1]], leftText + topText);
30889
31372
  return;
30890
31373
  }
30891
- this.drawTextBelow([leftTopWidthHeight[0] + leftTopWidthHeight[2] * 0.5, leftTopWidthHeight[1]], topText);
30892
- this.drawTextRight([leftTopWidthHeight[0], leftTopWidthHeight[1] + leftTopWidthHeight[3] * 0.5], leftText);
31374
+ let drawBelow = true;
31375
+ let drawRight = true;
31376
+ if (!isNaN(padLeftTop[0])) {
31377
+ const ht = this.opts.textHeight * this.gl.canvas.height + 2;
31378
+ if (padLeftTop[1] > ht) {
31379
+ this.drawTextBelow(
31380
+ [leftTopWidthHeight[0] + leftTopWidthHeight[2] * 0.5, leftTopWidthHeight[1] + padLeftTop[1] - ht],
31381
+ topText
31382
+ );
31383
+ drawBelow = false;
31384
+ }
31385
+ const wid = this.textWidth(ht, leftText) + 2;
31386
+ if (padLeftTop[0] > wid) {
31387
+ this.drawTextRight(
31388
+ [leftTopWidthHeight[0] + padLeftTop[0] - wid, leftTopWidthHeight[1] + leftTopWidthHeight[3] * 0.5],
31389
+ leftText
31390
+ );
31391
+ drawRight = false;
31392
+ }
31393
+ }
31394
+ if (drawBelow) {
31395
+ this.drawTextBelow([leftTopWidthHeight[0] + leftTopWidthHeight[2] * 0.5, leftTopWidthHeight[1]], topText);
31396
+ }
31397
+ if (drawRight) {
31398
+ this.drawTextRight([leftTopWidthHeight[0], leftTopWidthHeight[1] + leftTopWidthHeight[3] * 0.5], leftText);
31399
+ }
30893
31400
  }
30894
31401
  // not included in public docs
30895
31402
  xyMM2xyzMM(axCorSag, sliceFrac) {
@@ -30927,7 +31434,7 @@ var Niivue = class _Niivue {
30927
31434
  }
30928
31435
  // not included in public docs
30929
31436
  // draw 2D tile
30930
- draw2D(leftTopWidthHeight, axCorSag, customMM = NaN) {
31437
+ draw2DMain(leftTopWidthHeight, axCorSag, customMM = NaN) {
30931
31438
  let frac2mmTexture = this.volumes[0].frac2mm.slice();
30932
31439
  let screen = this.screenFieldOfViewExtendedMM(axCorSag);
30933
31440
  let mesh2ortho = mat46.create();
@@ -30956,7 +31463,9 @@ var Niivue = class _Niivue {
30956
31463
  elevation = isRadiolgical ? -90 : 90;
30957
31464
  }
30958
31465
  const gl = this.gl;
31466
+ let isStretchToScreen = false;
30959
31467
  if (leftTopWidthHeight[2] === 0 || leftTopWidthHeight[3] === 0) {
31468
+ isStretchToScreen = true;
30960
31469
  const pixPerMMw = gl.canvas.width / screen.fovMM[0];
30961
31470
  const pixPerMMh = gl.canvas.height / screen.fovMM[1];
30962
31471
  const pixPerMMmin = Math.min(pixPerMMw, pixPerMMh);
@@ -31103,9 +31612,32 @@ var Niivue = class _Niivue {
31103
31612
  if (isNaN(customMM)) {
31104
31613
  this.drawCrosshairs3D(false, 0.15, obj.modelViewProjectionMatrix, true, this.opts.isSliceMM);
31105
31614
  }
31106
- this.drawSliceOrientationText(leftTopWidthHeight, axCorSag);
31615
+ if (isStretchToScreen) {
31616
+ this.drawSliceOrientationText(leftTopWidthHeight, axCorSag);
31617
+ }
31107
31618
  this.readyForSync = true;
31108
31619
  }
31620
+ draw2D(leftTopWidthHeight, axCorSag, customMM = NaN, imageWidthHeight = [NaN, NaN]) {
31621
+ const padLeftTop = [NaN, NaN];
31622
+ if (isNaN(imageWidthHeight[0])) {
31623
+ this.draw2DMain(leftTopWidthHeight, axCorSag, customMM);
31624
+ } else {
31625
+ const ltwh = leftTopWidthHeight.slice();
31626
+ padLeftTop[0] = Math.floor(0.5 * (ltwh[2] - imageWidthHeight[0]));
31627
+ padLeftTop[1] = Math.floor(0.5 * (ltwh[3] - imageWidthHeight[1]));
31628
+ ltwh[0] += padLeftTop[0];
31629
+ ltwh[1] += padLeftTop[1];
31630
+ ltwh[2] = imageWidthHeight[0];
31631
+ ltwh[3] = imageWidthHeight[1];
31632
+ this.draw2DMain(ltwh, axCorSag, customMM);
31633
+ }
31634
+ if (customMM === Infinity || customMM === -Infinity || axCorSag === 4 /* RENDER */) {
31635
+ return;
31636
+ }
31637
+ if (leftTopWidthHeight[2] !== 0 && leftTopWidthHeight[3] !== 0) {
31638
+ this.drawSliceOrientationText(leftTopWidthHeight, axCorSag, padLeftTop);
31639
+ }
31640
+ }
31109
31641
  // not included in public docs
31110
31642
  // determine 3D model view projection matrix
31111
31643
  calculateMvpMatrix(_unused, leftTopWidthHeight = [0, 0, 0, 0], azimuth, elevation) {
@@ -31716,7 +32248,7 @@ var Niivue = class _Niivue {
31716
32248
  * @param style - label style
31717
32249
  * @param point - 3D point on the model
31718
32250
  */
31719
- addLabel(text, style, points) {
32251
+ addLabel(text, style, points, anchor, onClick) {
31720
32252
  const defaultStyle = {
31721
32253
  textColor: this.opts.legendTextColor,
31722
32254
  textScale: 1,
@@ -31728,7 +32260,7 @@ var Niivue = class _Niivue {
31728
32260
  bulletColor: this.opts.legendTextColor
31729
32261
  };
31730
32262
  const labelStyle = style ? { ...defaultStyle, ...style } : { ...defaultStyle };
31731
- const label = new NVLabel3D(text, labelStyle, points);
32263
+ const label = new NVLabel3D(text, { ...labelStyle }, points, anchor, onClick);
31732
32264
  this.document.labels.push(label);
31733
32265
  return label;
31734
32266
  }
@@ -31746,6 +32278,60 @@ var Niivue = class _Niivue {
31746
32278
  return screenPoint;
31747
32279
  }
31748
32280
  getLabelAtPoint(screenPoint) {
32281
+ const scale = 1;
32282
+ const size = this.opts.textHeight * Math.min(this.gl.canvas.height, this.gl.canvas.width) * scale;
32283
+ const verticalMargin = this.opts.textHeight * this.gl.canvas.height * scale;
32284
+ for (const label of this.document.labels) {
32285
+ if (label.anchor == null || label.anchor === 0 /* NONE */) {
32286
+ continue;
32287
+ }
32288
+ const labelSize = this.opts.textHeight * this.gl.canvas.height * label.style.textScale;
32289
+ const textHeight = this.textHeight(labelSize, label.text);
32290
+ const textWidth = this.textWidth(labelSize, label.text);
32291
+ if (label.anchor & 1 /* LEFT */) {
32292
+ if (screenPoint[0] > textWidth) {
32293
+ continue;
32294
+ }
32295
+ }
32296
+ if (label.anchor & 2 /* CENTER */) {
32297
+ if (screenPoint[0] < (this.gl.canvas.width - textWidth) / 2) {
32298
+ continue;
32299
+ }
32300
+ if (screenPoint[0] > (this.gl.canvas.width + textWidth) / 2) {
32301
+ continue;
32302
+ }
32303
+ }
32304
+ if (label.anchor & 4 /* RIGHT */) {
32305
+ if (screenPoint[0] < this.gl.canvas.width - textWidth) {
32306
+ continue;
32307
+ }
32308
+ }
32309
+ if (label.anchor & 8 /* TOP */) {
32310
+ if (screenPoint[1] < verticalMargin / 2) {
32311
+ continue;
32312
+ }
32313
+ if (screenPoint[1] > textHeight + verticalMargin / 2) {
32314
+ continue;
32315
+ }
32316
+ }
32317
+ if (label.anchor & 16 /* MIDDLE */) {
32318
+ if (screenPoint[1] < (this.gl.canvas.height - textHeight - verticalMargin) / 2) {
32319
+ continue;
32320
+ }
32321
+ if (screenPoint[1] > (this.gl.canvas.height + textHeight - verticalMargin / 2) / 2) {
32322
+ continue;
32323
+ }
32324
+ }
32325
+ if (label.anchor & 32 /* BOTTOM */) {
32326
+ if (screenPoint[1] < this.gl.canvas.height - textHeight - verticalMargin) {
32327
+ continue;
32328
+ }
32329
+ if (screenPoint[1] > this.gl.canvas.height - verticalMargin / 2) {
32330
+ continue;
32331
+ }
32332
+ }
32333
+ return label;
32334
+ }
31749
32335
  log.debug("screenPoint", screenPoint);
31750
32336
  const panelHeight = this.getLegendPanelHeight();
31751
32337
  const panelWidth = this.getLegendPanelWidth();
@@ -31755,9 +32341,7 @@ var Niivue = class _Niivue {
31755
32341
  if (screenPoint[0] < left || screenPoint[1] < top || screenPoint[0] > left + panelWidth || screenPoint[1] > top + panelHeight) {
31756
32342
  return null;
31757
32343
  }
31758
- const scale = 1;
31759
- const size = this.opts.textHeight * Math.min(this.gl.canvas.height, this.gl.canvas.width) * scale;
31760
- const labels = this.getAllLabels();
32344
+ const labels = this.getConnectomeLabels();
31761
32345
  for (const label of labels) {
31762
32346
  const labelSize = this.opts.textHeight * this.gl.canvas.height * label.style.textScale;
31763
32347
  const textHeight = this.textHeight(labelSize, label.text);
@@ -31786,7 +32370,7 @@ var Niivue = class _Niivue {
31786
32370
  }
31787
32371
  }
31788
32372
  // not included in public docs
31789
- draw3DLabel(label, pos, mvpMatrix, leftTopWidthHeight, bulletMargin = 0, legendWidth, secondPass = false) {
32373
+ draw3DLabel(label, pos, mvpMatrix, leftTopWidthHeight, bulletMargin, legendWidth, secondPass) {
31790
32374
  const text = label.text;
31791
32375
  const left = pos[0];
31792
32376
  const top = pos[1];
@@ -31803,22 +32387,24 @@ var Niivue = class _Niivue {
31803
32387
  this.drawCircle([rectLeft, rectTop, bulletSize, bulletSize], label.style.bulletColor);
31804
32388
  }
31805
32389
  let textLeft = left;
31806
- if (label.style.textAlignment !== "left" /* LEFT */) {
31807
- const textWidth = this.textWidth(label.style.textScale, label.text) * size;
31808
- if (label.style.textAlignment === "right" /* RIGHT */) {
31809
- textLeft = left + legendWidth - size * 1.5 - textWidth;
32390
+ if (legendWidth) {
32391
+ if (label.style.textAlignment !== "left" /* LEFT */) {
32392
+ const textWidth = this.textWidth(label.style.textScale, label.text) * size;
32393
+ if (label.style.textAlignment === "right" /* RIGHT */) {
32394
+ textLeft = left + legendWidth - size * 1.5 - textWidth;
32395
+ } else {
32396
+ const remaining = legendWidth - (bulletMargin || size);
32397
+ textLeft += (remaining - textWidth) / 2;
32398
+ }
31810
32399
  } else {
31811
- const remaining = legendWidth - (bulletMargin || size);
31812
- textLeft += (remaining - textWidth) / 2;
32400
+ textLeft += bulletMargin;
31813
32401
  }
31814
- } else {
31815
- textLeft += bulletMargin;
31816
32402
  }
31817
32403
  this.drawText([textLeft, top], text, label.style.textScale, label.style.textColor);
31818
32404
  }
31819
32405
  // not included in public docs
31820
32406
  draw3DLabels(mvpMatrix, leftTopWidthHeight, secondPass = false) {
31821
- const labels = this.getAllLabels();
32407
+ const labels = this.getConnectomeLabels();
31822
32408
  if (!this.opts.showLegend || labels.length === 0) {
31823
32409
  return;
31824
32410
  }
@@ -31856,6 +32442,51 @@ var Niivue = class _Niivue {
31856
32442
  }
31857
32443
  }
31858
32444
  }
32445
+ drawAnchoredLabels() {
32446
+ const size = this.opts.textHeight * Math.min(this.gl.canvas.height, this.gl.canvas.width) * 1;
32447
+ const anchoredLabels = this.document.labels.filter((l) => l.anchor != null && l.anchor !== 0 /* NONE */);
32448
+ for (const label of anchoredLabels) {
32449
+ const text = label.text;
32450
+ const textHeight = this.textHeight(label.style.textScale, text) * size;
32451
+ const textWidth = this.textWidth(label.style.textScale, text) * size;
32452
+ let left;
32453
+ let top;
32454
+ const scale = 1;
32455
+ const verticalMargin = this.opts.textHeight * this.gl.canvas.height * scale;
32456
+ const rectHeightDiff = verticalMargin;
32457
+ let rectWidthDiff = verticalMargin / 4;
32458
+ let rectHorizontalOffset = 0;
32459
+ let rectVerticalOffset = 0;
32460
+ if (label.anchor & 1 /* LEFT */) {
32461
+ left = 0;
32462
+ }
32463
+ if (label.anchor & 4 /* RIGHT */) {
32464
+ left = this.canvas.width - textWidth;
32465
+ rectHorizontalOffset -= verticalMargin / 4;
32466
+ }
32467
+ if (label.anchor & 2 /* CENTER */) {
32468
+ left = (this.canvas.width - textWidth) / 2;
32469
+ rectHorizontalOffset -= verticalMargin / 4;
32470
+ rectWidthDiff += verticalMargin / 4;
32471
+ }
32472
+ if (label.anchor & 8 /* TOP */) {
32473
+ top = 0;
32474
+ }
32475
+ if (label.anchor & 16 /* MIDDLE */) {
32476
+ top = (this.canvas.height - textHeight - verticalMargin) / 2;
32477
+ rectVerticalOffset -= verticalMargin / 4;
32478
+ }
32479
+ if (label.anchor & 32 /* BOTTOM */) {
32480
+ top = this.canvas.height - textHeight - verticalMargin;
32481
+ rectVerticalOffset -= verticalMargin / 4;
32482
+ }
32483
+ this.drawRect(
32484
+ [left + rectHorizontalOffset, top + rectVerticalOffset, textWidth + rectWidthDiff, textHeight + rectHeightDiff],
32485
+ label.style.backgroundColor
32486
+ );
32487
+ this.draw3DLabel(label, [left, top]);
32488
+ }
32489
+ }
31859
32490
  // not included in public docs
31860
32491
  draw3D(leftTopWidthHeight = [0, 0, 0, 0], mvpMatrix = null, modelMatrix = null, normalMatrix = null, azimuth = null, elevation = 0) {
31861
32492
  const isMosaic = azimuth !== null;
@@ -32731,6 +33362,12 @@ var Niivue = class _Niivue {
32731
33362
  }
32732
33363
  const isDrawPenDown = isFinite(this.drawPenLocation[0]) && this.opts.drawingEnabled;
32733
33364
  const { volScale } = this.sliceScale();
33365
+ const actualScale = volScale.slice();
33366
+ if (this.opts.multiplanarEqualSize) {
33367
+ volScale[0] = 1;
33368
+ volScale[1] = 1;
33369
+ volScale[2] = 1;
33370
+ }
32734
33371
  if (typeof this.opts.multiplanarPadPixels !== "number") {
32735
33372
  log.debug("multiplanarPadPixels must be numeric");
32736
33373
  }
@@ -32768,38 +33405,43 @@ var Niivue = class _Niivue {
32768
33405
  isDrawGrid = true;
32769
33406
  }
32770
33407
  }
33408
+ let ltwh = ltwh2x2;
32771
33409
  if (isDrawColumn) {
32772
- let ltwh = ltwh1x3;
33410
+ ltwh = ltwh1x3;
32773
33411
  if (isShowRender || this.opts.multiplanarShowRender === 2 /* AUTO */ && ltwh1x4[4] >= ltwh1x3[4]) {
32774
33412
  ltwh = ltwh1x4;
32775
33413
  } else {
32776
33414
  isDraw3D = false;
32777
33415
  }
32778
- const sX = volScale[0] * ltwh[4];
32779
- const sY = volScale[1] * ltwh[4];
32780
- const sZ = volScale[2] * ltwh[4];
32781
- const sMx = mx * ltwh[4];
32782
- this.draw2D([ltwh[0], ltwh[1], sX, sY], 0);
32783
- this.draw2D([ltwh[0], ltwh[1] + sY + pad, sX, sZ], 1);
32784
- this.draw2D([ltwh[0], ltwh[1] + sY + pad + sZ + pad, sY, sZ], 2);
32785
- if (isDraw3D) {
32786
- this.draw3D([ltwh[0], ltwh[1] + sY + sZ + sZ + pad * 3, sMx, sMx]);
32787
- }
32788
33416
  } else if (isDrawRow) {
32789
- let ltwh = ltwh3x1;
33417
+ ltwh = ltwh3x1;
32790
33418
  if (isShowRender || this.opts.multiplanarShowRender === 2 /* AUTO */ && ltwh4x1[4] >= ltwh3x1[4]) {
32791
33419
  ltwh = ltwh4x1;
32792
33420
  } else {
32793
33421
  isDraw3D = false;
32794
33422
  }
32795
- const sX = volScale[0] * ltwh[4];
32796
- const sY = volScale[1] * ltwh[4];
32797
- const sZ = volScale[2] * ltwh[4];
32798
- this.draw2D([ltwh[0], ltwh[1], sX, sY], 0);
32799
- this.draw2D([ltwh[0] + sX + pad, ltwh[1], sX, sZ], 1);
32800
- this.draw2D([ltwh[0] + sX + sX + pad * 2, ltwh[1], sY, sZ], 2);
33423
+ }
33424
+ const sX = volScale[0] * ltwh[4];
33425
+ const sY = volScale[1] * ltwh[4];
33426
+ const sZ = volScale[2] * ltwh[4];
33427
+ const actualX = actualScale[0] * ltwh[4];
33428
+ const actualY = actualScale[1] * ltwh[4];
33429
+ const actualZ = actualScale[2] * ltwh[4];
33430
+ if (isDrawColumn) {
33431
+ this.draw2D([ltwh[0], ltwh[1], sX, sY], 0, NaN, [actualX, actualY]);
33432
+ this.draw2D([ltwh[0], ltwh[1] + sY + pad, sX, sZ], 1, NaN, [actualX, actualZ]);
33433
+ this.draw2D([ltwh[0], ltwh[1] + sY + pad + sZ + pad, sY, sZ], 2, NaN, [actualY, actualZ]);
32801
33434
  if (isDraw3D) {
32802
- this.draw3D([ltwh[0] + sX + sX + sY + pad * 3, ltwh[1], ltwh[3], ltwh[3]]);
33435
+ const sMx = mx * ltwh[4];
33436
+ this.draw3D([ltwh[0], ltwh[1] + sY + sZ + sZ + pad * 3, sMx, sMx]);
33437
+ }
33438
+ } else if (isDrawRow) {
33439
+ this.draw2D([ltwh[0], ltwh[1], sX, sY], 0, NaN, [actualX, actualY]);
33440
+ this.draw2D([ltwh[0] + sX + pad, ltwh[1], sX, sZ], 1, NaN, [actualX, actualZ]);
33441
+ this.draw2D([ltwh[0] + sX + sX + pad * 2, ltwh[1], sY, sZ], 2, NaN, [actualY, actualZ]);
33442
+ if (isDraw3D) {
33443
+ const sMx = mx * ltwh[4];
33444
+ this.draw3D([ltwh[0] + sX + sX + sY + pad * 3, ltwh[1], sMx, sMx]);
32803
33445
  }
32804
33446
  } else if (isDrawGrid) {
32805
33447
  if (!isShowRender) {
@@ -32808,13 +33450,9 @@ var Niivue = class _Niivue {
32808
33450
  if (this.opts.multiplanarShowRender === 2 /* AUTO */) {
32809
33451
  isDraw3D = true;
32810
33452
  }
32811
- const ltwh = ltwh2x2;
32812
- const sX = volScale[0] * ltwh[4];
32813
- const sY = volScale[1] * ltwh[4];
32814
- const sZ = volScale[2] * ltwh[4];
32815
- this.draw2D([ltwh[0], ltwh[1] + sZ + pad, sX, sY], 0);
32816
- this.draw2D([ltwh[0], ltwh[1], sX, sZ], 1);
32817
- this.draw2D([ltwh[0] + sX + pad, ltwh[1], sY, sZ], 2);
33453
+ this.draw2D([ltwh[0], ltwh[1] + sZ + pad, sX, sY], 0, NaN, [actualX, actualY]);
33454
+ this.draw2D([ltwh[0], ltwh[1], sX, sZ], 1, NaN, [actualX, actualZ]);
33455
+ this.draw2D([ltwh[0] + sX + pad, ltwh[1], sY, sZ], 2, NaN, [actualY, actualZ]);
32818
33456
  if (isDraw3D) {
32819
33457
  this.draw3D([ltwh[0] + sX + pad, ltwh[1] + sZ + pad, sY, sY]);
32820
33458
  }
@@ -32878,11 +33516,30 @@ var Niivue = class _Niivue {
32878
33516
  width,
32879
33517
  height
32880
33518
  ]);
33519
+ return;
33520
+ }
33521
+ if (this.opts.clickToSegment) {
33522
+ const x = this.mousePos[0];
33523
+ const y = this.mousePos[1];
33524
+ if (this.inRenderTile(x, y) >= 0) {
33525
+ return;
33526
+ }
33527
+ const tileIdx = this.tileIndex(x, y);
33528
+ if (tileIdx > -1) {
33529
+ const fovMM = this.screenSlices[tileIdx].fovMM;
33530
+ const ltwh = this.screenSlices[tileIdx].leftTopWidthHeight;
33531
+ const pixPerMM = ltwh[2] / fovMM[0];
33532
+ const color = this.opts.crosshairColor;
33533
+ const segmentCursorColor = [color[0], color[1], color[2], 0.4];
33534
+ const radius = this.opts.clickToSegmentRadius * pixPerMM;
33535
+ this.drawCircle([x - radius, y - radius, radius * 2, radius * 2], segmentCursorColor, 1);
33536
+ }
32881
33537
  }
32882
33538
  const pos = this.frac2mm([this.scene.crosshairPos[0], this.scene.crosshairPos[1], this.scene.crosshairPos[2]]);
32883
33539
  posString = pos[0].toFixed(2) + "\xD7" + pos[1].toFixed(2) + "\xD7" + pos[2].toFixed(2);
32884
33540
  this.readyForSync = true;
32885
33541
  this.sync();
33542
+ this.drawAnchoredLabels();
32886
33543
  return posString;
32887
33544
  }
32888
33545
  // not included in public docs
@@ -32917,6 +33574,7 @@ export {
32917
33574
  DEFAULT_OPTIONS,
32918
33575
  DRAG_MODE,
32919
33576
  INITIAL_SCENE_DATA,
33577
+ LabelAnchorPoint,
32920
33578
  LabelLineTerminator,
32921
33579
  LabelTextAlignment,
32922
33580
  MULTIPLANAR_TYPE,