@niivue/niivue 0.44.1 → 0.44.2-dev.2

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.45.1";
17
14
 
18
15
  // src/logger.ts
19
16
  var _Log = class _Log {
@@ -1271,11 +1268,11 @@ uniform mat4 mvpMtx;
1271
1268
  uniform mat4 normMtx;
1272
1269
  out vec4 vClr;
1273
1270
  out vec3 vN;
1274
- out vec4 vP;
1271
+ out vec4 vPc;
1275
1272
  void main(void) {
1276
1273
  vec3 lightPosition = vec3(0.0, 0.0, -10.0);
1277
- vP = vec4(pos, 1.0);
1278
- gl_Position = mvpMtx * vP;
1274
+ vPc = mvpMtx * vec4(pos, 1.0);
1275
+ gl_Position = vPc;
1279
1276
  vN = normalize((normMtx * vec4(norm.xyz,1.0)).xyz);
1280
1277
  //vV = -vec3(modelMtx*vec4(pos,1.0));
1281
1278
  vClr = clr;
@@ -1420,6 +1417,45 @@ void main() {
1420
1417
  s *= specularWhite;
1421
1418
  color = vec4(d3 + s3 + s, opacity);
1422
1419
  }`;
1420
+ var fragMeshShaderCrevice = `#version 300 es
1421
+ precision highp int;
1422
+ precision highp float;
1423
+ uniform float opacity;
1424
+ in vec4 vClr;
1425
+ in vec3 vN;
1426
+ in vec4 vPc;
1427
+ out vec4 color;
1428
+ void main() {
1429
+ vec3 n = normalize(vN);
1430
+ // Compute curvature
1431
+ vec3 dx = dFdx(n);
1432
+ vec3 dy = dFdy(n);
1433
+ vec3 xneg = n - dx;
1434
+ vec3 xpos = n + dx;
1435
+ vec3 yneg = n - dy;
1436
+ vec3 ypos = n + dy;
1437
+ float depth = length(vPc.xyz);
1438
+ float curv = (cross(xneg, xpos).y - cross(yneg, ypos).x) / depth;
1439
+ //at this stage 0.5 for flat, with valleys dark and ridges bright
1440
+ curv = 1.0 - (curv + 0.5);
1441
+ //clamp
1442
+ curv = min(max(curv, 0.0), 1.0);
1443
+ // easing function
1444
+ curv = pow(curv, 0.5);
1445
+ //modulate ambient and diffuse with curvature
1446
+ vec3 r = vec3(0.0, 0.0, 1.0); //rayDir: for orthographic projections moving in Z direction (no need for normal matrix)
1447
+ float ambient = 0.6;
1448
+ float diffuse = 0.6;
1449
+ float specular = 0.2;
1450
+ float shininess = 10.0;
1451
+ vec3 lightPosition = vec3(0.0, 10.0, -2.0);
1452
+ vec3 l = normalize(lightPosition);
1453
+ float lightNormDot = dot(n, l);
1454
+ vec3 a = vClr.rgb * ambient * curv;
1455
+ vec3 d = max(lightNormDot, 0.0) * vClr.rgb * diffuse;
1456
+ float s = specular * pow(max(dot(reflect(l, n), r), 0.0), shininess);
1457
+ color = vec4(a + d + s, opacity);
1458
+ }`;
1423
1459
  var fragMeshShader = `#version 300 es
1424
1460
  precision highp int;
1425
1461
  precision highp float;
@@ -12357,6 +12393,12 @@ var NVMeshLoaders = class _NVMeshLoaders {
12357
12393
  while (lines[pos].length < 1) {
12358
12394
  pos++;
12359
12395
  }
12396
+ if (lines[pos].startsWith("METADATA")) {
12397
+ while (lines[pos].length > 1) {
12398
+ pos++;
12399
+ }
12400
+ pos++;
12401
+ }
12360
12402
  items = lines[pos].trim().split(/\s+/);
12361
12403
  pos++;
12362
12404
  if (items[0].includes("LINES")) {
@@ -12535,7 +12577,7 @@ var NVMeshLoaders = class _NVMeshLoaders {
12535
12577
  layer.values = obj;
12536
12578
  }
12537
12579
  }
12538
- } else if (ext === "CRV" || ext === "CURV") {
12580
+ } else if (ext === "CRV" || ext === "CURV" || ext === "THICKNESS" || ext === "AREA") {
12539
12581
  layer.values = _NVMeshLoaders.readCURV(buffer, n_vert);
12540
12582
  layer.isTransparentBelowCalMin = false;
12541
12583
  } else if (ext === "GII") {
@@ -12560,6 +12602,9 @@ var NVMeshLoaders = class _NVMeshLoaders {
12560
12602
  layer.values = _NVMeshLoaders.readSMP(buffer, n_vert);
12561
12603
  } else if (ext === "STC") {
12562
12604
  layer.values = _NVMeshLoaders.readSTC(buffer, n_vert);
12605
+ } else if (_NVMeshLoaders.isCurv(buffer)) {
12606
+ layer.values = _NVMeshLoaders.readCURV(buffer, n_vert);
12607
+ layer.isTransparentBelowCalMin = false;
12563
12608
  } else {
12564
12609
  log.warn("Unknown layer overlay format " + name);
12565
12610
  return layer;
@@ -12733,6 +12778,17 @@ var NVMeshLoaders = class _NVMeshLoaders {
12733
12778
  return f32;
12734
12779
  }
12735
12780
  // readSTC()
12781
+ static isCurv(buffer) {
12782
+ const view = new DataView(buffer);
12783
+ const sig0 = view.getUint8(0);
12784
+ const sig1 = view.getUint8(1);
12785
+ const sig2 = view.getUint8(2);
12786
+ if (sig0 !== 255 || sig1 !== 255 || sig2 !== 255) {
12787
+ utiltiesLogger.debug("Unable to recognize file type: does not appear to be FreeSurfer format.");
12788
+ return false;
12789
+ }
12790
+ return true;
12791
+ }
12736
12792
  // read freesurfer curv big-endian format
12737
12793
  // https://github.com/bonilhamusclab/MRIcroS/blob/master/%2BfileUtils/%2Bpial/readPial.m
12738
12794
  // http://www.grahamwideman.com/gw/brain/fs/surfacefileformats.htm
@@ -18369,76 +18425,90 @@ function getBestTransform(imageDirections, voxelDimensions, imagePosition) {
18369
18425
  }
18370
18426
  return m;
18371
18427
  }
18372
- function str2Buffer(str) {
18428
+ function str2Buffer(str, maxLen = 80) {
18429
+ str = str.replace(/[`$]/g, "");
18373
18430
  const bytes = [];
18374
- for (let i = 0; i < str.length; i++) {
18431
+ const len = Math.min(maxLen, str.length);
18432
+ for (let i = 0; i < len; i++) {
18375
18433
  const char = str.charCodeAt(i);
18376
18434
  bytes.push(char & 255);
18377
18435
  }
18378
18436
  return bytes;
18379
18437
  }
18380
- function hdrToArrayBuffer(hdr, isDrawing8 = false) {
18438
+ function hdrToArrayBuffer(hdr, isDrawing8 = false, isInputEndian = false) {
18381
18439
  const SHORT_SIZE = 2;
18382
18440
  const FLOAT32_SIZE = 4;
18441
+ let isLittleEndian = true;
18442
+ if (isInputEndian) {
18443
+ isLittleEndian = hdr.littleEndian;
18444
+ }
18383
18445
  const byteArray = new Uint8Array(348);
18384
18446
  const view = new DataView(byteArray.buffer);
18385
- view.setInt32(0, 348, hdr.littleEndian);
18447
+ view.setInt32(0, 348, isLittleEndian);
18448
+ view.setUint8(38, 114);
18386
18449
  view.setUint8(39, hdr.dim_info);
18387
18450
  for (let i = 0; i < 8; i++) {
18388
- view.setUint16(40 + SHORT_SIZE * i, hdr.dims[i], hdr.littleEndian);
18451
+ view.setUint16(40 + SHORT_SIZE * i, hdr.dims[i], isLittleEndian);
18389
18452
  }
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);
18453
+ view.setFloat32(56, hdr.intent_p1, isLittleEndian);
18454
+ view.setFloat32(60, hdr.intent_p2, isLittleEndian);
18455
+ view.setFloat32(64, hdr.intent_p3, isLittleEndian);
18456
+ view.setInt16(68, hdr.intent_code, isLittleEndian);
18394
18457
  if (isDrawing8) {
18395
- view.setInt16(70, 2, hdr.littleEndian);
18396
- view.setInt16(72, 8, hdr.littleEndian);
18458
+ view.setInt16(70, 2, isLittleEndian);
18459
+ view.setInt16(72, 8, isLittleEndian);
18397
18460
  } else {
18398
- view.setInt16(70, hdr.datatypeCode, hdr.littleEndian);
18399
- view.setInt16(72, hdr.numBitsPerVoxel, hdr.littleEndian);
18461
+ view.setInt16(70, hdr.datatypeCode, isLittleEndian);
18462
+ view.setInt16(72, hdr.numBitsPerVoxel, isLittleEndian);
18400
18463
  }
18401
- view.setInt16(74, hdr.slice_start, hdr.littleEndian);
18464
+ view.setInt16(74, hdr.slice_start, isLittleEndian);
18402
18465
  for (let i = 0; i < 8; i++) {
18403
- view.setFloat32(76 + FLOAT32_SIZE * i, hdr.pixDims[i], hdr.littleEndian);
18466
+ view.setFloat32(76 + FLOAT32_SIZE * i, hdr.pixDims[i], isLittleEndian);
18404
18467
  }
18405
18468
  if (isDrawing8) {
18406
- view.setFloat32(108, 352, hdr.littleEndian);
18407
- view.setFloat32(112, 1, hdr.littleEndian);
18408
- view.setFloat32(116, 0, hdr.littleEndian);
18469
+ view.setFloat32(108, 352, isLittleEndian);
18470
+ view.setFloat32(112, 1, isLittleEndian);
18471
+ view.setFloat32(116, 0, isLittleEndian);
18409
18472
  } 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);
18473
+ view.setFloat32(108, 352, isLittleEndian);
18474
+ view.setFloat32(112, hdr.scl_slope, isLittleEndian);
18475
+ view.setFloat32(116, hdr.scl_inter, isLittleEndian);
18413
18476
  }
18414
- view.setInt16(120, hdr.slice_end, hdr.littleEndian);
18477
+ view.setInt16(120, hdr.slice_end, isLittleEndian);
18415
18478
  view.setUint8(122, hdr.slice_code);
18416
- view.setUint8(123, hdr.xyzt_units);
18479
+ if (hdr.xyzt_units === 0) {
18480
+ view.setUint8(123, 10);
18481
+ } else {
18482
+ view.setUint8(123, hdr.xyzt_units);
18483
+ }
18417
18484
  if (isDrawing8) {
18418
- view.setFloat32(124, 0, hdr.littleEndian);
18419
- view.setFloat32(128, 0, hdr.littleEndian);
18485
+ view.setFloat32(124, 0, isLittleEndian);
18486
+ view.setFloat32(128, 0, isLittleEndian);
18420
18487
  } else {
18421
- view.setFloat32(124, hdr.cal_max, hdr.littleEndian);
18422
- view.setFloat32(128, hdr.cal_min, hdr.littleEndian);
18488
+ view.setFloat32(124, hdr.cal_max, isLittleEndian);
18489
+ view.setFloat32(128, hdr.cal_min, isLittleEndian);
18423
18490
  }
18424
- view.setFloat32(132, hdr.slice_duration, hdr.littleEndian);
18425
- view.setFloat32(136, hdr.toffset, hdr.littleEndian);
18491
+ view.setFloat32(132, hdr.slice_duration, isLittleEndian);
18492
+ view.setFloat32(136, hdr.toffset, isLittleEndian);
18426
18493
  byteArray.set(str2Buffer(hdr.description), 148);
18427
18494
  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);
18495
+ view.setInt16(252, hdr.qform_code, isLittleEndian);
18496
+ if (hdr.sform_code < 1 || hdr.sform_code < 1) {
18497
+ view.setInt16(254, 1, isLittleEndian);
18498
+ } else {
18499
+ view.setInt16(254, hdr.sform_code, isLittleEndian);
18500
+ }
18501
+ view.setFloat32(256, hdr.quatern_b, isLittleEndian);
18502
+ view.setFloat32(260, hdr.quatern_c, isLittleEndian);
18503
+ view.setFloat32(264, hdr.quatern_d, isLittleEndian);
18504
+ view.setFloat32(268, hdr.qoffset_x, isLittleEndian);
18505
+ view.setFloat32(272, hdr.qoffset_y, isLittleEndian);
18506
+ view.setFloat32(276, hdr.qoffset_z, isLittleEndian);
18436
18507
  const flattened = hdr.affine.flat();
18437
18508
  for (let i = 0; i < 12; i++) {
18438
- view.setFloat32(280 + FLOAT32_SIZE * i, flattened[i], hdr.littleEndian);
18509
+ view.setFloat32(280 + FLOAT32_SIZE * i, flattened[i], isLittleEndian);
18439
18510
  }
18440
- byteArray.set(str2Buffer(hdr.intent_name), 328);
18441
- byteArray.set(str2Buffer(hdr.magic), 344);
18511
+ view.setInt32(344, 3222382, true);
18442
18512
  return byteArray;
18443
18513
  }
18444
18514
  function getExtents(positions, forceOriginInVolume = true) {
@@ -19982,7 +20052,7 @@ var NVImage = class _NVImage {
19982
20052
  hdr.datatypeCode = 64 /* DT_FLOAT64 */;
19983
20053
  break;
19984
20054
  default:
19985
- throw new Error("Unsupported NRRD data type: " + items[0]);
20055
+ throw new Error("Unsupported MHA data type: " + items[0]);
19986
20056
  }
19987
20057
  }
19988
20058
  if (line.startsWith("ObjectType") && !items[0].includes("Image")) {
@@ -20004,6 +20074,9 @@ var NVImage = class _NVImage {
20004
20074
  [mat33[2], mat33[5], mat33[8], offset[2]],
20005
20075
  [0, 0, 0, 1]
20006
20076
  ];
20077
+ while (bytes[pos] === 10) {
20078
+ pos++;
20079
+ }
20007
20080
  hdr.vox_offset = pos;
20008
20081
  if (isDetached && pairedImgData) {
20009
20082
  if (isGz) {
@@ -22211,6 +22284,7 @@ var DEFAULT_OPTIONS = {
22211
22284
  multiplanarPadPixels: 0,
22212
22285
  // @deprecated
22213
22286
  multiplanarForceRender: false,
22287
+ multiplanarEqualSize: false,
22214
22288
  multiplanarShowRender: 2 /* AUTO */,
22215
22289
  // auto is the same behaviour as multiplanarForceRender: false
22216
22290
  isRadiologicalConvention: false,
@@ -22251,6 +22325,7 @@ var DEFAULT_OPTIONS = {
22251
22325
  clickToSegmentBright: true
22252
22326
  };
22253
22327
  var INITIAL_SCENE_DATA = {
22328
+ gamma: 1,
22254
22329
  azimuth: 110,
22255
22330
  elevation: 10,
22256
22331
  crosshairPos: vec38.create(),
@@ -22489,6 +22564,9 @@ var NVDocument = class _NVDocument {
22489
22564
  data.opts.meshThicknessOn2D = "infinity";
22490
22565
  }
22491
22566
  data.labels = [...this.data.labels];
22567
+ for (const label of data.labels) {
22568
+ delete label.onClick;
22569
+ }
22492
22570
  data.customData = this.customData;
22493
22571
  if (this.volumes.length) {
22494
22572
  let imageOptions = this.imageOptionsArray[0];
@@ -22744,6 +22822,19 @@ var LabelLineTerminator = /* @__PURE__ */ ((LabelLineTerminator2) => {
22744
22822
  LabelLineTerminator2["RING"] = "ring";
22745
22823
  return LabelLineTerminator2;
22746
22824
  })(LabelLineTerminator || {});
22825
+ var LabelAnchorPoint = /* @__PURE__ */ ((LabelAnchorPoint2) => {
22826
+ LabelAnchorPoint2[LabelAnchorPoint2["NONE"] = 0 /* NONE */] = "NONE";
22827
+ LabelAnchorPoint2[LabelAnchorPoint2["TOPLEFT"] = 9] = "TOPLEFT";
22828
+ LabelAnchorPoint2[LabelAnchorPoint2["TOPCENTER"] = 10] = "TOPCENTER";
22829
+ LabelAnchorPoint2[LabelAnchorPoint2["TOPRIGHT"] = 12] = "TOPRIGHT";
22830
+ LabelAnchorPoint2[LabelAnchorPoint2["MIDDLELEFT"] = 17] = "MIDDLELEFT";
22831
+ LabelAnchorPoint2[LabelAnchorPoint2["MIDDLECENTER"] = 18] = "MIDDLECENTER";
22832
+ LabelAnchorPoint2[LabelAnchorPoint2["MIDDLERIGHT"] = 20] = "MIDDLERIGHT";
22833
+ LabelAnchorPoint2[LabelAnchorPoint2["BOTTOMLEFT"] = 33] = "BOTTOMLEFT";
22834
+ LabelAnchorPoint2[LabelAnchorPoint2["BOTTOMCENTER"] = 34] = "BOTTOMCENTER";
22835
+ LabelAnchorPoint2[LabelAnchorPoint2["BOTTOMRIGHT"] = 36] = "BOTTOMRIGHT";
22836
+ return LabelAnchorPoint2;
22837
+ })(LabelAnchorPoint || {});
22747
22838
  var NVLabel3DStyle = class {
22748
22839
  /**
22749
22840
  * @param textColor - Color of text
@@ -22781,13 +22872,17 @@ var NVLabel3D = class {
22781
22872
  * @param style - The style of the label
22782
22873
  * @param points - An array of points label for label lines
22783
22874
  */
22784
- constructor(text, style, points) {
22875
+ constructor(text, style, points, anchor, onClick) {
22785
22876
  __publicField(this, "text");
22786
22877
  __publicField(this, "style");
22787
22878
  __publicField(this, "points");
22879
+ __publicField(this, "anchor");
22880
+ __publicField(this, "onClick");
22788
22881
  this.text = text;
22789
22882
  this.style = style;
22790
22883
  this.points = points;
22884
+ this.anchor = anchor || 0 /* NONE */;
22885
+ this.onClick = onClick;
22791
22886
  }
22792
22887
  };
22793
22888
 
@@ -23929,6 +24024,7 @@ var Niivue = class _Niivue {
23929
24024
  * @param options - options object to set modifiable Niivue properties
23930
24025
  */
23931
24026
  constructor(options = DEFAULT_OPTIONS) {
24027
+ __publicField(this, "loaders", {});
23932
24028
  __publicField(this, "canvas", null);
23933
24029
  // the reference to the canvas element on the page
23934
24030
  __publicField(this, "_gl", null);
@@ -24101,6 +24197,10 @@ var Niivue = class _Niivue {
24101
24197
  Name: "Hemispheric",
24102
24198
  Frag: fragMeshHemiShader
24103
24199
  },
24200
+ {
24201
+ Name: "Crevice",
24202
+ Frag: fragMeshShaderCrevice
24203
+ },
24104
24204
  {
24105
24205
  Name: "Edge",
24106
24206
  Frag: fragMeshEdgeShader
@@ -24534,6 +24634,11 @@ var Niivue = class _Niivue {
24534
24634
  }
24535
24635
  const thisMM = this.frac2mm(this.scene.crosshairPos);
24536
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);
24641
+ }
24537
24642
  if (this.syncOpts["2d"]) {
24538
24643
  this.otherNV.scene.crosshairPos = this.otherNV.mm2frac(thisMM);
24539
24644
  this.otherNV.scene.pan2Dxyzmm = vec48.clone(this.scene.pan2Dxyzmm);
@@ -24550,6 +24655,11 @@ var Niivue = class _Niivue {
24550
24655
  if (this.otherNV[i] === this) {
24551
24656
  continue;
24552
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
+ }
24553
24663
  if (this.syncOpts["2d"]) {
24554
24664
  this.otherNV[i].scene.crosshairPos = this.otherNV[i].mm2frac(thisMM);
24555
24665
  this.otherNV[i].scene.pan2Dxyzmm = vec48.clone(this.scene.pan2Dxyzmm);
@@ -24665,6 +24775,10 @@ var Niivue = class _Niivue {
24665
24775
  const [x, y] = [pos.x * this.uiData.dpr, pos.y * this.uiData.dpr];
24666
24776
  const label = this.getLabelAtPoint([x, y]);
24667
24777
  if (label) {
24778
+ if (label.onClick) {
24779
+ label.onClick(label);
24780
+ return;
24781
+ }
24668
24782
  for (const mesh of this.meshes) {
24669
24783
  if (mesh.type !== "connectome" /* CONNECTOME */) {
24670
24784
  continue;
@@ -24979,6 +25093,15 @@ var Niivue = class _Niivue {
24979
25093
  this.drawScene();
24980
25094
  this.uiData.prevX = this.uiData.currX;
24981
25095
  this.uiData.prevY = this.uiData.currY;
25096
+ } else if (!this.uiData.mousedown && this.opts.clickToSegment) {
25097
+ const pos = this.getNoPaddingNoBorderCanvasRelativeMousePosition(e, this.gl.canvas);
25098
+ if (!pos) {
25099
+ return;
25100
+ }
25101
+ const x = pos.x * this.uiData.dpr;
25102
+ const y = pos.y * this.uiData.dpr;
25103
+ this.mousePos = [x, y];
25104
+ this.drawScene();
24982
25105
  }
24983
25106
  }
24984
25107
  // not included in public docs
@@ -25216,6 +25339,11 @@ var Niivue = class _Niivue {
25216
25339
  if (ext === "GZ") {
25217
25340
  ext = re.exec(fullname.slice(0, -3))[1];
25218
25341
  ext = ext.toUpperCase();
25342
+ } else if (ext === "CBOR") {
25343
+ const endExt = ext;
25344
+ ext = re.exec(fullname.slice(0, -5))[1];
25345
+ ext = ext.toUpperCase();
25346
+ ext = `${ext}.${endExt}`;
25219
25347
  }
25220
25348
  return upperCase ? ext : ext.toLowerCase();
25221
25349
  }
@@ -25236,6 +25364,28 @@ var Niivue = class _Niivue {
25236
25364
  }
25237
25365
  async addVolumesFromUrl(imageOptionsArray) {
25238
25366
  const promises = imageOptionsArray.map(async (imageItem) => {
25367
+ const ext = this.getFileExt(imageItem.name || imageItem.url);
25368
+ if (this.loaders[ext]) {
25369
+ let itemToLoad = imageItem.url;
25370
+ const toExt = this.loaders[ext].toExt;
25371
+ let name = imageItem.name || imageItem.url;
25372
+ name = name.split("/").pop();
25373
+ if (typeof imageItem.url === "string") {
25374
+ const url = imageItem.url;
25375
+ try {
25376
+ const response = await fetch(url);
25377
+ if (!response.ok) {
25378
+ throw new Error(`Failed to load file: ${response.statusText}`);
25379
+ }
25380
+ itemToLoad = await response.arrayBuffer();
25381
+ } catch (error) {
25382
+ throw new Error(`Failed to load url ${url}: ${error}`);
25383
+ }
25384
+ }
25385
+ const buffer = await this.loaders[ext].loader(itemToLoad);
25386
+ imageItem.url = buffer;
25387
+ imageItem.name = `${name}.${toExt}`;
25388
+ }
25239
25389
  const imageOptions = {
25240
25390
  url: imageItem.url,
25241
25391
  headers: imageItem.headers,
@@ -25375,6 +25525,40 @@ var Niivue = class _Niivue {
25375
25525
  this.addVolume(volume);
25376
25526
  });
25377
25527
  }
25528
+ /* useLoader
25529
+
25530
+ registers an external file loader for niivue to use when reading files.
25531
+
25532
+ the loader must return an array buffer of the file contents for niivue
25533
+ to parse and the extension of the file so niivue can infer the file type to load.
25534
+
25535
+ example:
25536
+
25537
+ const myCustomLoader = (File) => {
25538
+ // ... do parsing here ...
25539
+ return {
25540
+ arrayBuffer: ArrayBuffer,
25541
+ fileExt: 'iwi.cbor',
25542
+ positions: Float32Array | [], // allows for mesh loading to mz3
25543
+ indices: Uint32Array | [], // allows for mesh loading to mz3
25544
+ }
25545
+
25546
+ nv.useLoader({
25547
+ loader: myCustomLoader,
25548
+ fileExt: 'iwi.cbor',
25549
+ toExt: 'nii'
25550
+ })
25551
+
25552
+ */
25553
+ useLoader(loader, fileExt, toExt) {
25554
+ this.loaders = {
25555
+ ...this.loaders,
25556
+ [fileExt.toUpperCase()]: {
25557
+ loader,
25558
+ toExt
25559
+ }
25560
+ };
25561
+ }
25378
25562
  // not included in public docs
25379
25563
  dropListener(e) {
25380
25564
  e.stopPropagation();
@@ -25503,7 +25687,6 @@ var Niivue = class _Niivue {
25503
25687
  this.loadDrawing(volume);
25504
25688
  } else {
25505
25689
  this.addVolume(volume);
25506
- this.setDrawingEnabled(true);
25507
25690
  }
25508
25691
  }).catch((e2) => {
25509
25692
  throw e2;
@@ -27047,6 +27230,37 @@ var Niivue = class _Niivue {
27047
27230
  this.document.meshes = this.meshes;
27048
27231
  return this.document.download(fileName, compress2);
27049
27232
  }
27233
+ // generic loadImages that wraps loadVolumes and loadMeshes
27234
+ async loadImages(images) {
27235
+ const volumes = [];
27236
+ const meshes = [];
27237
+ for (const image of images) {
27238
+ if ("url" in image) {
27239
+ const ext = this.getFileExt(image.url);
27240
+ if (this.loaders[ext]) {
27241
+ const toExt = this.loaders[ext].toExt.toUpperCase();
27242
+ if (MESH_EXTENSIONS.includes(toExt)) {
27243
+ meshes.push(image);
27244
+ } else {
27245
+ volumes.push(image);
27246
+ }
27247
+ continue;
27248
+ }
27249
+ if (MESH_EXTENSIONS.includes(ext.toUpperCase())) {
27250
+ meshes.push(image);
27251
+ } else {
27252
+ volumes.push(image);
27253
+ }
27254
+ }
27255
+ }
27256
+ if (volumes.length > 0) {
27257
+ await this.loadVolumes(volumes);
27258
+ }
27259
+ if (meshes.length > 0) {
27260
+ await this.loadMeshes(meshes);
27261
+ }
27262
+ return this;
27263
+ }
27050
27264
  /**
27051
27265
  * load an array of volume objects
27052
27266
  * @param volumeList - the array of objects to load. each object must have a resolvable "url" property at a minimum
@@ -27066,27 +27280,7 @@ var Niivue = class _Niivue {
27066
27280
  this.volumes = [];
27067
27281
  this.gl.clearColor(0, 0, 0, 1);
27068
27282
  this.gl.clear(this.gl.COLOR_BUFFER_BIT);
27069
- if (volumeList.length > 1) {
27070
- await this.addVolumesFromUrl(volumeList);
27071
- return this;
27072
- }
27073
- const imageOptions = {
27074
- url: volumeList[0].url,
27075
- headers: volumeList[0].headers,
27076
- name: volumeList[0].name,
27077
- colormap: volumeList[0].colormap ? volumeList[0].colormap : volumeList[0].colorMap,
27078
- colormapNegative: volumeList[0].colormapNegative ? volumeList[0].colormapNegative : volumeList[0].colorMapNegative,
27079
- opacity: volumeList[0].opacity,
27080
- urlImgData: volumeList[0].urlImgData,
27081
- cal_min: volumeList[0].cal_min,
27082
- cal_max: volumeList[0].cal_max,
27083
- trustCalMinMax: this.opts.trustCalMinMax,
27084
- isManifest: volumeList[0].isManifest,
27085
- frame4D: volumeList[0].frame4D,
27086
- limitFrames4D: volumeList[0].limitFrames4D || this.opts.limitFrames4D,
27087
- colorbarVisible: volumeList[0].colorbarVisible
27088
- };
27089
- await this.addVolumeFromUrl(imageOptions);
27283
+ await this.addVolumesFromUrl(volumeList);
27090
27284
  return this;
27091
27285
  }
27092
27286
  /**
@@ -27116,7 +27310,29 @@ var Niivue = class _Niivue {
27116
27310
  */
27117
27311
  async addMeshesFromUrl(meshOptions) {
27118
27312
  const promises = meshOptions.map(async (meshItem) => {
27119
- const ext = this.getFileExt(meshItem.url);
27313
+ const ext = this.getFileExt(meshItem.name || meshItem.url);
27314
+ if (this.loaders[ext]) {
27315
+ let itemToLoad = meshItem.url;
27316
+ const toExt = this.loaders[ext].toExt;
27317
+ let name = meshItem.name || meshItem.url;
27318
+ name = name.split("/").pop();
27319
+ if (typeof meshItem.url === "string") {
27320
+ const url = meshItem.url;
27321
+ try {
27322
+ const response = await fetch(url);
27323
+ if (!response.ok) {
27324
+ throw new Error(`Failed to load file: ${response.statusText}`);
27325
+ }
27326
+ itemToLoad = await response.arrayBuffer();
27327
+ } catch (error) {
27328
+ throw new Error(`Failed to load url ${url}: ${error}`);
27329
+ }
27330
+ }
27331
+ const { positions, indices } = await this.loaders[ext].loader(itemToLoad);
27332
+ meshItem.name = `${name}.${toExt}`;
27333
+ const mz3 = NVMeshUtilities.createMZ3(positions, indices, false);
27334
+ meshItem.buffer = mz3;
27335
+ }
27120
27336
  if (ext === "JCON" || ext === "JSON") {
27121
27337
  const response = await fetch(meshItem.url, {});
27122
27338
  const json = await response.json();
@@ -27157,13 +27373,7 @@ var Niivue = class _Niivue {
27157
27373
  this.meshes = [];
27158
27374
  this.gl.clearColor(0, 0, 0, 1);
27159
27375
  this.gl.clear(this.gl.COLOR_BUFFER_BIT);
27160
- if (meshList.length > 1) {
27161
- await this.addMeshesFromUrl(meshList);
27162
- this.updateGLVolume();
27163
- this.drawScene();
27164
- return this;
27165
- }
27166
- await this.addMeshFromUrl(meshList[0]);
27376
+ await this.addMeshesFromUrl(meshList);
27167
27377
  this.updateGLVolume();
27168
27378
  this.drawScene();
27169
27379
  return this;
@@ -29728,6 +29938,7 @@ var Niivue = class _Niivue {
29728
29938
  * @see {@link https://niivue.github.io/niivue/features/colormaps.html | live demo usage}
29729
29939
  */
29730
29940
  setGamma(gamma = 1) {
29941
+ this.scene.gamma = gamma;
29731
29942
  cmapper.gamma = gamma;
29732
29943
  this.updateGLVolume();
29733
29944
  }
@@ -30091,6 +30302,7 @@ var Niivue = class _Niivue {
30091
30302
  }
30092
30303
  if (this.opts.drawingEnabled) {
30093
30304
  const pt = this.frac2vox(this.scene.crosshairPos);
30305
+ const ptMM = this.frac2mm(this.scene.crosshairPos);
30094
30306
  if (this.opts.clickToSegment) {
30095
30307
  const radius = this.opts.clickToSegmentRadius;
30096
30308
  const steps = this.opts.clickToSegmentSteps;
@@ -30099,11 +30311,12 @@ var Niivue = class _Niivue {
30099
30311
  this.drawPenAxCorSag = axCorSag;
30100
30312
  for (let i2 = 1; i2 <= steps; i2++) {
30101
30313
  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);
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];
30107
30320
  this.drawPt(xVox, yVox, zVox, this.opts.penValue);
30108
30321
  this.drawPenFillPts.push([xVox, yVox, pt[2]]);
30109
30322
  if (i2 === steps) {
@@ -30387,9 +30600,21 @@ var Niivue = class _Niivue {
30387
30600
  const labels = [...this.document.labels, ...definedMeshLabels];
30388
30601
  return labels;
30389
30602
  }
30603
+ getConnectomeLabels() {
30604
+ const connectomes = this.meshes.filter((m) => m.type === "connectome" /* CONNECTOME */);
30605
+ const meshNodes = connectomes.flatMap((m) => m.nodes);
30606
+ const meshLabels = meshNodes.map((n) => n.label);
30607
+ const definedMeshLabels = meshLabels.filter((l) => l !== void 0);
30608
+ const nonAnchoredLabels = this.document.labels.filter((l) => l.anchor == null || l.anchor === 0 /* NONE */);
30609
+ const nonAnchoredLabelSet = new Set(definedMeshLabels);
30610
+ for (const label of nonAnchoredLabels) {
30611
+ nonAnchoredLabelSet.add(label);
30612
+ }
30613
+ return Array.from(nonAnchoredLabelSet);
30614
+ }
30390
30615
  getBulletMarginWidth() {
30391
30616
  let bulletMargin = 0;
30392
- const labels = this.getAllLabels();
30617
+ const labels = this.getConnectomeLabels();
30393
30618
  if (labels.length === 0) {
30394
30619
  return 0;
30395
30620
  }
@@ -30406,7 +30631,7 @@ var Niivue = class _Niivue {
30406
30631
  return bulletMargin;
30407
30632
  }
30408
30633
  getLegendPanelWidth() {
30409
- const labels = this.getAllLabels();
30634
+ const labels = this.getConnectomeLabels();
30410
30635
  if (!this.opts.showLegend || labels.length === 0) {
30411
30636
  return 0;
30412
30637
  }
@@ -30432,7 +30657,7 @@ var Niivue = class _Niivue {
30432
30657
  return width;
30433
30658
  }
30434
30659
  getLegendPanelHeight() {
30435
- const labels = this.getAllLabels();
30660
+ const labels = this.getConnectomeLabels();
30436
30661
  let height = 0;
30437
30662
  const scale = 1;
30438
30663
  const verticalMargin = this.opts.textHeight * this.gl.canvas.height * scale;
@@ -30874,7 +31099,7 @@ var Niivue = class _Niivue {
30874
31099
  }
30875
31100
  // not included in public docs
30876
31101
  // show text labels for L/R, A/P, I/S dimensions
30877
- drawSliceOrientationText(leftTopWidthHeight, axCorSag) {
31102
+ drawSliceOrientationText(leftTopWidthHeight, axCorSag, padLeftTop = [NaN, NaN]) {
30878
31103
  this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
30879
31104
  let topText = "S";
30880
31105
  if (axCorSag === 0 /* AXIAL */) {
@@ -30888,8 +31113,32 @@ var Niivue = class _Niivue {
30888
31113
  this.drawTextRightBelow([leftTopWidthHeight[0], leftTopWidthHeight[1]], leftText + topText);
30889
31114
  return;
30890
31115
  }
30891
- this.drawTextBelow([leftTopWidthHeight[0] + leftTopWidthHeight[2] * 0.5, leftTopWidthHeight[1]], topText);
30892
- this.drawTextRight([leftTopWidthHeight[0], leftTopWidthHeight[1] + leftTopWidthHeight[3] * 0.5], leftText);
31116
+ let drawBelow = true;
31117
+ let drawRight = true;
31118
+ if (!isNaN(padLeftTop[0])) {
31119
+ const ht = this.opts.textHeight * this.gl.canvas.height + 2;
31120
+ if (padLeftTop[1] > ht) {
31121
+ this.drawTextBelow(
31122
+ [leftTopWidthHeight[0] + leftTopWidthHeight[2] * 0.5, leftTopWidthHeight[1] + padLeftTop[1] - ht],
31123
+ topText
31124
+ );
31125
+ drawBelow = false;
31126
+ }
31127
+ const wid = this.textWidth(ht, leftText) + 2;
31128
+ if (padLeftTop[0] > wid) {
31129
+ this.drawTextRight(
31130
+ [leftTopWidthHeight[0] + padLeftTop[0] - wid, leftTopWidthHeight[1] + leftTopWidthHeight[3] * 0.5],
31131
+ leftText
31132
+ );
31133
+ drawRight = false;
31134
+ }
31135
+ }
31136
+ if (drawBelow) {
31137
+ this.drawTextBelow([leftTopWidthHeight[0] + leftTopWidthHeight[2] * 0.5, leftTopWidthHeight[1]], topText);
31138
+ }
31139
+ if (drawRight) {
31140
+ this.drawTextRight([leftTopWidthHeight[0], leftTopWidthHeight[1] + leftTopWidthHeight[3] * 0.5], leftText);
31141
+ }
30893
31142
  }
30894
31143
  // not included in public docs
30895
31144
  xyMM2xyzMM(axCorSag, sliceFrac) {
@@ -30927,7 +31176,7 @@ var Niivue = class _Niivue {
30927
31176
  }
30928
31177
  // not included in public docs
30929
31178
  // draw 2D tile
30930
- draw2D(leftTopWidthHeight, axCorSag, customMM = NaN) {
31179
+ draw2DMain(leftTopWidthHeight, axCorSag, customMM = NaN) {
30931
31180
  let frac2mmTexture = this.volumes[0].frac2mm.slice();
30932
31181
  let screen = this.screenFieldOfViewExtendedMM(axCorSag);
30933
31182
  let mesh2ortho = mat46.create();
@@ -30956,7 +31205,9 @@ var Niivue = class _Niivue {
30956
31205
  elevation = isRadiolgical ? -90 : 90;
30957
31206
  }
30958
31207
  const gl = this.gl;
31208
+ let isStretchToScreen = false;
30959
31209
  if (leftTopWidthHeight[2] === 0 || leftTopWidthHeight[3] === 0) {
31210
+ isStretchToScreen = true;
30960
31211
  const pixPerMMw = gl.canvas.width / screen.fovMM[0];
30961
31212
  const pixPerMMh = gl.canvas.height / screen.fovMM[1];
30962
31213
  const pixPerMMmin = Math.min(pixPerMMw, pixPerMMh);
@@ -31103,9 +31354,32 @@ var Niivue = class _Niivue {
31103
31354
  if (isNaN(customMM)) {
31104
31355
  this.drawCrosshairs3D(false, 0.15, obj.modelViewProjectionMatrix, true, this.opts.isSliceMM);
31105
31356
  }
31106
- this.drawSliceOrientationText(leftTopWidthHeight, axCorSag);
31357
+ if (isStretchToScreen) {
31358
+ this.drawSliceOrientationText(leftTopWidthHeight, axCorSag);
31359
+ }
31107
31360
  this.readyForSync = true;
31108
31361
  }
31362
+ draw2D(leftTopWidthHeight, axCorSag, customMM = NaN, imageWidthHeight = [NaN, NaN]) {
31363
+ const padLeftTop = [NaN, NaN];
31364
+ if (isNaN(imageWidthHeight[0])) {
31365
+ this.draw2DMain(leftTopWidthHeight, axCorSag, customMM);
31366
+ } else {
31367
+ const ltwh = leftTopWidthHeight.slice();
31368
+ padLeftTop[0] = Math.floor(0.5 * (ltwh[2] - imageWidthHeight[0]));
31369
+ padLeftTop[1] = Math.floor(0.5 * (ltwh[3] - imageWidthHeight[1]));
31370
+ ltwh[0] += padLeftTop[0];
31371
+ ltwh[1] += padLeftTop[1];
31372
+ ltwh[2] = imageWidthHeight[0];
31373
+ ltwh[3] = imageWidthHeight[1];
31374
+ this.draw2DMain(ltwh, axCorSag, customMM);
31375
+ }
31376
+ if (customMM === Infinity || customMM === -Infinity || axCorSag === 4 /* RENDER */) {
31377
+ return;
31378
+ }
31379
+ if (leftTopWidthHeight[2] !== 0 && leftTopWidthHeight[3] !== 0) {
31380
+ this.drawSliceOrientationText(leftTopWidthHeight, axCorSag, padLeftTop);
31381
+ }
31382
+ }
31109
31383
  // not included in public docs
31110
31384
  // determine 3D model view projection matrix
31111
31385
  calculateMvpMatrix(_unused, leftTopWidthHeight = [0, 0, 0, 0], azimuth, elevation) {
@@ -31716,7 +31990,7 @@ var Niivue = class _Niivue {
31716
31990
  * @param style - label style
31717
31991
  * @param point - 3D point on the model
31718
31992
  */
31719
- addLabel(text, style, points) {
31993
+ addLabel(text, style, points, anchor, onClick) {
31720
31994
  const defaultStyle = {
31721
31995
  textColor: this.opts.legendTextColor,
31722
31996
  textScale: 1,
@@ -31728,7 +32002,7 @@ var Niivue = class _Niivue {
31728
32002
  bulletColor: this.opts.legendTextColor
31729
32003
  };
31730
32004
  const labelStyle = style ? { ...defaultStyle, ...style } : { ...defaultStyle };
31731
- const label = new NVLabel3D(text, labelStyle, points);
32005
+ const label = new NVLabel3D(text, { ...labelStyle }, points, anchor, onClick);
31732
32006
  this.document.labels.push(label);
31733
32007
  return label;
31734
32008
  }
@@ -31746,6 +32020,60 @@ var Niivue = class _Niivue {
31746
32020
  return screenPoint;
31747
32021
  }
31748
32022
  getLabelAtPoint(screenPoint) {
32023
+ const scale = 1;
32024
+ const size = this.opts.textHeight * Math.min(this.gl.canvas.height, this.gl.canvas.width) * scale;
32025
+ const verticalMargin = this.opts.textHeight * this.gl.canvas.height * scale;
32026
+ for (const label of this.document.labels) {
32027
+ if (label.anchor == null || label.anchor === 0 /* NONE */) {
32028
+ continue;
32029
+ }
32030
+ const labelSize = this.opts.textHeight * this.gl.canvas.height * label.style.textScale;
32031
+ const textHeight = this.textHeight(labelSize, label.text);
32032
+ const textWidth = this.textWidth(labelSize, label.text);
32033
+ if (label.anchor & 1 /* LEFT */) {
32034
+ if (screenPoint[0] > textWidth) {
32035
+ continue;
32036
+ }
32037
+ }
32038
+ if (label.anchor & 2 /* CENTER */) {
32039
+ if (screenPoint[0] < (this.gl.canvas.width - textWidth) / 2) {
32040
+ continue;
32041
+ }
32042
+ if (screenPoint[0] > (this.gl.canvas.width + textWidth) / 2) {
32043
+ continue;
32044
+ }
32045
+ }
32046
+ if (label.anchor & 4 /* RIGHT */) {
32047
+ if (screenPoint[0] < this.gl.canvas.width - textWidth) {
32048
+ continue;
32049
+ }
32050
+ }
32051
+ if (label.anchor & 8 /* TOP */) {
32052
+ if (screenPoint[1] < verticalMargin / 2) {
32053
+ continue;
32054
+ }
32055
+ if (screenPoint[1] > textHeight + verticalMargin / 2) {
32056
+ continue;
32057
+ }
32058
+ }
32059
+ if (label.anchor & 16 /* MIDDLE */) {
32060
+ if (screenPoint[1] < (this.gl.canvas.height - textHeight - verticalMargin) / 2) {
32061
+ continue;
32062
+ }
32063
+ if (screenPoint[1] > (this.gl.canvas.height + textHeight - verticalMargin / 2) / 2) {
32064
+ continue;
32065
+ }
32066
+ }
32067
+ if (label.anchor & 32 /* BOTTOM */) {
32068
+ if (screenPoint[1] < this.gl.canvas.height - textHeight - verticalMargin) {
32069
+ continue;
32070
+ }
32071
+ if (screenPoint[1] > this.gl.canvas.height - verticalMargin / 2) {
32072
+ continue;
32073
+ }
32074
+ }
32075
+ return label;
32076
+ }
31749
32077
  log.debug("screenPoint", screenPoint);
31750
32078
  const panelHeight = this.getLegendPanelHeight();
31751
32079
  const panelWidth = this.getLegendPanelWidth();
@@ -31755,9 +32083,7 @@ var Niivue = class _Niivue {
31755
32083
  if (screenPoint[0] < left || screenPoint[1] < top || screenPoint[0] > left + panelWidth || screenPoint[1] > top + panelHeight) {
31756
32084
  return null;
31757
32085
  }
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();
32086
+ const labels = this.getConnectomeLabels();
31761
32087
  for (const label of labels) {
31762
32088
  const labelSize = this.opts.textHeight * this.gl.canvas.height * label.style.textScale;
31763
32089
  const textHeight = this.textHeight(labelSize, label.text);
@@ -31786,7 +32112,7 @@ var Niivue = class _Niivue {
31786
32112
  }
31787
32113
  }
31788
32114
  // not included in public docs
31789
- draw3DLabel(label, pos, mvpMatrix, leftTopWidthHeight, bulletMargin = 0, legendWidth, secondPass = false) {
32115
+ draw3DLabel(label, pos, mvpMatrix, leftTopWidthHeight, bulletMargin, legendWidth, secondPass) {
31790
32116
  const text = label.text;
31791
32117
  const left = pos[0];
31792
32118
  const top = pos[1];
@@ -31803,22 +32129,24 @@ var Niivue = class _Niivue {
31803
32129
  this.drawCircle([rectLeft, rectTop, bulletSize, bulletSize], label.style.bulletColor);
31804
32130
  }
31805
32131
  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;
32132
+ if (legendWidth) {
32133
+ if (label.style.textAlignment !== "left" /* LEFT */) {
32134
+ const textWidth = this.textWidth(label.style.textScale, label.text) * size;
32135
+ if (label.style.textAlignment === "right" /* RIGHT */) {
32136
+ textLeft = left + legendWidth - size * 1.5 - textWidth;
32137
+ } else {
32138
+ const remaining = legendWidth - (bulletMargin || size);
32139
+ textLeft += (remaining - textWidth) / 2;
32140
+ }
31810
32141
  } else {
31811
- const remaining = legendWidth - (bulletMargin || size);
31812
- textLeft += (remaining - textWidth) / 2;
32142
+ textLeft += bulletMargin;
31813
32143
  }
31814
- } else {
31815
- textLeft += bulletMargin;
31816
32144
  }
31817
32145
  this.drawText([textLeft, top], text, label.style.textScale, label.style.textColor);
31818
32146
  }
31819
32147
  // not included in public docs
31820
32148
  draw3DLabels(mvpMatrix, leftTopWidthHeight, secondPass = false) {
31821
- const labels = this.getAllLabels();
32149
+ const labels = this.getConnectomeLabels();
31822
32150
  if (!this.opts.showLegend || labels.length === 0) {
31823
32151
  return;
31824
32152
  }
@@ -31856,6 +32184,51 @@ var Niivue = class _Niivue {
31856
32184
  }
31857
32185
  }
31858
32186
  }
32187
+ drawAnchoredLabels() {
32188
+ const size = this.opts.textHeight * Math.min(this.gl.canvas.height, this.gl.canvas.width) * 1;
32189
+ const anchoredLabels = this.document.labels.filter((l) => l.anchor != null && l.anchor !== 0 /* NONE */);
32190
+ for (const label of anchoredLabels) {
32191
+ const text = label.text;
32192
+ const textHeight = this.textHeight(label.style.textScale, text) * size;
32193
+ const textWidth = this.textWidth(label.style.textScale, text) * size;
32194
+ let left;
32195
+ let top;
32196
+ const scale = 1;
32197
+ const verticalMargin = this.opts.textHeight * this.gl.canvas.height * scale;
32198
+ const rectHeightDiff = verticalMargin;
32199
+ let rectWidthDiff = verticalMargin / 4;
32200
+ let rectHorizontalOffset = 0;
32201
+ let rectVerticalOffset = 0;
32202
+ if (label.anchor & 1 /* LEFT */) {
32203
+ left = 0;
32204
+ }
32205
+ if (label.anchor & 4 /* RIGHT */) {
32206
+ left = this.canvas.width - textWidth;
32207
+ rectHorizontalOffset -= verticalMargin / 4;
32208
+ }
32209
+ if (label.anchor & 2 /* CENTER */) {
32210
+ left = (this.canvas.width - textWidth) / 2;
32211
+ rectHorizontalOffset -= verticalMargin / 4;
32212
+ rectWidthDiff += verticalMargin / 4;
32213
+ }
32214
+ if (label.anchor & 8 /* TOP */) {
32215
+ top = 0;
32216
+ }
32217
+ if (label.anchor & 16 /* MIDDLE */) {
32218
+ top = (this.canvas.height - textHeight - verticalMargin) / 2;
32219
+ rectVerticalOffset -= verticalMargin / 4;
32220
+ }
32221
+ if (label.anchor & 32 /* BOTTOM */) {
32222
+ top = this.canvas.height - textHeight - verticalMargin;
32223
+ rectVerticalOffset -= verticalMargin / 4;
32224
+ }
32225
+ this.drawRect(
32226
+ [left + rectHorizontalOffset, top + rectVerticalOffset, textWidth + rectWidthDiff, textHeight + rectHeightDiff],
32227
+ label.style.backgroundColor
32228
+ );
32229
+ this.draw3DLabel(label, [left, top]);
32230
+ }
32231
+ }
31859
32232
  // not included in public docs
31860
32233
  draw3D(leftTopWidthHeight = [0, 0, 0, 0], mvpMatrix = null, modelMatrix = null, normalMatrix = null, azimuth = null, elevation = 0) {
31861
32234
  const isMosaic = azimuth !== null;
@@ -32731,6 +33104,12 @@ var Niivue = class _Niivue {
32731
33104
  }
32732
33105
  const isDrawPenDown = isFinite(this.drawPenLocation[0]) && this.opts.drawingEnabled;
32733
33106
  const { volScale } = this.sliceScale();
33107
+ const actualScale = volScale.slice();
33108
+ if (this.opts.multiplanarEqualSize) {
33109
+ volScale[0] = 1;
33110
+ volScale[1] = 1;
33111
+ volScale[2] = 1;
33112
+ }
32734
33113
  if (typeof this.opts.multiplanarPadPixels !== "number") {
32735
33114
  log.debug("multiplanarPadPixels must be numeric");
32736
33115
  }
@@ -32768,38 +33147,43 @@ var Niivue = class _Niivue {
32768
33147
  isDrawGrid = true;
32769
33148
  }
32770
33149
  }
33150
+ let ltwh = ltwh2x2;
32771
33151
  if (isDrawColumn) {
32772
- let ltwh = ltwh1x3;
33152
+ ltwh = ltwh1x3;
32773
33153
  if (isShowRender || this.opts.multiplanarShowRender === 2 /* AUTO */ && ltwh1x4[4] >= ltwh1x3[4]) {
32774
33154
  ltwh = ltwh1x4;
32775
33155
  } else {
32776
33156
  isDraw3D = false;
32777
33157
  }
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
33158
  } else if (isDrawRow) {
32789
- let ltwh = ltwh3x1;
33159
+ ltwh = ltwh3x1;
32790
33160
  if (isShowRender || this.opts.multiplanarShowRender === 2 /* AUTO */ && ltwh4x1[4] >= ltwh3x1[4]) {
32791
33161
  ltwh = ltwh4x1;
32792
33162
  } else {
32793
33163
  isDraw3D = false;
32794
33164
  }
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);
33165
+ }
33166
+ const sX = volScale[0] * ltwh[4];
33167
+ const sY = volScale[1] * ltwh[4];
33168
+ const sZ = volScale[2] * ltwh[4];
33169
+ const actualX = actualScale[0] * ltwh[4];
33170
+ const actualY = actualScale[1] * ltwh[4];
33171
+ const actualZ = actualScale[2] * ltwh[4];
33172
+ if (isDrawColumn) {
33173
+ this.draw2D([ltwh[0], ltwh[1], sX, sY], 0, NaN, [actualX, actualY]);
33174
+ this.draw2D([ltwh[0], ltwh[1] + sY + pad, sX, sZ], 1, NaN, [actualX, actualZ]);
33175
+ this.draw2D([ltwh[0], ltwh[1] + sY + pad + sZ + pad, sY, sZ], 2, NaN, [actualY, actualZ]);
33176
+ if (isDraw3D) {
33177
+ const sMx = mx * ltwh[4];
33178
+ this.draw3D([ltwh[0], ltwh[1] + sY + sZ + sZ + pad * 3, sMx, sMx]);
33179
+ }
33180
+ } else if (isDrawRow) {
33181
+ this.draw2D([ltwh[0], ltwh[1], sX, sY], 0, NaN, [actualX, actualY]);
33182
+ this.draw2D([ltwh[0] + sX + pad, ltwh[1], sX, sZ], 1, NaN, [actualX, actualZ]);
33183
+ this.draw2D([ltwh[0] + sX + sX + pad * 2, ltwh[1], sY, sZ], 2, NaN, [actualY, actualZ]);
32801
33184
  if (isDraw3D) {
32802
- this.draw3D([ltwh[0] + sX + sX + sY + pad * 3, ltwh[1], ltwh[3], ltwh[3]]);
33185
+ const sMx = mx * ltwh[4];
33186
+ this.draw3D([ltwh[0] + sX + sX + sY + pad * 3, ltwh[1], sMx, sMx]);
32803
33187
  }
32804
33188
  } else if (isDrawGrid) {
32805
33189
  if (!isShowRender) {
@@ -32808,13 +33192,9 @@ var Niivue = class _Niivue {
32808
33192
  if (this.opts.multiplanarShowRender === 2 /* AUTO */) {
32809
33193
  isDraw3D = true;
32810
33194
  }
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);
33195
+ this.draw2D([ltwh[0], ltwh[1] + sZ + pad, sX, sY], 0, NaN, [actualX, actualY]);
33196
+ this.draw2D([ltwh[0], ltwh[1], sX, sZ], 1, NaN, [actualX, actualZ]);
33197
+ this.draw2D([ltwh[0] + sX + pad, ltwh[1], sY, sZ], 2, NaN, [actualY, actualZ]);
32818
33198
  if (isDraw3D) {
32819
33199
  this.draw3D([ltwh[0] + sX + pad, ltwh[1] + sZ + pad, sY, sY]);
32820
33200
  }
@@ -32879,10 +33259,25 @@ var Niivue = class _Niivue {
32879
33259
  height
32880
33260
  ]);
32881
33261
  }
33262
+ if (this.opts.clickToSegment) {
33263
+ const x = this.mousePos[0];
33264
+ const y = this.mousePos[1];
33265
+ const tileIdx = this.tileIndex(x, y);
33266
+ if (tileIdx > -1) {
33267
+ const fovMM = this.screenSlices[tileIdx].fovMM;
33268
+ const ltwh = this.screenSlices[tileIdx].leftTopWidthHeight;
33269
+ const pixPerMM = ltwh[2] / fovMM[0];
33270
+ const color = this.opts.crosshairColor;
33271
+ const segmentCursorColor = [color[0], color[1], color[2], 0.4];
33272
+ const radius = this.opts.clickToSegmentRadius * pixPerMM;
33273
+ this.drawCircle([x - radius, y - radius, radius * 2, radius * 2], segmentCursorColor, 1);
33274
+ }
33275
+ }
32882
33276
  const pos = this.frac2mm([this.scene.crosshairPos[0], this.scene.crosshairPos[1], this.scene.crosshairPos[2]]);
32883
33277
  posString = pos[0].toFixed(2) + "\xD7" + pos[1].toFixed(2) + "\xD7" + pos[2].toFixed(2);
32884
33278
  this.readyForSync = true;
32885
33279
  this.sync();
33280
+ this.drawAnchoredLabels();
32886
33281
  return posString;
32887
33282
  }
32888
33283
  // not included in public docs
@@ -32917,6 +33312,7 @@ export {
32917
33312
  DEFAULT_OPTIONS,
32918
33313
  DRAG_MODE,
32919
33314
  INITIAL_SCENE_DATA,
33315
+ LabelAnchorPoint,
32920
33316
  LabelLineTerminator,
32921
33317
  LabelTextAlignment,
32922
33318
  MULTIPLANAR_TYPE,