@niivue/niivue 0.44.2 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -10,7 +10,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
10
10
  import { mat4 as mat46, vec2, vec3 as vec311, vec4 as vec48 } from "gl-matrix";
11
11
 
12
12
  // package.json
13
- var version = "0.44.2";
13
+ var version = "0.45.0";
14
14
 
15
15
  // src/logger.ts
16
16
  var _Log = class _Log {
@@ -12393,6 +12393,12 @@ var NVMeshLoaders = class _NVMeshLoaders {
12393
12393
  while (lines[pos].length < 1) {
12394
12394
  pos++;
12395
12395
  }
12396
+ if (lines[pos].startsWith("METADATA")) {
12397
+ while (lines[pos].length > 1) {
12398
+ pos++;
12399
+ }
12400
+ pos++;
12401
+ }
12396
12402
  items = lines[pos].trim().split(/\s+/);
12397
12403
  pos++;
12398
12404
  if (items[0].includes("LINES")) {
@@ -12571,7 +12577,7 @@ var NVMeshLoaders = class _NVMeshLoaders {
12571
12577
  layer.values = obj;
12572
12578
  }
12573
12579
  }
12574
- } else if (ext === "CRV" || ext === "CURV") {
12580
+ } else if (ext === "CRV" || ext === "CURV" || ext === "THICKNESS" || ext === "AREA") {
12575
12581
  layer.values = _NVMeshLoaders.readCURV(buffer, n_vert);
12576
12582
  layer.isTransparentBelowCalMin = false;
12577
12583
  } else if (ext === "GII") {
@@ -12596,6 +12602,9 @@ var NVMeshLoaders = class _NVMeshLoaders {
12596
12602
  layer.values = _NVMeshLoaders.readSMP(buffer, n_vert);
12597
12603
  } else if (ext === "STC") {
12598
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;
12599
12608
  } else {
12600
12609
  log.warn("Unknown layer overlay format " + name);
12601
12610
  return layer;
@@ -12769,6 +12778,17 @@ var NVMeshLoaders = class _NVMeshLoaders {
12769
12778
  return f32;
12770
12779
  }
12771
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
+ }
12772
12792
  // read freesurfer curv big-endian format
12773
12793
  // https://github.com/bonilhamusclab/MRIcroS/blob/master/%2BfileUtils/%2Bpial/readPial.m
12774
12794
  // http://www.grahamwideman.com/gw/brain/fs/surfacefileformats.htm
@@ -20032,7 +20052,7 @@ var NVImage = class _NVImage {
20032
20052
  hdr.datatypeCode = 64 /* DT_FLOAT64 */;
20033
20053
  break;
20034
20054
  default:
20035
- throw new Error("Unsupported NRRD data type: " + items[0]);
20055
+ throw new Error("Unsupported MHA data type: " + items[0]);
20036
20056
  }
20037
20057
  }
20038
20058
  if (line.startsWith("ObjectType") && !items[0].includes("Image")) {
@@ -20054,6 +20074,9 @@ var NVImage = class _NVImage {
20054
20074
  [mat33[2], mat33[5], mat33[8], offset[2]],
20055
20075
  [0, 0, 0, 1]
20056
20076
  ];
20077
+ while (bytes[pos] === 10) {
20078
+ pos++;
20079
+ }
20057
20080
  hdr.vox_offset = pos;
20058
20081
  if (isDetached && pairedImgData) {
20059
20082
  if (isGz) {
@@ -22261,6 +22284,7 @@ var DEFAULT_OPTIONS = {
22261
22284
  multiplanarPadPixels: 0,
22262
22285
  // @deprecated
22263
22286
  multiplanarForceRender: false,
22287
+ multiplanarEqualSize: false,
22264
22288
  multiplanarShowRender: 2 /* AUTO */,
22265
22289
  // auto is the same behaviour as multiplanarForceRender: false
22266
22290
  isRadiologicalConvention: false,
@@ -22301,6 +22325,7 @@ var DEFAULT_OPTIONS = {
22301
22325
  clickToSegmentBright: true
22302
22326
  };
22303
22327
  var INITIAL_SCENE_DATA = {
22328
+ gamma: 1,
22304
22329
  azimuth: 110,
22305
22330
  elevation: 10,
22306
22331
  crosshairPos: vec38.create(),
@@ -22539,6 +22564,9 @@ var NVDocument = class _NVDocument {
22539
22564
  data.opts.meshThicknessOn2D = "infinity";
22540
22565
  }
22541
22566
  data.labels = [...this.data.labels];
22567
+ for (const label of data.labels) {
22568
+ delete label.onClick;
22569
+ }
22542
22570
  data.customData = this.customData;
22543
22571
  if (this.volumes.length) {
22544
22572
  let imageOptions = this.imageOptionsArray[0];
@@ -22794,6 +22822,19 @@ var LabelLineTerminator = /* @__PURE__ */ ((LabelLineTerminator2) => {
22794
22822
  LabelLineTerminator2["RING"] = "ring";
22795
22823
  return LabelLineTerminator2;
22796
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 || {});
22797
22838
  var NVLabel3DStyle = class {
22798
22839
  /**
22799
22840
  * @param textColor - Color of text
@@ -22831,13 +22872,17 @@ var NVLabel3D = class {
22831
22872
  * @param style - The style of the label
22832
22873
  * @param points - An array of points label for label lines
22833
22874
  */
22834
- constructor(text, style, points) {
22875
+ constructor(text, style, points, anchor, onClick) {
22835
22876
  __publicField(this, "text");
22836
22877
  __publicField(this, "style");
22837
22878
  __publicField(this, "points");
22879
+ __publicField(this, "anchor");
22880
+ __publicField(this, "onClick");
22838
22881
  this.text = text;
22839
22882
  this.style = style;
22840
22883
  this.points = points;
22884
+ this.anchor = anchor || 0 /* NONE */;
22885
+ this.onClick = onClick;
22841
22886
  }
22842
22887
  };
22843
22888
 
@@ -23979,6 +24024,7 @@ var Niivue = class _Niivue {
23979
24024
  * @param options - options object to set modifiable Niivue properties
23980
24025
  */
23981
24026
  constructor(options = DEFAULT_OPTIONS) {
24027
+ __publicField(this, "loaders", {});
23982
24028
  __publicField(this, "canvas", null);
23983
24029
  // the reference to the canvas element on the page
23984
24030
  __publicField(this, "_gl", null);
@@ -24588,6 +24634,11 @@ var Niivue = class _Niivue {
24588
24634
  }
24589
24635
  const thisMM = this.frac2mm(this.scene.crosshairPos);
24590
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
+ }
24591
24642
  if (this.syncOpts["2d"]) {
24592
24643
  this.otherNV.scene.crosshairPos = this.otherNV.mm2frac(thisMM);
24593
24644
  this.otherNV.scene.pan2Dxyzmm = vec48.clone(this.scene.pan2Dxyzmm);
@@ -24604,6 +24655,11 @@ var Niivue = class _Niivue {
24604
24655
  if (this.otherNV[i] === this) {
24605
24656
  continue;
24606
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
+ }
24607
24663
  if (this.syncOpts["2d"]) {
24608
24664
  this.otherNV[i].scene.crosshairPos = this.otherNV[i].mm2frac(thisMM);
24609
24665
  this.otherNV[i].scene.pan2Dxyzmm = vec48.clone(this.scene.pan2Dxyzmm);
@@ -24719,6 +24775,10 @@ var Niivue = class _Niivue {
24719
24775
  const [x, y] = [pos.x * this.uiData.dpr, pos.y * this.uiData.dpr];
24720
24776
  const label = this.getLabelAtPoint([x, y]);
24721
24777
  if (label) {
24778
+ if (label.onClick) {
24779
+ label.onClick(label);
24780
+ return;
24781
+ }
24722
24782
  for (const mesh of this.meshes) {
24723
24783
  if (mesh.type !== "connectome" /* CONNECTOME */) {
24724
24784
  continue;
@@ -25033,6 +25093,15 @@ var Niivue = class _Niivue {
25033
25093
  this.drawScene();
25034
25094
  this.uiData.prevX = this.uiData.currX;
25035
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();
25036
25105
  }
25037
25106
  }
25038
25107
  // not included in public docs
@@ -25270,6 +25339,11 @@ var Niivue = class _Niivue {
25270
25339
  if (ext === "GZ") {
25271
25340
  ext = re.exec(fullname.slice(0, -3))[1];
25272
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}`;
25273
25347
  }
25274
25348
  return upperCase ? ext : ext.toLowerCase();
25275
25349
  }
@@ -25290,6 +25364,28 @@ var Niivue = class _Niivue {
25290
25364
  }
25291
25365
  async addVolumesFromUrl(imageOptionsArray) {
25292
25366
  const promises = imageOptionsArray.map(async (imageItem) => {
25367
+ const ext = this.getFileExt(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
+ }
25293
25389
  const imageOptions = {
25294
25390
  url: imageItem.url,
25295
25391
  headers: imageItem.headers,
@@ -25429,6 +25525,40 @@ var Niivue = class _Niivue {
25429
25525
  this.addVolume(volume);
25430
25526
  });
25431
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
+ }
25432
25562
  // not included in public docs
25433
25563
  dropListener(e) {
25434
25564
  e.stopPropagation();
@@ -27100,6 +27230,37 @@ var Niivue = class _Niivue {
27100
27230
  this.document.meshes = this.meshes;
27101
27231
  return this.document.download(fileName, compress2);
27102
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
+ }
27103
27264
  /**
27104
27265
  * load an array of volume objects
27105
27266
  * @param volumeList - the array of objects to load. each object must have a resolvable "url" property at a minimum
@@ -27119,27 +27280,7 @@ var Niivue = class _Niivue {
27119
27280
  this.volumes = [];
27120
27281
  this.gl.clearColor(0, 0, 0, 1);
27121
27282
  this.gl.clear(this.gl.COLOR_BUFFER_BIT);
27122
- if (volumeList.length > 1) {
27123
- await this.addVolumesFromUrl(volumeList);
27124
- return this;
27125
- }
27126
- const imageOptions = {
27127
- url: volumeList[0].url,
27128
- headers: volumeList[0].headers,
27129
- name: volumeList[0].name,
27130
- colormap: volumeList[0].colormap ? volumeList[0].colormap : volumeList[0].colorMap,
27131
- colormapNegative: volumeList[0].colormapNegative ? volumeList[0].colormapNegative : volumeList[0].colorMapNegative,
27132
- opacity: volumeList[0].opacity,
27133
- urlImgData: volumeList[0].urlImgData,
27134
- cal_min: volumeList[0].cal_min,
27135
- cal_max: volumeList[0].cal_max,
27136
- trustCalMinMax: this.opts.trustCalMinMax,
27137
- isManifest: volumeList[0].isManifest,
27138
- frame4D: volumeList[0].frame4D,
27139
- limitFrames4D: volumeList[0].limitFrames4D || this.opts.limitFrames4D,
27140
- colorbarVisible: volumeList[0].colorbarVisible
27141
- };
27142
- await this.addVolumeFromUrl(imageOptions);
27283
+ await this.addVolumesFromUrl(volumeList);
27143
27284
  return this;
27144
27285
  }
27145
27286
  /**
@@ -27170,6 +27311,28 @@ var Niivue = class _Niivue {
27170
27311
  async addMeshesFromUrl(meshOptions) {
27171
27312
  const promises = meshOptions.map(async (meshItem) => {
27172
27313
  const ext = this.getFileExt(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
+ }
27173
27336
  if (ext === "JCON" || ext === "JSON") {
27174
27337
  const response = await fetch(meshItem.url, {});
27175
27338
  const json = await response.json();
@@ -27210,13 +27373,7 @@ var Niivue = class _Niivue {
27210
27373
  this.meshes = [];
27211
27374
  this.gl.clearColor(0, 0, 0, 1);
27212
27375
  this.gl.clear(this.gl.COLOR_BUFFER_BIT);
27213
- if (meshList.length > 1) {
27214
- await this.addMeshesFromUrl(meshList);
27215
- this.updateGLVolume();
27216
- this.drawScene();
27217
- return this;
27218
- }
27219
- await this.addMeshFromUrl(meshList[0]);
27376
+ await this.addMeshesFromUrl(meshList);
27220
27377
  this.updateGLVolume();
27221
27378
  this.drawScene();
27222
27379
  return this;
@@ -29781,6 +29938,7 @@ var Niivue = class _Niivue {
29781
29938
  * @see {@link https://niivue.github.io/niivue/features/colormaps.html | live demo usage}
29782
29939
  */
29783
29940
  setGamma(gamma = 1) {
29941
+ this.scene.gamma = gamma;
29784
29942
  cmapper.gamma = gamma;
29785
29943
  this.updateGLVolume();
29786
29944
  }
@@ -30144,6 +30302,7 @@ var Niivue = class _Niivue {
30144
30302
  }
30145
30303
  if (this.opts.drawingEnabled) {
30146
30304
  const pt = this.frac2vox(this.scene.crosshairPos);
30305
+ const ptMM = this.frac2mm(this.scene.crosshairPos);
30147
30306
  if (this.opts.clickToSegment) {
30148
30307
  const radius = this.opts.clickToSegmentRadius;
30149
30308
  const steps = this.opts.clickToSegmentSteps;
@@ -30152,11 +30311,12 @@ var Niivue = class _Niivue {
30152
30311
  this.drawPenAxCorSag = axCorSag;
30153
30312
  for (let i2 = 1; i2 <= steps; i2++) {
30154
30313
  const angle = i2 / steps * 2 * Math.PI;
30155
- let xVox = pt[0] + radius * Math.cos(angle);
30156
- let yVox = pt[1] + radius * Math.sin(angle);
30157
- const zVox = pt[2];
30158
- xVox = Math.round(xVox);
30159
- 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];
30160
30320
  this.drawPt(xVox, yVox, zVox, this.opts.penValue);
30161
30321
  this.drawPenFillPts.push([xVox, yVox, pt[2]]);
30162
30322
  if (i2 === steps) {
@@ -30440,9 +30600,21 @@ var Niivue = class _Niivue {
30440
30600
  const labels = [...this.document.labels, ...definedMeshLabels];
30441
30601
  return labels;
30442
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
+ }
30443
30615
  getBulletMarginWidth() {
30444
30616
  let bulletMargin = 0;
30445
- const labels = this.getAllLabels();
30617
+ const labels = this.getConnectomeLabels();
30446
30618
  if (labels.length === 0) {
30447
30619
  return 0;
30448
30620
  }
@@ -30459,7 +30631,7 @@ var Niivue = class _Niivue {
30459
30631
  return bulletMargin;
30460
30632
  }
30461
30633
  getLegendPanelWidth() {
30462
- const labels = this.getAllLabels();
30634
+ const labels = this.getConnectomeLabels();
30463
30635
  if (!this.opts.showLegend || labels.length === 0) {
30464
30636
  return 0;
30465
30637
  }
@@ -30485,7 +30657,7 @@ var Niivue = class _Niivue {
30485
30657
  return width;
30486
30658
  }
30487
30659
  getLegendPanelHeight() {
30488
- const labels = this.getAllLabels();
30660
+ const labels = this.getConnectomeLabels();
30489
30661
  let height = 0;
30490
30662
  const scale = 1;
30491
30663
  const verticalMargin = this.opts.textHeight * this.gl.canvas.height * scale;
@@ -30927,7 +31099,7 @@ var Niivue = class _Niivue {
30927
31099
  }
30928
31100
  // not included in public docs
30929
31101
  // show text labels for L/R, A/P, I/S dimensions
30930
- drawSliceOrientationText(leftTopWidthHeight, axCorSag) {
31102
+ drawSliceOrientationText(leftTopWidthHeight, axCorSag, padLeftTop = [NaN, NaN]) {
30931
31103
  this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
30932
31104
  let topText = "S";
30933
31105
  if (axCorSag === 0 /* AXIAL */) {
@@ -30941,8 +31113,32 @@ var Niivue = class _Niivue {
30941
31113
  this.drawTextRightBelow([leftTopWidthHeight[0], leftTopWidthHeight[1]], leftText + topText);
30942
31114
  return;
30943
31115
  }
30944
- this.drawTextBelow([leftTopWidthHeight[0] + leftTopWidthHeight[2] * 0.5, leftTopWidthHeight[1]], topText);
30945
- 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
+ }
30946
31142
  }
30947
31143
  // not included in public docs
30948
31144
  xyMM2xyzMM(axCorSag, sliceFrac) {
@@ -30980,7 +31176,7 @@ var Niivue = class _Niivue {
30980
31176
  }
30981
31177
  // not included in public docs
30982
31178
  // draw 2D tile
30983
- draw2D(leftTopWidthHeight, axCorSag, customMM = NaN) {
31179
+ draw2DMain(leftTopWidthHeight, axCorSag, customMM = NaN) {
30984
31180
  let frac2mmTexture = this.volumes[0].frac2mm.slice();
30985
31181
  let screen = this.screenFieldOfViewExtendedMM(axCorSag);
30986
31182
  let mesh2ortho = mat46.create();
@@ -31009,7 +31205,9 @@ var Niivue = class _Niivue {
31009
31205
  elevation = isRadiolgical ? -90 : 90;
31010
31206
  }
31011
31207
  const gl = this.gl;
31208
+ let isStretchToScreen = false;
31012
31209
  if (leftTopWidthHeight[2] === 0 || leftTopWidthHeight[3] === 0) {
31210
+ isStretchToScreen = true;
31013
31211
  const pixPerMMw = gl.canvas.width / screen.fovMM[0];
31014
31212
  const pixPerMMh = gl.canvas.height / screen.fovMM[1];
31015
31213
  const pixPerMMmin = Math.min(pixPerMMw, pixPerMMh);
@@ -31156,9 +31354,32 @@ var Niivue = class _Niivue {
31156
31354
  if (isNaN(customMM)) {
31157
31355
  this.drawCrosshairs3D(false, 0.15, obj.modelViewProjectionMatrix, true, this.opts.isSliceMM);
31158
31356
  }
31159
- this.drawSliceOrientationText(leftTopWidthHeight, axCorSag);
31357
+ if (isStretchToScreen) {
31358
+ this.drawSliceOrientationText(leftTopWidthHeight, axCorSag);
31359
+ }
31160
31360
  this.readyForSync = true;
31161
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
+ }
31162
31383
  // not included in public docs
31163
31384
  // determine 3D model view projection matrix
31164
31385
  calculateMvpMatrix(_unused, leftTopWidthHeight = [0, 0, 0, 0], azimuth, elevation) {
@@ -31769,7 +31990,7 @@ var Niivue = class _Niivue {
31769
31990
  * @param style - label style
31770
31991
  * @param point - 3D point on the model
31771
31992
  */
31772
- addLabel(text, style, points) {
31993
+ addLabel(text, style, points, anchor, onClick) {
31773
31994
  const defaultStyle = {
31774
31995
  textColor: this.opts.legendTextColor,
31775
31996
  textScale: 1,
@@ -31781,7 +32002,7 @@ var Niivue = class _Niivue {
31781
32002
  bulletColor: this.opts.legendTextColor
31782
32003
  };
31783
32004
  const labelStyle = style ? { ...defaultStyle, ...style } : { ...defaultStyle };
31784
- const label = new NVLabel3D(text, labelStyle, points);
32005
+ const label = new NVLabel3D(text, { ...labelStyle }, points, anchor, onClick);
31785
32006
  this.document.labels.push(label);
31786
32007
  return label;
31787
32008
  }
@@ -31799,6 +32020,60 @@ var Niivue = class _Niivue {
31799
32020
  return screenPoint;
31800
32021
  }
31801
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
+ }
31802
32077
  log.debug("screenPoint", screenPoint);
31803
32078
  const panelHeight = this.getLegendPanelHeight();
31804
32079
  const panelWidth = this.getLegendPanelWidth();
@@ -31808,9 +32083,7 @@ var Niivue = class _Niivue {
31808
32083
  if (screenPoint[0] < left || screenPoint[1] < top || screenPoint[0] > left + panelWidth || screenPoint[1] > top + panelHeight) {
31809
32084
  return null;
31810
32085
  }
31811
- const scale = 1;
31812
- const size = this.opts.textHeight * Math.min(this.gl.canvas.height, this.gl.canvas.width) * scale;
31813
- const labels = this.getAllLabels();
32086
+ const labels = this.getConnectomeLabels();
31814
32087
  for (const label of labels) {
31815
32088
  const labelSize = this.opts.textHeight * this.gl.canvas.height * label.style.textScale;
31816
32089
  const textHeight = this.textHeight(labelSize, label.text);
@@ -31839,7 +32112,7 @@ var Niivue = class _Niivue {
31839
32112
  }
31840
32113
  }
31841
32114
  // not included in public docs
31842
- draw3DLabel(label, pos, mvpMatrix, leftTopWidthHeight, bulletMargin = 0, legendWidth, secondPass = false) {
32115
+ draw3DLabel(label, pos, mvpMatrix, leftTopWidthHeight, bulletMargin, legendWidth, secondPass) {
31843
32116
  const text = label.text;
31844
32117
  const left = pos[0];
31845
32118
  const top = pos[1];
@@ -31856,22 +32129,24 @@ var Niivue = class _Niivue {
31856
32129
  this.drawCircle([rectLeft, rectTop, bulletSize, bulletSize], label.style.bulletColor);
31857
32130
  }
31858
32131
  let textLeft = left;
31859
- if (label.style.textAlignment !== "left" /* LEFT */) {
31860
- const textWidth = this.textWidth(label.style.textScale, label.text) * size;
31861
- if (label.style.textAlignment === "right" /* RIGHT */) {
31862
- 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
+ }
31863
32141
  } else {
31864
- const remaining = legendWidth - (bulletMargin || size);
31865
- textLeft += (remaining - textWidth) / 2;
32142
+ textLeft += bulletMargin;
31866
32143
  }
31867
- } else {
31868
- textLeft += bulletMargin;
31869
32144
  }
31870
32145
  this.drawText([textLeft, top], text, label.style.textScale, label.style.textColor);
31871
32146
  }
31872
32147
  // not included in public docs
31873
32148
  draw3DLabels(mvpMatrix, leftTopWidthHeight, secondPass = false) {
31874
- const labels = this.getAllLabels();
32149
+ const labels = this.getConnectomeLabels();
31875
32150
  if (!this.opts.showLegend || labels.length === 0) {
31876
32151
  return;
31877
32152
  }
@@ -31909,6 +32184,51 @@ var Niivue = class _Niivue {
31909
32184
  }
31910
32185
  }
31911
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
+ }
31912
32232
  // not included in public docs
31913
32233
  draw3D(leftTopWidthHeight = [0, 0, 0, 0], mvpMatrix = null, modelMatrix = null, normalMatrix = null, azimuth = null, elevation = 0) {
31914
32234
  const isMosaic = azimuth !== null;
@@ -32784,6 +33104,12 @@ var Niivue = class _Niivue {
32784
33104
  }
32785
33105
  const isDrawPenDown = isFinite(this.drawPenLocation[0]) && this.opts.drawingEnabled;
32786
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
+ }
32787
33113
  if (typeof this.opts.multiplanarPadPixels !== "number") {
32788
33114
  log.debug("multiplanarPadPixels must be numeric");
32789
33115
  }
@@ -32821,38 +33147,43 @@ var Niivue = class _Niivue {
32821
33147
  isDrawGrid = true;
32822
33148
  }
32823
33149
  }
33150
+ let ltwh = ltwh2x2;
32824
33151
  if (isDrawColumn) {
32825
- let ltwh = ltwh1x3;
33152
+ ltwh = ltwh1x3;
32826
33153
  if (isShowRender || this.opts.multiplanarShowRender === 2 /* AUTO */ && ltwh1x4[4] >= ltwh1x3[4]) {
32827
33154
  ltwh = ltwh1x4;
32828
33155
  } else {
32829
33156
  isDraw3D = false;
32830
33157
  }
32831
- const sX = volScale[0] * ltwh[4];
32832
- const sY = volScale[1] * ltwh[4];
32833
- const sZ = volScale[2] * ltwh[4];
32834
- const sMx = mx * ltwh[4];
32835
- this.draw2D([ltwh[0], ltwh[1], sX, sY], 0);
32836
- this.draw2D([ltwh[0], ltwh[1] + sY + pad, sX, sZ], 1);
32837
- this.draw2D([ltwh[0], ltwh[1] + sY + pad + sZ + pad, sY, sZ], 2);
32838
- if (isDraw3D) {
32839
- this.draw3D([ltwh[0], ltwh[1] + sY + sZ + sZ + pad * 3, sMx, sMx]);
32840
- }
32841
33158
  } else if (isDrawRow) {
32842
- let ltwh = ltwh3x1;
33159
+ ltwh = ltwh3x1;
32843
33160
  if (isShowRender || this.opts.multiplanarShowRender === 2 /* AUTO */ && ltwh4x1[4] >= ltwh3x1[4]) {
32844
33161
  ltwh = ltwh4x1;
32845
33162
  } else {
32846
33163
  isDraw3D = false;
32847
33164
  }
32848
- const sX = volScale[0] * ltwh[4];
32849
- const sY = volScale[1] * ltwh[4];
32850
- const sZ = volScale[2] * ltwh[4];
32851
- this.draw2D([ltwh[0], ltwh[1], sX, sY], 0);
32852
- this.draw2D([ltwh[0] + sX + pad, ltwh[1], sX, sZ], 1);
32853
- 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]);
32854
33184
  if (isDraw3D) {
32855
- 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]);
32856
33187
  }
32857
33188
  } else if (isDrawGrid) {
32858
33189
  if (!isShowRender) {
@@ -32861,13 +33192,9 @@ var Niivue = class _Niivue {
32861
33192
  if (this.opts.multiplanarShowRender === 2 /* AUTO */) {
32862
33193
  isDraw3D = true;
32863
33194
  }
32864
- const ltwh = ltwh2x2;
32865
- const sX = volScale[0] * ltwh[4];
32866
- const sY = volScale[1] * ltwh[4];
32867
- const sZ = volScale[2] * ltwh[4];
32868
- this.draw2D([ltwh[0], ltwh[1] + sZ + pad, sX, sY], 0);
32869
- this.draw2D([ltwh[0], ltwh[1], sX, sZ], 1);
32870
- 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]);
32871
33198
  if (isDraw3D) {
32872
33199
  this.draw3D([ltwh[0] + sX + pad, ltwh[1] + sZ + pad, sY, sY]);
32873
33200
  }
@@ -32932,10 +33259,25 @@ var Niivue = class _Niivue {
32932
33259
  height
32933
33260
  ]);
32934
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
+ }
32935
33276
  const pos = this.frac2mm([this.scene.crosshairPos[0], this.scene.crosshairPos[1], this.scene.crosshairPos[2]]);
32936
33277
  posString = pos[0].toFixed(2) + "\xD7" + pos[1].toFixed(2) + "\xD7" + pos[2].toFixed(2);
32937
33278
  this.readyForSync = true;
32938
33279
  this.sync();
33280
+ this.drawAnchoredLabels();
32939
33281
  return posString;
32940
33282
  }
32941
33283
  // not included in public docs
@@ -32970,6 +33312,7 @@ export {
32970
33312
  DEFAULT_OPTIONS,
32971
33313
  DRAG_MODE,
32972
33314
  INITIAL_SCENE_DATA,
33315
+ LabelAnchorPoint,
32973
33316
  LabelLineTerminator,
32974
33317
  LabelTextAlignment,
32975
33318
  MULTIPLANAR_TYPE,