@niivue/niivue 0.44.0 → 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.0";
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;
@@ -11254,6 +11330,7 @@ var cmapper = new ColorTables();
11254
11330
 
11255
11331
  // src/nvmesh-utilities.ts
11256
11332
  import { vec3 as vec32 } from "gl-matrix";
11333
+ import { gzipSync } from "fflate/browser";
11257
11334
  var NVMeshUtilities = class {
11258
11335
  static getClusterBoundaryU8(u8, faces) {
11259
11336
  const border = new Array(u8.length).fill(false);
@@ -11279,6 +11356,112 @@ var NVMeshUtilities = class {
11279
11356
  }
11280
11357
  return border;
11281
11358
  }
11359
+ static createMZ3(vertices, indices, compress2 = false) {
11360
+ const magic = 23117;
11361
+ const attr = 3;
11362
+ const nface = indices.length / 3;
11363
+ const nvert = vertices.length / 3;
11364
+ const nskip = 0;
11365
+ const headerSize = 16;
11366
+ const indexSize = nface * 3 * 4;
11367
+ const vertexSize = nvert * 3 * 4;
11368
+ const totalSize = headerSize + indexSize + vertexSize;
11369
+ const buffer = new ArrayBuffer(totalSize);
11370
+ const writer = new DataView(buffer);
11371
+ writer.setUint16(0, magic, true);
11372
+ writer.setUint16(2, attr, true);
11373
+ writer.setUint32(4, nface, true);
11374
+ writer.setUint32(8, nvert, true);
11375
+ writer.setUint32(12, nskip, true);
11376
+ let offset = headerSize;
11377
+ new Uint32Array(buffer, offset, indices.length).set(indices);
11378
+ offset += indexSize;
11379
+ new Float32Array(buffer, offset, vertices.length).set(vertices);
11380
+ if (compress2) {
11381
+ return gzipSync(new Uint8Array(buffer));
11382
+ }
11383
+ return buffer;
11384
+ }
11385
+ static createOBJ(vertices, indices) {
11386
+ let objContent = "";
11387
+ for (let i = 0; i < vertices.length; i += 3) {
11388
+ objContent += `v ${vertices[i]} ${vertices[i + 1]} ${vertices[i + 2]}
11389
+ `;
11390
+ }
11391
+ for (let i = 0; i < indices.length; i += 3) {
11392
+ objContent += `f ${indices[i] + 1} ${indices[i + 1] + 1} ${indices[i + 2] + 1}
11393
+ `;
11394
+ }
11395
+ const encoder = new TextEncoder();
11396
+ const arrayBuffer = encoder.encode(objContent).buffer;
11397
+ return arrayBuffer;
11398
+ }
11399
+ static createSTL(vertices, indices) {
11400
+ const numTriangles = indices.length / 3;
11401
+ const bufferLength = 84 + numTriangles * 50;
11402
+ const arrayBuffer = new ArrayBuffer(bufferLength);
11403
+ const dataView = new DataView(arrayBuffer);
11404
+ for (let i = 0; i < 80; i++) {
11405
+ dataView.setUint8(i, 0);
11406
+ }
11407
+ dataView.setUint32(80, numTriangles, true);
11408
+ let offset = 84;
11409
+ for (let i = 0; i < indices.length; i += 3) {
11410
+ const i0 = indices[i] * 3;
11411
+ const i1 = indices[i + 1] * 3;
11412
+ const i2 = indices[i + 2] * 3;
11413
+ dataView.setFloat32(offset, 0, true);
11414
+ dataView.setFloat32(offset + 4, 0, true);
11415
+ dataView.setFloat32(offset + 8, 0, true);
11416
+ offset += 12;
11417
+ dataView.setFloat32(offset, vertices[i0], true);
11418
+ dataView.setFloat32(offset + 4, vertices[i0 + 1], true);
11419
+ dataView.setFloat32(offset + 8, vertices[i0 + 2], true);
11420
+ offset += 12;
11421
+ dataView.setFloat32(offset, vertices[i1], true);
11422
+ dataView.setFloat32(offset + 4, vertices[i1 + 1], true);
11423
+ dataView.setFloat32(offset + 8, vertices[i1 + 2], true);
11424
+ offset += 12;
11425
+ dataView.setFloat32(offset, vertices[i2], true);
11426
+ dataView.setFloat32(offset + 4, vertices[i2 + 1], true);
11427
+ dataView.setFloat32(offset + 8, vertices[i2 + 2], true);
11428
+ offset += 12;
11429
+ dataView.setUint16(offset, 0, true);
11430
+ offset += 2;
11431
+ }
11432
+ return arrayBuffer;
11433
+ }
11434
+ static downloadArrayBuffer(buffer, filename) {
11435
+ const blob = new Blob([buffer], { type: "application/octet-stream" });
11436
+ const url = URL.createObjectURL(blob);
11437
+ const a = document.createElement("a");
11438
+ a.href = url;
11439
+ a.download = filename;
11440
+ document.body.appendChild(a);
11441
+ a.style.display = "none";
11442
+ a.click();
11443
+ setTimeout(() => {
11444
+ document.body.removeChild(a);
11445
+ URL.revokeObjectURL(url);
11446
+ }, 0);
11447
+ }
11448
+ static saveMesh(vertices, indices, filename = ".mz3", compress2 = false) {
11449
+ let buff = new ArrayBuffer(0);
11450
+ if (/\.obj$/i.test(filename)) {
11451
+ buff = this.createOBJ(vertices, indices);
11452
+ } else if (/\.stl$/i.test(filename)) {
11453
+ buff = this.createSTL(vertices, indices);
11454
+ } else {
11455
+ if (!/\.mz3$/i.test(filename)) {
11456
+ filename += ".mz3";
11457
+ }
11458
+ buff = this.createMZ3(vertices, indices, compress2);
11459
+ }
11460
+ if (filename.length > 4) {
11461
+ this.downloadArrayBuffer(buff, filename);
11462
+ }
11463
+ return buff;
11464
+ }
11282
11465
  static getClusterBoundary(rgba8, faces) {
11283
11466
  const rgba32 = new Uint32Array(rgba8.buffer);
11284
11467
  const border = new Array(rgba32.length).fill(false);
@@ -12250,6 +12433,12 @@ var NVMeshLoaders = class _NVMeshLoaders {
12250
12433
  while (lines[pos].length < 1) {
12251
12434
  pos++;
12252
12435
  }
12436
+ if (lines[pos].startsWith("METADATA")) {
12437
+ while (lines[pos].length > 1) {
12438
+ pos++;
12439
+ }
12440
+ pos++;
12441
+ }
12253
12442
  items = lines[pos].trim().split(/\s+/);
12254
12443
  pos++;
12255
12444
  if (items[0].includes("LINES")) {
@@ -12428,7 +12617,7 @@ var NVMeshLoaders = class _NVMeshLoaders {
12428
12617
  layer.values = obj;
12429
12618
  }
12430
12619
  }
12431
- } else if (ext === "CRV" || ext === "CURV") {
12620
+ } else if (ext === "CRV" || ext === "CURV" || ext === "THICKNESS" || ext === "AREA") {
12432
12621
  layer.values = _NVMeshLoaders.readCURV(buffer, n_vert);
12433
12622
  layer.isTransparentBelowCalMin = false;
12434
12623
  } else if (ext === "GII") {
@@ -12453,6 +12642,9 @@ var NVMeshLoaders = class _NVMeshLoaders {
12453
12642
  layer.values = _NVMeshLoaders.readSMP(buffer, n_vert);
12454
12643
  } else if (ext === "STC") {
12455
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;
12456
12648
  } else {
12457
12649
  log.warn("Unknown layer overlay format " + name);
12458
12650
  return layer;
@@ -12626,6 +12818,17 @@ var NVMeshLoaders = class _NVMeshLoaders {
12626
12818
  return f32;
12627
12819
  }
12628
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
+ }
12629
12832
  // read freesurfer curv big-endian format
12630
12833
  // https://github.com/bonilhamusclab/MRIcroS/blob/master/%2BfileUtils/%2Bpial/readPial.m
12631
12834
  // http://www.grahamwideman.com/gw/brain/fs/surfacefileformats.htm
@@ -12669,7 +12872,8 @@ var NVMeshLoaders = class _NVMeshLoaders {
12669
12872
  static readANNOT(buffer, n_vert, isReadColortables = false) {
12670
12873
  const view = new DataView(buffer);
12671
12874
  const n_vertex = view.getUint32(0, false);
12672
- if (n_vert !== n_vertex) {
12875
+ const n_vertexDecimated = this.decimateLayerVertices(n_vertex, n_vert);
12876
+ if (n_vert !== n_vertexDecimated) {
12673
12877
  throw new Error("ANNOT file has different number of vertices than mesh");
12674
12878
  }
12675
12879
  if (buffer.byteLength < 4 + 8 * n_vertex) {
@@ -13917,6 +14121,20 @@ var NVMeshLoaders = class _NVMeshLoaders {
13917
14121
  };
13918
14122
  }
13919
14123
  // readSTL()
14124
+ static decimateLayerVertices(nVertLayer, nVertMesh) {
14125
+ if (nVertLayer % nVertMesh === 0) {
14126
+ return nVertLayer;
14127
+ }
14128
+ const V0 = 12;
14129
+ const orderLayer = Math.round(Math.log((nVertLayer - 2) / (V0 - 2)) / Math.log(4));
14130
+ const orderMesh = Math.round(Math.log((nVertMesh - 2) / (V0 - 2)) / Math.log(4));
14131
+ const nVLayer = Math.pow(4, orderLayer) * (V0 - 2) + 2;
14132
+ const nVMesh = Math.pow(4, orderMesh) * (V0 - 2) + 2;
14133
+ if (nVLayer !== nVertLayer || nVMesh !== nVertMesh) {
14134
+ return nVertLayer;
14135
+ }
14136
+ return nVertMesh;
14137
+ }
13920
14138
  // read NIfTI2 format with embedded CIfTI
13921
14139
  // this variation very specific to connectome workbench
13922
14140
  // https://brainder.org/2015/04/03/the-nifti-2-file-format/
@@ -14077,8 +14295,9 @@ var NVMeshLoaders = class _NVMeshLoaders {
14077
14295
  );
14078
14296
  return scalars2;
14079
14297
  }
14298
+ nvert = this.decimateLayerVertices(nvert, n_vert);
14080
14299
  if (nvert % n_vert !== 0) {
14081
- throw new Error("Vertices in NIfTI (" + nvert + ") is not a multiple of number of vertices (" + n_vert + ")");
14300
+ throw new Error("Vertices in layer (" + nvert + ") is not a multiple of number of vertices (" + n_vert + ")");
14082
14301
  }
14083
14302
  if (isLittleEndian) {
14084
14303
  if (datatype === 16) {
@@ -14157,8 +14376,9 @@ var NVMeshLoaders = class _NVMeshLoaders {
14157
14376
  const dim = reader.getUint16(40 + i * 2, isLittleEndian);
14158
14377
  nvert *= Math.max(dim, 1);
14159
14378
  }
14379
+ nvert = this.decimateLayerVertices(nvert, n_vert);
14160
14380
  if (nvert % n_vert !== 0) {
14161
- throw new Error("Vertices in NIfTI (" + nvert + ") is not a multiple of number of vertices (" + n_vert + ")");
14381
+ throw new Error("Vertices in layer (" + nvert + ") is not a multiple of number of vertices (" + n_vert + ")");
14162
14382
  }
14163
14383
  if (isLittleEndian) {
14164
14384
  if (datatype === 16) {
@@ -14214,10 +14434,11 @@ var NVMeshLoaders = class _NVMeshLoaders {
14214
14434
  if (version2 !== 1 || mtype < 0 || mtype > 4) {
14215
14435
  log.warn("Not a valid MGH file");
14216
14436
  }
14217
- const nvert = width * height * depth * nframes;
14437
+ let nvert = width * height * depth * nframes;
14218
14438
  let scalars = [];
14439
+ nvert = this.decimateLayerVertices(nvert, n_vert);
14219
14440
  if (nvert % n_vert !== 0) {
14220
- log.warn("Vertices in NIfTI (" + nvert + ") is not a multiple of number of vertices (" + n_vert + ")");
14441
+ log.warn("Vertices in layer (" + nvert + ") is not a multiple of number of vertices (" + n_vert + ")");
14221
14442
  return scalars;
14222
14443
  }
14223
14444
  if (mtype === 3) {
@@ -15961,6 +16182,91 @@ var NVMesh2 = class _NVMesh {
15961
16182
  }
15962
16183
  this.updateMesh(gl);
15963
16184
  }
16185
+ hierarchicalOrder() {
16186
+ const V0 = 12;
16187
+ const F0 = 20;
16188
+ const nF = this.tris.length / 3;
16189
+ const order = Math.log(nF / F0) / Math.log(4);
16190
+ if (nF !== Math.pow(4, order) * F0) {
16191
+ return NaN;
16192
+ }
16193
+ const nV = this.pts.length / 3;
16194
+ if (nV !== Math.pow(4, order) * (V0 - 2) + 2) {
16195
+ return NaN;
16196
+ }
16197
+ for (let i = 0; i < 15; i += 3) {
16198
+ if (this.tris[i] !== 0) {
16199
+ return NaN;
16200
+ }
16201
+ }
16202
+ for (let i = 15; i < 24; i += 3) {
16203
+ if (this.tris[i] !== 3) {
16204
+ return NaN;
16205
+ }
16206
+ }
16207
+ for (let i = 24; i < 30; i += 3) {
16208
+ if (this.tris[i] !== 4) {
16209
+ return NaN;
16210
+ }
16211
+ }
16212
+ return order;
16213
+ }
16214
+ decimateFaces(n, ntarget) {
16215
+ let fac = this.tris;
16216
+ const V0 = 12;
16217
+ const F0 = 20;
16218
+ for (let j = n - 1; j >= ntarget; j--) {
16219
+ const nVjprev = Math.pow(4, j + 1) * (V0 - 2) + 2;
16220
+ const nVj = Math.pow(4, j) * (V0 - 2) + 2;
16221
+ const nFjprev = fac.length / 3;
16222
+ const nFj = Math.pow(4, j) * F0;
16223
+ console.log(`order ${j + 1} -> ${j} vertices ${nVjprev} -> ${nVj} faces ${nFjprev} -> ${nFj}`);
16224
+ const remap = Array.from({ length: nVjprev }, (_, i) => i + 1);
16225
+ for (let i = 0; i < nFjprev; i++) {
16226
+ const v1 = fac[3 * i];
16227
+ const v2 = fac[3 * i + 1];
16228
+ const v3 = fac[3 * i + 2];
16229
+ remap[v1 - 1] = Math.min(remap[v1 - 1], v2, v3);
16230
+ }
16231
+ const facJ = new Uint32Array(nFj * 3);
16232
+ for (let i = 0; i < nFj; i++) {
16233
+ facJ[3 * i] = remap[fac[3 * i] - 1];
16234
+ facJ[3 * i + 1] = remap[fac[3 * i + 1] - 1];
16235
+ facJ[3 * i + 2] = remap[fac[3 * i + 2] - 1];
16236
+ }
16237
+ fac = facJ;
16238
+ }
16239
+ this.tris = new Uint32Array(fac);
16240
+ }
16241
+ // internal function simplifies FreeSurfer triangulated mesh and overlays
16242
+ decimateHierarchicalMesh(gl, order = 4) {
16243
+ const inputOrder = this.hierarchicalOrder();
16244
+ if (isNaN(inputOrder)) {
16245
+ log.warn("Unable to decimate mesh: it does not have a hierarchical structure");
16246
+ return false;
16247
+ }
16248
+ if (order >= inputOrder) {
16249
+ log.warn(`Unable to decimate mesh: input order (${inputOrder}) must be larger than downsampled order (${order})`);
16250
+ return false;
16251
+ }
16252
+ const inputVLength = this.pts.length / 3;
16253
+ const V0 = 12;
16254
+ const nV = Math.pow(4, order) * (V0 - 2) + 2;
16255
+ this.pts = new Float32Array(this.pts.slice(0, nV * 3));
16256
+ this.decimateFaces(inputOrder, order);
16257
+ if (this.layers && this.layers.length > 0) {
16258
+ for (let i = 0; i < this.layers.length; i++) {
16259
+ const layer = this.layers[i];
16260
+ if (layer.values instanceof Float32Array || layer.values.length !== inputVLength) {
16261
+ layer.values = new Float32Array(layer.values.slice(0, nV));
16262
+ } else {
16263
+ log.warn(`decimation logic needs to be updated`);
16264
+ }
16265
+ }
16266
+ }
16267
+ this.updateMesh(gl);
16268
+ return true;
16269
+ }
15964
16270
  // adjust attributes of a mesh layer. invoked by niivue.setMeshLayerProperty()
15965
16271
  // TODO this method is a bit too generic
15966
16272
  setLayerProperty(id, key, val, gl) {
@@ -18016,7 +18322,7 @@ import { vec3 as vec38, vec4 as vec46 } from "gl-matrix";
18016
18322
  import * as nifti from "nifti-reader-js";
18017
18323
  import daikon from "daikon";
18018
18324
  import { mat3, mat4 as mat44, vec3 as vec37, vec4 as vec45 } from "gl-matrix";
18019
- import { Decompress, decompressSync as decompressSync3, gzipSync } from "fflate/browser";
18325
+ import { Decompress, decompressSync as decompressSync3, gzipSync as gzipSync2 } from "fflate/browser";
18020
18326
  import { v4 as uuidv42 } from "@lukeed/uuid";
18021
18327
 
18022
18328
  // src/nvimage/utils.ts
@@ -18159,76 +18465,90 @@ function getBestTransform(imageDirections, voxelDimensions, imagePosition) {
18159
18465
  }
18160
18466
  return m;
18161
18467
  }
18162
- function str2Buffer(str) {
18468
+ function str2Buffer(str, maxLen = 80) {
18469
+ str = str.replace(/[`$]/g, "");
18163
18470
  const bytes = [];
18164
- for (let i = 0; i < str.length; i++) {
18471
+ const len = Math.min(maxLen, str.length);
18472
+ for (let i = 0; i < len; i++) {
18165
18473
  const char = str.charCodeAt(i);
18166
18474
  bytes.push(char & 255);
18167
18475
  }
18168
18476
  return bytes;
18169
18477
  }
18170
- function hdrToArrayBuffer(hdr, isDrawing8 = false) {
18478
+ function hdrToArrayBuffer(hdr, isDrawing8 = false, isInputEndian = false) {
18171
18479
  const SHORT_SIZE = 2;
18172
18480
  const FLOAT32_SIZE = 4;
18481
+ let isLittleEndian = true;
18482
+ if (isInputEndian) {
18483
+ isLittleEndian = hdr.littleEndian;
18484
+ }
18173
18485
  const byteArray = new Uint8Array(348);
18174
18486
  const view = new DataView(byteArray.buffer);
18175
- view.setInt32(0, 348, hdr.littleEndian);
18487
+ view.setInt32(0, 348, isLittleEndian);
18488
+ view.setUint8(38, 114);
18176
18489
  view.setUint8(39, hdr.dim_info);
18177
18490
  for (let i = 0; i < 8; i++) {
18178
- view.setUint16(40 + SHORT_SIZE * i, hdr.dims[i], hdr.littleEndian);
18491
+ view.setUint16(40 + SHORT_SIZE * i, hdr.dims[i], isLittleEndian);
18179
18492
  }
18180
- view.setFloat32(56, hdr.intent_p1, hdr.littleEndian);
18181
- view.setFloat32(60, hdr.intent_p2, hdr.littleEndian);
18182
- view.setFloat32(64, hdr.intent_p3, hdr.littleEndian);
18183
- 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);
18184
18497
  if (isDrawing8) {
18185
- view.setInt16(70, 2, hdr.littleEndian);
18186
- view.setInt16(72, 8, hdr.littleEndian);
18498
+ view.setInt16(70, 2, isLittleEndian);
18499
+ view.setInt16(72, 8, isLittleEndian);
18187
18500
  } else {
18188
- view.setInt16(70, hdr.datatypeCode, hdr.littleEndian);
18189
- view.setInt16(72, hdr.numBitsPerVoxel, hdr.littleEndian);
18501
+ view.setInt16(70, hdr.datatypeCode, isLittleEndian);
18502
+ view.setInt16(72, hdr.numBitsPerVoxel, isLittleEndian);
18190
18503
  }
18191
- view.setInt16(74, hdr.slice_start, hdr.littleEndian);
18504
+ view.setInt16(74, hdr.slice_start, isLittleEndian);
18192
18505
  for (let i = 0; i < 8; i++) {
18193
- view.setFloat32(76 + FLOAT32_SIZE * i, hdr.pixDims[i], hdr.littleEndian);
18506
+ view.setFloat32(76 + FLOAT32_SIZE * i, hdr.pixDims[i], isLittleEndian);
18194
18507
  }
18195
18508
  if (isDrawing8) {
18196
- view.setFloat32(108, 352, hdr.littleEndian);
18197
- view.setFloat32(112, 1, hdr.littleEndian);
18198
- view.setFloat32(116, 0, hdr.littleEndian);
18509
+ view.setFloat32(108, 352, isLittleEndian);
18510
+ view.setFloat32(112, 1, isLittleEndian);
18511
+ view.setFloat32(116, 0, isLittleEndian);
18199
18512
  } else {
18200
- view.setFloat32(108, hdr.vox_offset, hdr.littleEndian);
18201
- view.setFloat32(112, hdr.scl_slope, hdr.littleEndian);
18202
- 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);
18203
18516
  }
18204
- view.setInt16(120, hdr.slice_end, hdr.littleEndian);
18517
+ view.setInt16(120, hdr.slice_end, isLittleEndian);
18205
18518
  view.setUint8(122, hdr.slice_code);
18206
- 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
+ }
18207
18524
  if (isDrawing8) {
18208
- view.setFloat32(124, 0, hdr.littleEndian);
18209
- view.setFloat32(128, 0, hdr.littleEndian);
18525
+ view.setFloat32(124, 0, isLittleEndian);
18526
+ view.setFloat32(128, 0, isLittleEndian);
18210
18527
  } else {
18211
- view.setFloat32(124, hdr.cal_max, hdr.littleEndian);
18212
- view.setFloat32(128, hdr.cal_min, hdr.littleEndian);
18528
+ view.setFloat32(124, hdr.cal_max, isLittleEndian);
18529
+ view.setFloat32(128, hdr.cal_min, isLittleEndian);
18213
18530
  }
18214
- view.setFloat32(132, hdr.slice_duration, hdr.littleEndian);
18215
- view.setFloat32(136, hdr.toffset, hdr.littleEndian);
18531
+ view.setFloat32(132, hdr.slice_duration, isLittleEndian);
18532
+ view.setFloat32(136, hdr.toffset, isLittleEndian);
18216
18533
  byteArray.set(str2Buffer(hdr.description), 148);
18217
18534
  byteArray.set(str2Buffer(hdr.aux_file), 228);
18218
- view.setInt16(252, hdr.qform_code, hdr.littleEndian);
18219
- view.setInt16(254, hdr.sform_code, hdr.littleEndian);
18220
- view.setFloat32(256, hdr.quatern_b, hdr.littleEndian);
18221
- view.setFloat32(260, hdr.quatern_c, hdr.littleEndian);
18222
- view.setFloat32(264, hdr.quatern_d, hdr.littleEndian);
18223
- view.setFloat32(268, hdr.qoffset_x, hdr.littleEndian);
18224
- view.setFloat32(272, hdr.qoffset_y, hdr.littleEndian);
18225
- 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);
18226
18547
  const flattened = hdr.affine.flat();
18227
18548
  for (let i = 0; i < 12; i++) {
18228
- view.setFloat32(280 + FLOAT32_SIZE * i, flattened[i], hdr.littleEndian);
18549
+ view.setFloat32(280 + FLOAT32_SIZE * i, flattened[i], isLittleEndian);
18229
18550
  }
18230
- byteArray.set(str2Buffer(hdr.intent_name), 328);
18231
- byteArray.set(str2Buffer(hdr.magic), 344);
18551
+ view.setInt32(344, 3222382, true);
18232
18552
  return byteArray;
18233
18553
  }
18234
18554
  function getExtents(positions, forceOriginInVolume = true) {
@@ -19772,7 +20092,7 @@ var NVImage = class _NVImage {
19772
20092
  hdr.datatypeCode = 64 /* DT_FLOAT64 */;
19773
20093
  break;
19774
20094
  default:
19775
- throw new Error("Unsupported NRRD data type: " + items[0]);
20095
+ throw new Error("Unsupported MHA data type: " + items[0]);
19776
20096
  }
19777
20097
  }
19778
20098
  if (line.startsWith("ObjectType") && !items[0].includes("Image")) {
@@ -19794,6 +20114,9 @@ var NVImage = class _NVImage {
19794
20114
  [mat33[2], mat33[5], mat33[8], offset[2]],
19795
20115
  [0, 0, 0, 1]
19796
20116
  ];
20117
+ while (bytes[pos] === 10) {
20118
+ pos++;
20119
+ }
19797
20120
  hdr.vox_offset = pos;
19798
20121
  if (isDetached && pairedImgData) {
19799
20122
  if (isGz) {
@@ -20875,7 +21198,7 @@ var NVImage = class _NVImage {
20875
21198
  let saveData = null;
20876
21199
  const compress2 = fnm.endsWith(".gz");
20877
21200
  if (compress2) {
20878
- saveData = gzipSync(odata, {
21201
+ saveData = gzipSync2(odata, {
20879
21202
  // GZIP-specific: the filename to use when decompressed
20880
21203
  filename: fnm,
20881
21204
  // GZIP-specific: the modification time. Can be a Date, date string,
@@ -21338,7 +21661,7 @@ var NVImage = class _NVImage {
21338
21661
  * read a 3D slab of voxels from a volume
21339
21662
  * @param voxStart - first row, column and slice (RAS order) for selection
21340
21663
  * @param voxEnd - final row, column and slice (RAS order) for selection
21341
- * @param dataType - array data type. Options: 'same' (default), 'uint8', 'float32'
21664
+ * @param dataType - array data type. Options: 'same' (default), 'uint8', 'float32', 'scaled', 'normalized', 'windowed'
21342
21665
  * @returns the an array where ret[0] is the voxel values and ret[1] is dimension of selection
21343
21666
  * @see {@link https://niivue.github.io/niivue/features/slab_selection.html | live demo usage}
21344
21667
  */
@@ -21362,7 +21685,7 @@ var NVImage = class _NVImage {
21362
21685
  let dt = this.hdr.datatypeCode;
21363
21686
  if (dataType === "uint8") {
21364
21687
  dt = 2 /* DT_UINT8 */;
21365
- } else if (dataType === "float32") {
21688
+ } else if (dataType === "float32" || dataType === "scaled" || dataType === "normalized" || dataType === "windowed") {
21366
21689
  dt = 16 /* DT_FLOAT32 */;
21367
21690
  }
21368
21691
  if (dt === 2 /* DT_UINT8 */) {
@@ -21392,6 +21715,24 @@ var NVImage = class _NVImage {
21392
21715
  }
21393
21716
  }
21394
21717
  }
21718
+ if (dataType === "scaled" || dataType === "normalized" || dataType === "windowed") {
21719
+ for (let i2 = 0; i2 < img.length; i2++) {
21720
+ img[i2] = img[i2] * this.hdr.scl_slope + this.hdr.scl_inter;
21721
+ }
21722
+ }
21723
+ if (dataType === "normalized" || dataType === "windowed") {
21724
+ let mn = this.cal_min;
21725
+ let mx = this.cal_max;
21726
+ if (dataType === "normalized") {
21727
+ mn = this.global_min;
21728
+ mx = this.global_max;
21729
+ }
21730
+ const scale = 1 / (mx - mn);
21731
+ for (let i2 = 0; i2 < img.length; i2++) {
21732
+ img[i2] = (img[i2] - mn) * scale;
21733
+ img[i2] = Math.max(Math.min(img[i2], 1), 0);
21734
+ }
21735
+ }
21395
21736
  return [img, slabDims];
21396
21737
  }
21397
21738
  // getVolumeData()
@@ -21949,6 +22290,7 @@ var DRAG_MODE = /* @__PURE__ */ ((DRAG_MODE2) => {
21949
22290
  DRAG_MODE2[DRAG_MODE2["pan"] = 3] = "pan";
21950
22291
  DRAG_MODE2[DRAG_MODE2["slicer3D"] = 4] = "slicer3D";
21951
22292
  DRAG_MODE2[DRAG_MODE2["callbackOnly"] = 5] = "callbackOnly";
22293
+ DRAG_MODE2[DRAG_MODE2["roiSelection"] = 6] = "roiSelection";
21952
22294
  return DRAG_MODE2;
21953
22295
  })(DRAG_MODE || {});
21954
22296
  var DEFAULT_OPTIONS = {
@@ -21983,6 +22325,7 @@ var DEFAULT_OPTIONS = {
21983
22325
  multiplanarPadPixels: 0,
21984
22326
  // @deprecated
21985
22327
  multiplanarForceRender: false,
22328
+ multiplanarEqualSize: false,
21986
22329
  multiplanarShowRender: 2 /* AUTO */,
21987
22330
  // auto is the same behaviour as multiplanarForceRender: false
21988
22331
  isRadiologicalConvention: false,
@@ -22017,12 +22360,30 @@ var DEFAULT_OPTIONS = {
22017
22360
  renderOverlayBlend: 1,
22018
22361
  sliceMosaicString: "",
22019
22362
  centerMosaic: false,
22363
+ penSize: 1,
22364
+ // in voxels, since all drawing is done using bitmap indices
22020
22365
  clickToSegment: false,
22021
- clickToSegmentRadius: 2,
22022
- clickToSegmentSteps: 10,
22023
- 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
22024
22384
  };
22025
22385
  var INITIAL_SCENE_DATA = {
22386
+ gamma: 1,
22026
22387
  azimuth: 110,
22027
22388
  elevation: 10,
22028
22389
  crosshairPos: vec38.create(),
@@ -22261,6 +22622,9 @@ var NVDocument = class _NVDocument {
22261
22622
  data.opts.meshThicknessOn2D = "infinity";
22262
22623
  }
22263
22624
  data.labels = [...this.data.labels];
22625
+ for (const label of data.labels) {
22626
+ delete label.onClick;
22627
+ }
22264
22628
  data.customData = this.customData;
22265
22629
  if (this.volumes.length) {
22266
22630
  let imageOptions = this.imageOptionsArray[0];
@@ -22516,6 +22880,19 @@ var LabelLineTerminator = /* @__PURE__ */ ((LabelLineTerminator2) => {
22516
22880
  LabelLineTerminator2["RING"] = "ring";
22517
22881
  return LabelLineTerminator2;
22518
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 || {});
22519
22896
  var NVLabel3DStyle = class {
22520
22897
  /**
22521
22898
  * @param textColor - Color of text
@@ -22553,13 +22930,17 @@ var NVLabel3D = class {
22553
22930
  * @param style - The style of the label
22554
22931
  * @param points - An array of points label for label lines
22555
22932
  */
22556
- constructor(text, style, points) {
22933
+ constructor(text, style, points, anchor, onClick) {
22557
22934
  __publicField(this, "text");
22558
22935
  __publicField(this, "style");
22559
22936
  __publicField(this, "points");
22937
+ __publicField(this, "anchor");
22938
+ __publicField(this, "onClick");
22560
22939
  this.text = text;
22561
22940
  this.style = style;
22562
22941
  this.points = points;
22942
+ this.anchor = anchor || 0 /* NONE */;
22943
+ this.onClick = onClick;
22563
22944
  }
22564
22945
  };
22565
22946
 
@@ -23696,7 +24077,7 @@ var defaultSaveImageOptions = {
23696
24077
  isSaveDrawing: false,
23697
24078
  volumeByIndex: 0
23698
24079
  };
23699
- var Niivue = class _Niivue {
24080
+ var Niivue = class {
23700
24081
  /**
23701
24082
  * @param options - options object to set modifiable Niivue properties
23702
24083
  */
@@ -23743,6 +24124,7 @@ var Niivue = class _Niivue {
23743
24124
  __publicField(this, "orientCubeShader");
23744
24125
  __publicField(this, "orientCubeShaderVAO", null);
23745
24126
  __publicField(this, "rectShader");
24127
+ __publicField(this, "rectOutlineShader");
23746
24128
  __publicField(this, "renderShader");
23747
24129
  __publicField(this, "lineShader");
23748
24130
  __publicField(this, "line3DShader");
@@ -23795,7 +24177,20 @@ var Niivue = class _Niivue {
23795
24177
  __publicField(this, "extentsMax");
23796
24178
  // ResizeObserver
23797
24179
  __publicField(this, "resizeObserver", null);
23798
- __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
+ });
23799
24194
  __publicField(this, "readyForSync", false);
23800
24195
  // UI Data
23801
24196
  __publicField(this, "uiData", {
@@ -23873,6 +24268,10 @@ var Niivue = class _Niivue {
23873
24268
  Name: "Hemispheric",
23874
24269
  Frag: fragMeshHemiShader
23875
24270
  },
24271
+ {
24272
+ Name: "Crevice",
24273
+ Frag: fragMeshShaderCrevice
24274
+ },
23876
24275
  {
23877
24276
  Name: "Edge",
23878
24277
  Frag: fragMeshEdgeShader
@@ -24270,6 +24669,9 @@ var Niivue = class _Niivue {
24270
24669
  * @see {@link https://niivue.github.io/niivue/features/sync.mesh.html | live demo usage}
24271
24670
  */
24272
24671
  syncWith(otherNV, syncOpts = { "2d": true, "3d": true }) {
24672
+ if (!(otherNV instanceof Array)) {
24673
+ otherNV = [otherNV];
24674
+ }
24273
24675
  this.otherNV = otherNV;
24274
24676
  this.syncOpts = { ...syncOpts };
24275
24677
  }
@@ -24285,9 +24687,55 @@ var Niivue = class _Niivue {
24285
24687
  * @see {@link https://niivue.github.io/niivue/features/sync.mesh.html | live demo usage}
24286
24688
  */
24287
24689
  broadcastTo(otherNV, syncOpts = { "2d": true, "3d": true }) {
24690
+ if (!(otherNV instanceof Array)) {
24691
+ otherNV = [otherNV];
24692
+ }
24288
24693
  this.otherNV = otherNV;
24289
24694
  this.syncOpts = syncOpts;
24290
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
+ }
24291
24739
  /**
24292
24740
  * Sync the scene controls (orientation, crosshair location, etc.) from one Niivue instance to another. useful for using one canvas to drive another.
24293
24741
  * @internal
@@ -24304,36 +24752,39 @@ var Niivue = class _Niivue {
24304
24752
  if (!this.gl.canvas.matches(":focus")) {
24305
24753
  return;
24306
24754
  }
24307
- const thisMM = this.frac2mm(this.scene.crosshairPos);
24308
- 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
+ }
24309
24780
  if (this.syncOpts["2d"]) {
24310
- this.otherNV.scene.crosshairPos = this.otherNV.mm2frac(thisMM);
24311
- this.otherNV.scene.pan2Dxyzmm = vec48.clone(this.scene.pan2Dxyzmm);
24781
+ this.doSync2d(this.otherNV[i]);
24312
24782
  }
24313
24783
  if (this.syncOpts["3d"]) {
24314
- this.otherNV.scene.renderAzimuth = this.scene.renderAzimuth;
24315
- this.otherNV.scene.renderElevation = this.scene.renderElevation;
24316
- this.otherNV.scene.volScaleMultiplier = this.scene.volScaleMultiplier;
24317
- }
24318
- this.otherNV.drawScene();
24319
- this.otherNV.createOnLocationChange();
24320
- } else if (Array.isArray(this.otherNV)) {
24321
- for (let i = 0; i < this.otherNV.length; i++) {
24322
- if (this.otherNV[i] === this) {
24323
- continue;
24324
- }
24325
- if (this.syncOpts["2d"]) {
24326
- this.otherNV[i].scene.crosshairPos = this.otherNV[i].mm2frac(thisMM);
24327
- this.otherNV[i].scene.pan2Dxyzmm = vec48.clone(this.scene.pan2Dxyzmm);
24328
- }
24329
- if (this.syncOpts["3d"]) {
24330
- this.otherNV[i].scene.renderAzimuth = this.scene.renderAzimuth;
24331
- this.otherNV[i].scene.renderElevation = this.scene.renderElevation;
24332
- this.otherNV[i].scene.volScaleMultiplier = this.scene.volScaleMultiplier;
24333
- }
24334
- this.otherNV[i].drawScene();
24335
- this.otherNV[i].createOnLocationChange();
24784
+ this.doSync3d(this.otherNV[i]);
24336
24785
  }
24786
+ this.otherNV[i].drawScene();
24787
+ this.otherNV[i].createOnLocationChange();
24337
24788
  }
24338
24789
  }
24339
24790
  /** Not documented publicly for now
@@ -24437,6 +24888,10 @@ var Niivue = class _Niivue {
24437
24888
  const [x, y] = [pos.x * this.uiData.dpr, pos.y * this.uiData.dpr];
24438
24889
  const label = this.getLabelAtPoint([x, y]);
24439
24890
  if (label) {
24891
+ if (label.onClick) {
24892
+ label.onClick(label);
24893
+ return;
24894
+ }
24440
24895
  for (const mesh of this.meshes) {
24441
24896
  if (mesh.type !== "connectome" /* CONNECTOME */) {
24442
24897
  continue;
@@ -24646,6 +25101,9 @@ var Niivue = class _Niivue {
24646
25101
  const fracStart = this.canvasPos2frac([this.uiData.dragStart[0], this.uiData.dragStart[1]]);
24647
25102
  const fracEnd = this.canvasPos2frac([this.uiData.dragEnd[0], this.uiData.dragEnd[1]]);
24648
25103
  this.generateMouseUpCallback(fracStart, fracEnd);
25104
+ if (this.opts.dragMode === 6 /* roiSelection */) {
25105
+ return;
25106
+ }
24649
25107
  if (this.opts.dragMode !== 1 /* contrast */) {
24650
25108
  return;
24651
25109
  }
@@ -24751,6 +25209,15 @@ var Niivue = class _Niivue {
24751
25209
  this.drawScene();
24752
25210
  this.uiData.prevX = this.uiData.currX;
24753
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();
24754
25221
  }
24755
25222
  }
24756
25223
  // not included in public docs
@@ -25275,7 +25742,6 @@ var Niivue = class _Niivue {
25275
25742
  this.loadDrawing(volume);
25276
25743
  } else {
25277
25744
  this.addVolume(volume);
25278
- this.setDrawingEnabled(true);
25279
25745
  }
25280
25746
  }).catch((e2) => {
25281
25747
  throw e2;
@@ -25966,6 +26432,24 @@ var Niivue = class _Niivue {
25966
26432
  }
25967
26433
  return this.meshes[idx].indexNearestXYZmm(Xmm, Ymm, Zmm);
25968
26434
  }
26435
+ /**
26436
+ * reduce complexity of FreeSurfer mesh
26437
+ * @param mesh - identity of mesh to change
26438
+ * @param order - decimation order 0..6
26439
+ * @example niivue.decimateHierarchicalMesh(niivue.meshes[0].id, 4)
26440
+ * @returns boolean false if mesh is not hierarchical or of lower order
26441
+ * @see {@link https://niivue.github.io/niivue/features/meshes.html | live demo usage}
26442
+ */
26443
+ decimateHierarchicalMesh(mesh, order = 3) {
26444
+ const idx = this.getMeshIndexByID(mesh);
26445
+ if (idx < 0) {
26446
+ log.warn("reverseFaces() id not loaded", mesh);
26447
+ return;
26448
+ }
26449
+ const ret = this.meshes[idx].decimateHierarchicalMesh(this.gl, order);
26450
+ this.updateGLVolume();
26451
+ return ret;
26452
+ }
25969
26453
  /**
25970
26454
  * reverse triangle winding of mesh (swap front and back faces)
25971
26455
  * @param id - identity of mesh to change
@@ -26464,6 +26948,9 @@ var Niivue = class _Niivue {
26464
26948
  this.renderShader.use(this.gl);
26465
26949
  this.gl.uniform3fv(this.renderShader.uniforms.clipLo, this.opts.clipVolumeLow);
26466
26950
  this.gl.uniform3fv(this.renderShader.uniforms.clipHi, this.opts.clipVolumeHigh);
26951
+ this.pickingImageShader.use(this.gl);
26952
+ this.gl.uniform3fv(this.pickingImageShader.uniforms.clipLo, this.opts.clipVolumeLow);
26953
+ this.gl.uniform3fv(this.pickingImageShader.uniforms.clipHi, this.opts.clipVolumeHigh);
26467
26954
  this.drawScene();
26468
26955
  }
26469
26956
  /**
@@ -27188,6 +27675,23 @@ var Niivue = class _Niivue {
27188
27675
  y = Math.min(Math.max(y, 0), dy - 1);
27189
27676
  z = Math.min(Math.max(z, 0), dz - 1);
27190
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
+ }
27191
27695
  }
27192
27696
  // not included in public docs
27193
27697
  // create line between to voxels in drawing
@@ -27269,6 +27773,98 @@ var Niivue = class _Niivue {
27269
27773
  }
27270
27774
  }
27271
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
+ }
27272
27868
  // a voxel can be defined as having 6, 18 or 26 neighbors:
27273
27869
  // 6: neighbors share faces (distance=1)
27274
27870
  // 18: neighbors share faces (distance=1) or edges (1.4)
@@ -27350,7 +27946,7 @@ var Niivue = class _Niivue {
27350
27946
  }
27351
27947
  // not included in public docs
27352
27948
  // set all connected voxels in drawing to new color
27353
- 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) {
27354
27950
  if (!this.drawBitmap) {
27355
27951
  throw new Error("drawBitmap undefined");
27356
27952
  }
@@ -27372,9 +27968,25 @@ var Niivue = class _Niivue {
27372
27968
  if (img.length !== nxy * dims[2]) {
27373
27969
  return;
27374
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
+ }
27375
27977
  function xyz2vx(pt) {
27376
27978
  return pt[0] + pt[1] * nx + pt[2] * nxy;
27377
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
+ }
27378
27990
  const seedVx = xyz2vx(seedXYZ);
27379
27991
  const seedColor = img[seedVx];
27380
27992
  if (seedColor === newColor) {
@@ -27388,6 +28000,9 @@ var Niivue = class _Niivue {
27388
28000
  for (let i = 1; i < nxyz; i++) {
27389
28001
  img[i] = 0;
27390
28002
  if (this.drawBitmap[i] === seedColor) {
28003
+ if (!isWithinDistance(i)) {
28004
+ continue;
28005
+ }
27391
28006
  img[i] = 1;
27392
28007
  }
27393
28008
  }
@@ -27417,6 +28032,9 @@ var Niivue = class _Niivue {
27417
28032
  for (let i = 1; i < nxyz; i++) {
27418
28033
  img[i] = 0;
27419
28034
  if (backImg[i] >= mn && backImg[i] <= mx) {
28035
+ if (!isWithinDistance(i)) {
28036
+ continue;
28037
+ }
27420
28038
  img[i] = 1;
27421
28039
  }
27422
28040
  }
@@ -28038,6 +28656,8 @@ var Niivue = class _Niivue {
28038
28656
  gl.bindVertexArray(this.unusedVAO);
28039
28657
  this.rectShader = new Shader(gl, vertRectShader, fragRectShader);
28040
28658
  this.rectShader.use(gl);
28659
+ this.rectOutlineShader = new Shader(gl, vertRectShader, fragRectOutlineShader);
28660
+ this.rectOutlineShader.use(gl);
28041
28661
  this.lineShader = new Shader(gl, vertLineShader, fragRectShader);
28042
28662
  this.lineShader.use(gl);
28043
28663
  this.line3DShader = new Shader(gl, vertLine3DShader, fragRectShader);
@@ -28190,14 +28810,37 @@ var Niivue = class _Niivue {
28190
28810
  }
28191
28811
  /**
28192
28812
  * basic statistics for selected voxel-based image
28193
- * @param layer - selects image to describe
28194
- * @param masks - are optional binary images to filter voxles
28195
- * @returns numeric values to describe image
28196
- * @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
+ * });
28197
28830
  * @see {@link https://niivue.github.io/niivue/features/draw2.html | live demo usage}
28198
28831
  */
28199
- 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;
28200
28842
  const hdr = this.volumes[layer].hdr;
28843
+ const pixDimsRAS = this.volumes[layer].pixDimsRAS;
28201
28844
  let slope = hdr.scl_slope;
28202
28845
  if (isNaN(slope)) {
28203
28846
  slope = 1;
@@ -28235,6 +28878,37 @@ var Niivue = class _Niivue {
28235
28878
  mask[i] = 0;
28236
28879
  }
28237
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);
28238
28912
  }
28239
28913
  let k = 0;
28240
28914
  let M = 0;
@@ -28288,7 +28962,8 @@ var Niivue = class _Niivue {
28288
28962
  cal_min: this.volumes[layer].cal_min,
28289
28963
  cal_max: this.volumes[layer].cal_max,
28290
28964
  robust_min: this.volumes[layer].robust_min,
28291
- robust_max: this.volumes[layer].robust_max
28965
+ robust_max: this.volumes[layer].robust_max,
28966
+ area
28292
28967
  };
28293
28968
  }
28294
28969
  // not included in public docs
@@ -28765,6 +29440,8 @@ var Niivue = class _Niivue {
28765
29440
  this.pickingImageShader.use(this.gl);
28766
29441
  this.gl.uniform1f(this.pickingImageShader.uniforms.overlays, this.overlays.length);
28767
29442
  this.gl.uniform3fv(this.pickingImageShader.uniforms.texVox, vox);
29443
+ this.gl.uniform3fv(this.pickingImageShader.uniforms.clipLo, this.opts.clipVolumeLow);
29444
+ this.gl.uniform3fv(this.pickingImageShader.uniforms.clipHi, this.opts.clipVolumeHigh);
28768
29445
  let shader = this.sliceMMShader;
28769
29446
  if (this.opts.isV1SliceShader) {
28770
29447
  shader = this.sliceV1Shader;
@@ -29477,6 +30154,7 @@ var Niivue = class _Niivue {
29477
30154
  * @see {@link https://niivue.github.io/niivue/features/colormaps.html | live demo usage}
29478
30155
  */
29479
30156
  setGamma(gamma = 1) {
30157
+ this.scene.gamma = gamma;
29480
30158
  cmapper.gamma = gamma;
29481
30159
  this.updateGLVolume();
29482
30160
  }
@@ -29841,27 +30519,50 @@ var Niivue = class _Niivue {
29841
30519
  if (this.opts.drawingEnabled) {
29842
30520
  const pt = this.frac2vox(this.scene.crosshairPos);
29843
30521
  if (this.opts.clickToSegment) {
29844
- const radius = this.opts.clickToSegmentRadius;
29845
- 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
+ }
29846
30534
  const brightOrDark = this.opts.clickToSegmentBright ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
29847
- this.drawPenFillPts = [];
29848
30535
  this.drawPenAxCorSag = axCorSag;
29849
- for (let i2 = 1; i2 <= steps; i2++) {
29850
- const angle = i2 / steps * 2 * Math.PI;
29851
- let xVox = pt[0] + radius * Math.cos(angle);
29852
- let yVox = pt[1] + radius * Math.sin(angle);
29853
- const zVox = pt[2];
29854
- xVox = Math.round(xVox);
29855
- yVox = Math.round(yVox);
29856
- this.drawPt(xVox, yVox, zVox, this.opts.penValue);
29857
- this.drawPenFillPts.push([xVox, yVox, pt[2]]);
29858
- if (i2 === steps) {
29859
- this.drawFloodFill([xVox, yVox, pt[2]], 0, brightOrDark, NaN, NaN, this.opts.floodFillNeighbors);
29860
- }
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);
29861
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
+ );
29862
30559
  this.drawScene();
29863
30560
  this.createOnLocationChange(axCorSag);
29864
- const info = this.getDescriptives(0, [], true);
30561
+ const info = this.getDescriptives({
30562
+ layer: 0,
30563
+ masks: [],
30564
+ drawingIsMask: true
30565
+ });
29865
30566
  this.onClickToSegment({ mL: info.volumeML, mm3: info.volumeMM3 });
29866
30567
  return;
29867
30568
  }
@@ -30079,20 +30780,41 @@ var Niivue = class _Niivue {
30079
30780
  if (!this.rectShader) {
30080
30781
  throw new Error("rectShader undefined");
30081
30782
  }
30082
- this.rectShader.use(this.gl);
30083
- this.gl.enable(this.gl.BLEND);
30084
- this.gl.uniform4fv(this.rectShader.uniforms.lineColor, lineColor);
30085
- this.gl.uniform2fv(this.rectShader.uniforms.canvasWidthHeight, [this.gl.canvas.width, this.gl.canvas.height]);
30086
- this.gl.uniform4f(
30087
- this.rectShader.uniforms.leftTopWidthHeight,
30088
- leftTopWidthHeight[0],
30089
- leftTopWidthHeight[1],
30090
- leftTopWidthHeight[2],
30091
- leftTopWidthHeight[3]
30092
- );
30093
- this.gl.bindVertexArray(this.genericVAO);
30094
- this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
30095
- 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
+ }
30096
30818
  }
30097
30819
  drawCircle(leftTopWidthHeight, circleColor = this.opts.fontColor, fillPercent = 1) {
30098
30820
  if (!this.circleShader) {
@@ -30136,9 +30858,21 @@ var Niivue = class _Niivue {
30136
30858
  const labels = [...this.document.labels, ...definedMeshLabels];
30137
30859
  return labels;
30138
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
+ }
30139
30873
  getBulletMarginWidth() {
30140
30874
  let bulletMargin = 0;
30141
- const labels = this.getAllLabels();
30875
+ const labels = this.getConnectomeLabels();
30142
30876
  if (labels.length === 0) {
30143
30877
  return 0;
30144
30878
  }
@@ -30155,7 +30889,7 @@ var Niivue = class _Niivue {
30155
30889
  return bulletMargin;
30156
30890
  }
30157
30891
  getLegendPanelWidth() {
30158
- const labels = this.getAllLabels();
30892
+ const labels = this.getConnectomeLabels();
30159
30893
  if (!this.opts.showLegend || labels.length === 0) {
30160
30894
  return 0;
30161
30895
  }
@@ -30181,7 +30915,7 @@ var Niivue = class _Niivue {
30181
30915
  return width;
30182
30916
  }
30183
30917
  getLegendPanelHeight() {
30184
- const labels = this.getAllLabels();
30918
+ const labels = this.getConnectomeLabels();
30185
30919
  let height = 0;
30186
30920
  const scale = 1;
30187
30921
  const verticalMargin = this.opts.textHeight * this.gl.canvas.height * scale;
@@ -30623,7 +31357,7 @@ var Niivue = class _Niivue {
30623
31357
  }
30624
31358
  // not included in public docs
30625
31359
  // show text labels for L/R, A/P, I/S dimensions
30626
- drawSliceOrientationText(leftTopWidthHeight, axCorSag) {
31360
+ drawSliceOrientationText(leftTopWidthHeight, axCorSag, padLeftTop = [NaN, NaN]) {
30627
31361
  this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
30628
31362
  let topText = "S";
30629
31363
  if (axCorSag === 0 /* AXIAL */) {
@@ -30637,8 +31371,32 @@ var Niivue = class _Niivue {
30637
31371
  this.drawTextRightBelow([leftTopWidthHeight[0], leftTopWidthHeight[1]], leftText + topText);
30638
31372
  return;
30639
31373
  }
30640
- this.drawTextBelow([leftTopWidthHeight[0] + leftTopWidthHeight[2] * 0.5, leftTopWidthHeight[1]], topText);
30641
- 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
+ }
30642
31400
  }
30643
31401
  // not included in public docs
30644
31402
  xyMM2xyzMM(axCorSag, sliceFrac) {
@@ -30676,7 +31434,7 @@ var Niivue = class _Niivue {
30676
31434
  }
30677
31435
  // not included in public docs
30678
31436
  // draw 2D tile
30679
- draw2D(leftTopWidthHeight, axCorSag, customMM = NaN) {
31437
+ draw2DMain(leftTopWidthHeight, axCorSag, customMM = NaN) {
30680
31438
  let frac2mmTexture = this.volumes[0].frac2mm.slice();
30681
31439
  let screen = this.screenFieldOfViewExtendedMM(axCorSag);
30682
31440
  let mesh2ortho = mat46.create();
@@ -30705,7 +31463,9 @@ var Niivue = class _Niivue {
30705
31463
  elevation = isRadiolgical ? -90 : 90;
30706
31464
  }
30707
31465
  const gl = this.gl;
31466
+ let isStretchToScreen = false;
30708
31467
  if (leftTopWidthHeight[2] === 0 || leftTopWidthHeight[3] === 0) {
31468
+ isStretchToScreen = true;
30709
31469
  const pixPerMMw = gl.canvas.width / screen.fovMM[0];
30710
31470
  const pixPerMMh = gl.canvas.height / screen.fovMM[1];
30711
31471
  const pixPerMMmin = Math.min(pixPerMMw, pixPerMMh);
@@ -30852,9 +31612,32 @@ var Niivue = class _Niivue {
30852
31612
  if (isNaN(customMM)) {
30853
31613
  this.drawCrosshairs3D(false, 0.15, obj.modelViewProjectionMatrix, true, this.opts.isSliceMM);
30854
31614
  }
30855
- this.drawSliceOrientationText(leftTopWidthHeight, axCorSag);
31615
+ if (isStretchToScreen) {
31616
+ this.drawSliceOrientationText(leftTopWidthHeight, axCorSag);
31617
+ }
30856
31618
  this.readyForSync = true;
30857
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
+ }
30858
31641
  // not included in public docs
30859
31642
  // determine 3D model view projection matrix
30860
31643
  calculateMvpMatrix(_unused, leftTopWidthHeight = [0, 0, 0, 0], azimuth, elevation) {
@@ -31465,7 +32248,7 @@ var Niivue = class _Niivue {
31465
32248
  * @param style - label style
31466
32249
  * @param point - 3D point on the model
31467
32250
  */
31468
- addLabel(text, style, points) {
32251
+ addLabel(text, style, points, anchor, onClick) {
31469
32252
  const defaultStyle = {
31470
32253
  textColor: this.opts.legendTextColor,
31471
32254
  textScale: 1,
@@ -31477,7 +32260,7 @@ var Niivue = class _Niivue {
31477
32260
  bulletColor: this.opts.legendTextColor
31478
32261
  };
31479
32262
  const labelStyle = style ? { ...defaultStyle, ...style } : { ...defaultStyle };
31480
- const label = new NVLabel3D(text, labelStyle, points);
32263
+ const label = new NVLabel3D(text, { ...labelStyle }, points, anchor, onClick);
31481
32264
  this.document.labels.push(label);
31482
32265
  return label;
31483
32266
  }
@@ -31495,6 +32278,60 @@ var Niivue = class _Niivue {
31495
32278
  return screenPoint;
31496
32279
  }
31497
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
+ }
31498
32335
  log.debug("screenPoint", screenPoint);
31499
32336
  const panelHeight = this.getLegendPanelHeight();
31500
32337
  const panelWidth = this.getLegendPanelWidth();
@@ -31504,9 +32341,7 @@ var Niivue = class _Niivue {
31504
32341
  if (screenPoint[0] < left || screenPoint[1] < top || screenPoint[0] > left + panelWidth || screenPoint[1] > top + panelHeight) {
31505
32342
  return null;
31506
32343
  }
31507
- const scale = 1;
31508
- const size = this.opts.textHeight * Math.min(this.gl.canvas.height, this.gl.canvas.width) * scale;
31509
- const labels = this.getAllLabels();
32344
+ const labels = this.getConnectomeLabels();
31510
32345
  for (const label of labels) {
31511
32346
  const labelSize = this.opts.textHeight * this.gl.canvas.height * label.style.textScale;
31512
32347
  const textHeight = this.textHeight(labelSize, label.text);
@@ -31535,7 +32370,7 @@ var Niivue = class _Niivue {
31535
32370
  }
31536
32371
  }
31537
32372
  // not included in public docs
31538
- draw3DLabel(label, pos, mvpMatrix, leftTopWidthHeight, bulletMargin = 0, legendWidth, secondPass = false) {
32373
+ draw3DLabel(label, pos, mvpMatrix, leftTopWidthHeight, bulletMargin, legendWidth, secondPass) {
31539
32374
  const text = label.text;
31540
32375
  const left = pos[0];
31541
32376
  const top = pos[1];
@@ -31552,22 +32387,24 @@ var Niivue = class _Niivue {
31552
32387
  this.drawCircle([rectLeft, rectTop, bulletSize, bulletSize], label.style.bulletColor);
31553
32388
  }
31554
32389
  let textLeft = left;
31555
- if (label.style.textAlignment !== "left" /* LEFT */) {
31556
- const textWidth = this.textWidth(label.style.textScale, label.text) * size;
31557
- if (label.style.textAlignment === "right" /* RIGHT */) {
31558
- 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
+ }
31559
32399
  } else {
31560
- const remaining = legendWidth - (bulletMargin || size);
31561
- textLeft += (remaining - textWidth) / 2;
32400
+ textLeft += bulletMargin;
31562
32401
  }
31563
- } else {
31564
- textLeft += bulletMargin;
31565
32402
  }
31566
32403
  this.drawText([textLeft, top], text, label.style.textScale, label.style.textColor);
31567
32404
  }
31568
32405
  // not included in public docs
31569
32406
  draw3DLabels(mvpMatrix, leftTopWidthHeight, secondPass = false) {
31570
- const labels = this.getAllLabels();
32407
+ const labels = this.getConnectomeLabels();
31571
32408
  if (!this.opts.showLegend || labels.length === 0) {
31572
32409
  return;
31573
32410
  }
@@ -31605,6 +32442,51 @@ var Niivue = class _Niivue {
31605
32442
  }
31606
32443
  }
31607
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
+ }
31608
32490
  // not included in public docs
31609
32491
  draw3D(leftTopWidthHeight = [0, 0, 0, 0], mvpMatrix = null, modelMatrix = null, normalMatrix = null, azimuth = null, elevation = 0) {
31610
32492
  const isMosaic = azimuth !== null;
@@ -32480,6 +33362,12 @@ var Niivue = class _Niivue {
32480
33362
  }
32481
33363
  const isDrawPenDown = isFinite(this.drawPenLocation[0]) && this.opts.drawingEnabled;
32482
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
+ }
32483
33371
  if (typeof this.opts.multiplanarPadPixels !== "number") {
32484
33372
  log.debug("multiplanarPadPixels must be numeric");
32485
33373
  }
@@ -32517,38 +33405,43 @@ var Niivue = class _Niivue {
32517
33405
  isDrawGrid = true;
32518
33406
  }
32519
33407
  }
33408
+ let ltwh = ltwh2x2;
32520
33409
  if (isDrawColumn) {
32521
- let ltwh = ltwh1x3;
33410
+ ltwh = ltwh1x3;
32522
33411
  if (isShowRender || this.opts.multiplanarShowRender === 2 /* AUTO */ && ltwh1x4[4] >= ltwh1x3[4]) {
32523
33412
  ltwh = ltwh1x4;
32524
33413
  } else {
32525
33414
  isDraw3D = false;
32526
33415
  }
32527
- const sX = volScale[0] * ltwh[4];
32528
- const sY = volScale[1] * ltwh[4];
32529
- const sZ = volScale[2] * ltwh[4];
32530
- const sMx = mx * ltwh[4];
32531
- this.draw2D([ltwh[0], ltwh[1], sX, sY], 0);
32532
- this.draw2D([ltwh[0], ltwh[1] + sY + pad, sX, sZ], 1);
32533
- this.draw2D([ltwh[0], ltwh[1] + sY + pad + sZ + pad, sY, sZ], 2);
32534
- if (isDraw3D) {
32535
- this.draw3D([ltwh[0], ltwh[1] + sY + sZ + sZ + pad * 3, sMx, sMx]);
32536
- }
32537
33416
  } else if (isDrawRow) {
32538
- let ltwh = ltwh3x1;
33417
+ ltwh = ltwh3x1;
32539
33418
  if (isShowRender || this.opts.multiplanarShowRender === 2 /* AUTO */ && ltwh4x1[4] >= ltwh3x1[4]) {
32540
33419
  ltwh = ltwh4x1;
32541
33420
  } else {
32542
33421
  isDraw3D = false;
32543
33422
  }
32544
- const sX = volScale[0] * ltwh[4];
32545
- const sY = volScale[1] * ltwh[4];
32546
- const sZ = volScale[2] * ltwh[4];
32547
- this.draw2D([ltwh[0], ltwh[1], sX, sY], 0);
32548
- this.draw2D([ltwh[0] + sX + pad, ltwh[1], sX, sZ], 1);
32549
- 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]);
33434
+ if (isDraw3D) {
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]);
32550
33442
  if (isDraw3D) {
32551
- this.draw3D([ltwh[0] + sX + sX + sY + pad * 3, ltwh[1], ltwh[3], ltwh[3]]);
33443
+ const sMx = mx * ltwh[4];
33444
+ this.draw3D([ltwh[0] + sX + sX + sY + pad * 3, ltwh[1], sMx, sMx]);
32552
33445
  }
32553
33446
  } else if (isDrawGrid) {
32554
33447
  if (!isShowRender) {
@@ -32557,13 +33450,9 @@ var Niivue = class _Niivue {
32557
33450
  if (this.opts.multiplanarShowRender === 2 /* AUTO */) {
32558
33451
  isDraw3D = true;
32559
33452
  }
32560
- const ltwh = ltwh2x2;
32561
- const sX = volScale[0] * ltwh[4];
32562
- const sY = volScale[1] * ltwh[4];
32563
- const sZ = volScale[2] * ltwh[4];
32564
- this.draw2D([ltwh[0], ltwh[1] + sZ + pad, sX, sY], 0);
32565
- this.draw2D([ltwh[0], ltwh[1], sX, sZ], 1);
32566
- 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]);
32567
33456
  if (isDraw3D) {
32568
33457
  this.draw3D([ltwh[0] + sX + pad, ltwh[1] + sZ + pad, sY, sY]);
32569
33458
  }
@@ -32627,11 +33516,30 @@ var Niivue = class _Niivue {
32627
33516
  width,
32628
33517
  height
32629
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
+ }
32630
33537
  }
32631
33538
  const pos = this.frac2mm([this.scene.crosshairPos[0], this.scene.crosshairPos[1], this.scene.crosshairPos[2]]);
32632
33539
  posString = pos[0].toFixed(2) + "\xD7" + pos[1].toFixed(2) + "\xD7" + pos[2].toFixed(2);
32633
33540
  this.readyForSync = true;
32634
33541
  this.sync();
33542
+ this.drawAnchoredLabels();
32635
33543
  return posString;
32636
33544
  }
32637
33545
  // not included in public docs
@@ -32666,6 +33574,7 @@ export {
32666
33574
  DEFAULT_OPTIONS,
32667
33575
  DRAG_MODE,
32668
33576
  INITIAL_SCENE_DATA,
33577
+ LabelAnchorPoint,
32669
33578
  LabelLineTerminator,
32670
33579
  LabelTextAlignment,
32671
33580
  MULTIPLANAR_TYPE,
@@ -32679,6 +33588,7 @@ export {
32679
33588
  NVMeshFromUrlOptions,
32680
33589
  NVMeshLayerDefaults,
32681
33590
  NVMeshLoaders,
33591
+ NVMeshUtilities,
32682
33592
  NVUtilities,
32683
33593
  Niivue,
32684
33594
  SHOW_RENDER,