@niivue/niivue 0.46.0 → 0.48.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.46.0";
13
+ var version = "0.48.0";
14
14
 
15
15
  // src/logger.ts
16
16
  var _Log = class _Log {
@@ -12368,10 +12368,10 @@ var NVMeshLoaders = class _NVMeshLoaders {
12368
12368
  });
12369
12369
  }
12370
12370
  const dpv32 = [];
12371
- for (let s = 0; s < dpv.length; s++) {
12371
+ for (let i2 = 0; i2 < dpv.length; i2++) {
12372
12372
  dpv32.push({
12373
- id: dpv[i].id,
12374
- vals: Float32Array.from(dpv[i].vals)
12373
+ id: dpv[i2].id,
12374
+ vals: Float32Array.from(dpv[i2].vals)
12375
12375
  });
12376
12376
  }
12377
12377
  offsetPt0[noffset++] = npt;
@@ -14485,6 +14485,7 @@ var NVMeshLoaders = class _NVMeshLoaders {
14485
14485
  plen = reader.getInt32(voxoffset += 4, isLittleEndian) - 1;
14486
14486
  break;
14487
14487
  case TAG_OLD_SURF_GEOM:
14488
+ // these don't take lengths at all
14488
14489
  case TAG_OLD_USEREALRAS:
14489
14490
  plen = 0;
14490
14491
  break;
@@ -20261,6 +20262,11 @@ var NVImage = class _NVImage {
20261
20262
  case "comments":
20262
20263
  hdr.description = items[0].substring(0, Math.min(79, items[0].length));
20263
20264
  break;
20265
+ /* case 'command_history':
20266
+ if (items[0].startsWith('dwi2tensor')) {
20267
+ isTensor = true
20268
+ }
20269
+ break */
20264
20270
  case "RepetitionTime":
20265
20271
  TR = parseFloat(items[0]);
20266
20272
  break;
@@ -20861,9 +20867,48 @@ var NVImage = class _NVImage {
20861
20867
  this.img2RASstart = outStart;
20862
20868
  this.calculateOblique();
20863
20869
  }
20870
+ // Reorient raw header data to RAS
20871
+ // assume single volume, use nVolumes to specify, set nVolumes = 0 for same as input
20872
+ hdr2RAS(nVolumes = 1) {
20873
+ if (!this.permRAS) {
20874
+ throw new Error("permRAS undefined");
20875
+ }
20876
+ if (!this.hdr) {
20877
+ throw new Error("hdr undefined");
20878
+ }
20879
+ const hdrBytes = hdrToArrayBuffer({ ...this.hdr, vox_offset: 352 }, false);
20880
+ const hdr = nifti.readHeader(hdrBytes.buffer, true);
20881
+ if (nVolumes === 1) {
20882
+ hdr.dims[0] = 3;
20883
+ hdr.dims[4] = 1;
20884
+ } else if (nVolumes > 1) {
20885
+ hdr.dims[0] = 4;
20886
+ hdr.dims[4] = nVolumes;
20887
+ }
20888
+ const perm = this.permRAS.slice();
20889
+ if (perm[0] === 1 && perm[1] === 2 && perm[2] === 3) {
20890
+ return hdr;
20891
+ }
20892
+ hdr.qform_code = 0;
20893
+ for (let i = 1; i < 4; i++) {
20894
+ hdr.dims[i] = this.dimsRAS[i];
20895
+ }
20896
+ for (let i = 0; i < this.pixDimsRAS.length; i++) {
20897
+ hdr.pixDims[i] = this.pixDimsRAS[i];
20898
+ }
20899
+ let k = 0;
20900
+ for (let i = 0; i < 4; i++) {
20901
+ for (let j = 0; j < 4; j++) {
20902
+ hdr.affine[i][j] = this.matRAS[k];
20903
+ k++;
20904
+ }
20905
+ }
20906
+ return hdr;
20907
+ }
20864
20908
  // Reorient raw image data to RAS
20865
20909
  // note that GPU-based orient shader is much faster
20866
- img2RAS() {
20910
+ // returns single 3D volume even for 4D input. Use nVolume to select volume (0 indexed)
20911
+ img2RAS(nVolume = 0) {
20867
20912
  if (!this.permRAS) {
20868
20913
  throw new Error("permRAS undefined");
20869
20914
  }
@@ -20878,7 +20923,13 @@ var NVImage = class _NVImage {
20878
20923
  return this.img;
20879
20924
  }
20880
20925
  const hdr = this.hdr;
20881
- const imgRAS = this.img.slice(0, hdr.dims[1] * hdr.dims[1] * hdr.dims[2] * hdr.dims[3]);
20926
+ const nVox = hdr.dims[1] * hdr.dims[2] * hdr.dims[3];
20927
+ let volSkip = nVolume * nVox;
20928
+ if (volSkip + nVox > this.img.length || volSkip < 0) {
20929
+ volSkip = 0;
20930
+ log.warn(`img2RAS nVolume (${nVolume}) out of bounds (${nVolume}+1)\xD7${nVox} > ${this.img.length}`);
20931
+ }
20932
+ const imgRAS = this.img.slice(0, nVox);
20882
20933
  const aperm = [Math.abs(perm[0]), Math.abs(perm[1]), Math.abs(perm[2])];
20883
20934
  const outdim = [hdr.dims[aperm[0]], hdr.dims[aperm[1]], hdr.dims[aperm[2]]];
20884
20935
  const inStep = [1, hdr.dims[1], hdr.dims[1] * hdr.dims[2]];
@@ -20897,7 +20948,7 @@ var NVImage = class _NVImage {
20897
20948
  const yi = outStart[1] + y * outStep[1];
20898
20949
  for (let x = 0; x < outdim[0]; x++) {
20899
20950
  const xi = outStart[0] + x * outStep[0];
20900
- imgRAS[j] = this.img[xi + yi + zi];
20951
+ imgRAS[j] = this.img[xi + yi + zi + volSkip];
20901
20952
  j++;
20902
20953
  }
20903
20954
  }
@@ -22309,6 +22360,7 @@ var DEFAULT_OPTIONS = {
22309
22360
  textHeight: 0.06,
22310
22361
  colorbarHeight: 0.05,
22311
22362
  crosshairWidth: 1,
22363
+ crosshairWidthUnit: "voxels",
22312
22364
  crosshairGap: 0,
22313
22365
  rulerWidth: 4,
22314
22366
  show3Dcrosshair: false,
@@ -22334,6 +22386,7 @@ var DEFAULT_OPTIONS = {
22334
22386
  isRuler: false,
22335
22387
  isColorbar: false,
22336
22388
  isOrientCube: false,
22389
+ tileMargin: 0,
22337
22390
  multiplanarPadPixels: 0,
22338
22391
  // @deprecated
22339
22392
  multiplanarForceRender: false,
@@ -22346,6 +22399,8 @@ var DEFAULT_OPTIONS = {
22346
22399
  yoke3Dto2DZoom: false,
22347
22400
  isDepthPickMesh: false,
22348
22401
  isCornerOrientationText: false,
22402
+ heroImageFraction: 0,
22403
+ heroSliceType: 4 /* RENDER */,
22349
22404
  sagittalNoseLeft: false,
22350
22405
  isSliceMM: false,
22351
22406
  isV1SliceShader: false,
@@ -22394,8 +22449,17 @@ var DEFAULT_OPTIONS = {
22394
22449
  clickToSegmentIs2D: false,
22395
22450
  selectionBoxLineThickness: 4,
22396
22451
  selectionBoxIsOutline: false,
22397
- scrollRequiresFocus: false
22452
+ scrollRequiresFocus: false,
22398
22453
  // determines if the cavas need to be focused to scroll
22454
+ showMeasureUnits: true,
22455
+ // e.g. 20.2 vs 20.2 mm
22456
+ measureTextJustify: "center",
22457
+ // start, center, end
22458
+ measureTextColor: [1, 0, 0, 1],
22459
+ // red
22460
+ measureLineColor: [1, 0, 0, 1],
22461
+ // red
22462
+ measureTextHeight: 0.03
22399
22463
  };
22400
22464
  var INITIAL_SCENE_DATA = {
22401
22465
  gamma: 1,
@@ -23053,8 +23117,27 @@ var NVConnectome = class _NVConnectome extends NVMesh2 {
23053
23117
  const nodes = this.nodes;
23054
23118
  if (nodes && nodes.length > 0) {
23055
23119
  const largest = nodes.reduce((a, b) => a.sizeValue > b.sizeValue ? a : b).sizeValue;
23056
- const min = this.nodeMinColor ? this.nodeMinColor : nodes.reduce((a, b) => a.colorValue < b.colorValue ? a : b).colorValue;
23057
- const max = this.nodeMaxColor ? this.nodeMaxColor : nodes.reduce((a, b) => a.colorValue > b.colorValue ? a : b).colorValue;
23120
+ let min, max;
23121
+ if (typeof this.nodeMinColor !== "undefined" && isFinite(this.nodeMinColor)) {
23122
+ min = this.nodeMinColor;
23123
+ } else {
23124
+ min = nodes[0].colorValue;
23125
+ for (let i = 1; i < nodes.length; i++) {
23126
+ if (nodes[i].colorValue < min) {
23127
+ min = nodes[i].colorValue;
23128
+ }
23129
+ }
23130
+ }
23131
+ if (typeof this.nodeMaxColor !== "undefined" && isFinite(this.nodeMaxColor)) {
23132
+ max = this.nodeMaxColor;
23133
+ } else {
23134
+ max = nodes[0].colorValue;
23135
+ for (let i = 1; i < nodes.length; i++) {
23136
+ if (nodes[i].colorValue > max) {
23137
+ max = nodes[i].colorValue;
23138
+ }
23139
+ }
23140
+ }
23058
23141
  const lut = cmapper.colormap(this.nodeColormap, this.colormapInvert);
23059
23142
  const lutNeg = cmapper.colormap(this.nodeColormapNegative, this.colormapInvert);
23060
23143
  const hasNeg = "nodeColormapNegative" in this;
@@ -23185,14 +23268,29 @@ var NVConnectome = class _NVConnectome extends NVMesh2 {
23185
23268
  let lut = cmapper.colormap(this.nodeColormap, this.colormapInvert);
23186
23269
  let lutNeg = cmapper.colormap(this.nodeColormapNegative, this.colormapInvert);
23187
23270
  let hasNeg = "nodeColormapNegative" in this;
23188
- if (this.nodeMinColor === void 0 || this.nodeMaxColor === void 0) {
23189
- throw new Error("nodeMinColor or nodeMaxColor is undefined");
23271
+ if (this.nodeMinColor === void 0) {
23272
+ this.nodeMinColor = NaN;
23273
+ }
23274
+ if (this.nodeMaxColor === void 0) {
23275
+ this.nodeMaxColor = NaN;
23190
23276
  }
23191
- if (this.edgeMin === void 0 || this.edgeMax === void 0) {
23192
- throw new Error("edgeMin or edgeMax undefined");
23277
+ if (this.edgeMin === void 0) {
23278
+ this.edgeMin = NaN;
23279
+ }
23280
+ if (this.edgeMax === void 0) {
23281
+ this.edgeMax = NaN;
23193
23282
  }
23194
23283
  let min = this.nodeMinColor;
23195
23284
  let max = this.nodeMaxColor;
23285
+ if (!isFinite(min) || !isFinite(min)) {
23286
+ const nodes2 = this.nodes;
23287
+ min = nodes2[0].colorValue;
23288
+ max = nodes2[0].colorValue;
23289
+ for (let i = 0; i < nodes2.length; i++) {
23290
+ min = Math.min(min, nodes2[i].colorValue);
23291
+ max = Math.max(max, nodes2[i].colorValue);
23292
+ }
23293
+ }
23196
23294
  const nodes = this.nodes;
23197
23295
  const nNode = nodes.length;
23198
23296
  for (let i = 0; i < nNode; i++) {
@@ -23225,35 +23323,45 @@ var NVConnectome = class _NVConnectome extends NVMesh2 {
23225
23323
  lut = cmapper.colormap(this.edgeColormap, this.colormapInvert);
23226
23324
  lutNeg = cmapper.colormap(this.edgeColormapNegative, this.colormapInvert);
23227
23325
  hasNeg = "edgeColormapNegative" in this;
23228
- min = this.edgeMin;
23229
- max = this.edgeMax;
23230
23326
  const edges = this.edges;
23231
- for (const edge of edges) {
23232
- let color = edge.colorValue;
23233
- const isNeg = hasNeg && color < 0;
23234
- if (isNeg) {
23235
- color = -color;
23236
- }
23237
- const radius = color * this.edgeScale;
23238
- if (radius <= 0) {
23239
- continue;
23240
- }
23241
- if (min < max) {
23242
- if (color < min) {
23327
+ if (edges !== void 0 && edges.length > 0) {
23328
+ min = this.edgeMin;
23329
+ max = this.edgeMax;
23330
+ if (!isFinite(min) || !isFinite(min)) {
23331
+ min = edges[0].colorValue;
23332
+ max = edges[0].colorValue;
23333
+ for (let i = 0; i < edges.length; i++) {
23334
+ min = Math.min(min, edges[i].colorValue);
23335
+ max = Math.max(max, edges[i].colorValue);
23336
+ }
23337
+ }
23338
+ for (const edge of edges) {
23339
+ let color = edge.colorValue;
23340
+ const isNeg = hasNeg && color < 0;
23341
+ if (isNeg) {
23342
+ color = -color;
23343
+ }
23344
+ const radius = color * this.edgeScale;
23345
+ if (radius <= 0) {
23243
23346
  continue;
23244
23347
  }
23245
- color = (color - min) / (max - min);
23246
- } else {
23247
- color = 1;
23248
- }
23249
- color = Math.round(Math.max(Math.min(255, color * 255))) * 4;
23250
- let rgba = [lut[color], lut[color + 1], lut[color + 2], 255];
23251
- if (isNeg) {
23252
- rgba = [lutNeg[color], lutNeg[color + 1], lutNeg[color + 2], 255];
23348
+ if (min < max) {
23349
+ if (color < min) {
23350
+ continue;
23351
+ }
23352
+ color = (color - min) / (max - min);
23353
+ } else {
23354
+ color = 1;
23355
+ }
23356
+ color = Math.round(Math.max(Math.min(255, color * 255))) * 4;
23357
+ let rgba = [lut[color], lut[color + 1], lut[color + 2], 255];
23358
+ if (isNeg) {
23359
+ rgba = [lutNeg[color], lutNeg[color + 1], lutNeg[color + 2], 255];
23360
+ }
23361
+ const pti = vec39.fromValues(nodes[edge.first].x, nodes[edge.first].y, nodes[edge.first].z);
23362
+ const ptj = vec39.fromValues(nodes[edge.second].x, nodes[edge.second].y, nodes[edge.second].z);
23363
+ NiivueObject3D.makeColoredCylinder(pts, tris, rgba255, pti, ptj, radius, rgba);
23253
23364
  }
23254
- const pti = vec39.fromValues(nodes[edge.first].x, nodes[edge.first].y, nodes[edge.first].z);
23255
- const ptj = vec39.fromValues(nodes[edge.second].x, nodes[edge.second].y, nodes[edge.second].z);
23256
- NiivueObject3D.makeColoredCylinder(pts, tris, rgba255, pti, ptj, radius, rgba);
23257
23365
  }
23258
23366
  const pts32 = new Float32Array(pts);
23259
23367
  const tris32 = new Uint32Array(tris);
@@ -25922,6 +26030,16 @@ var Niivue = class {
25922
26030
  this.opts.isCornerOrientationText = isCornerOrientationText;
25923
26031
  this.updateGLVolume();
25924
26032
  }
26033
+ /**
26034
+ * determine proportion of screen real estate devoted to rendering in multiplanar view.
26035
+ * @param fraction - proportion of screen devoted to primary (hero) image (0 to disable)
26036
+ * @example niivue.setHeroImage(0.5)
26037
+ * @see {@link https://niivue.github.io/niivue/features/layout.html | live demo usage}
26038
+ */
26039
+ setHeroImage(fraction) {
26040
+ this.opts.heroImageFraction = fraction;
26041
+ this.drawScene();
26042
+ }
25925
26043
  /**
25926
26044
  * control whether 2D slices use radiological or neurological convention.
25927
26045
  * @param isRadiologicalConvention - new display convention
@@ -30887,30 +31005,39 @@ var Niivue = class {
30887
31005
  }
30888
31006
  const frac10cm = 100 / fovMM[0];
30889
31007
  const pix10cm = frac10cm * ltwh[2];
30890
- const pixLeft = ltwh[0] + 0.5 * ltwh[2] - 0.5 * pix10cm;
30891
- const pixTop = ltwh[1] + ltwh[3] - 2 * this.opts.rulerWidth;
31008
+ const pix1cm = Math.max(Math.round(pix10cm * 0.1), 2);
31009
+ const pixLeft = Math.floor(ltwh[0] + 0.5 * ltwh[2] - 0.5 * pix10cm);
31010
+ const thick = Number(this.opts.rulerWidth);
31011
+ const pixTop = Math.floor(ltwh[1] + ltwh[3] - pix1cm) + 0.5 * thick;
30892
31012
  const startXYendXY = [pixLeft, pixTop, pixLeft + pix10cm, pixTop];
30893
- this.drawRuler10cm(startXYendXY);
31013
+ let outlineColor = [0, 0, 0, 1];
31014
+ if (this.opts.rulerColor[0] + this.opts.rulerColor[1] + this.opts.rulerColor[2] < 0.8) {
31015
+ outlineColor = [1, 1, 1, 1];
31016
+ }
31017
+ this.drawRuler10cm(startXYendXY, outlineColor, thick + 1);
31018
+ this.drawRuler10cm(startXYendXY, this.opts.rulerColor, thick);
30894
31019
  }
30895
31020
  // not included in public docs
30896
31021
  // draw 10cm ruler at desired coordinates
30897
- drawRuler10cm(startXYendXY) {
31022
+ drawRuler10cm(startXYendXY, rulerColor, rulerWidth = 1) {
30898
31023
  if (!this.lineShader) {
30899
31024
  throw new Error("lineShader undefined");
30900
31025
  }
30901
31026
  this.gl.bindVertexArray(this.genericVAO);
30902
31027
  this.lineShader.use(this.gl);
30903
- this.gl.uniform4fv(this.lineShader.uniforms.lineColor, this.opts.rulerColor);
31028
+ this.gl.uniform4fv(this.lineShader.uniforms.lineColor, rulerColor);
30904
31029
  this.gl.uniform2fv(this.lineShader.uniforms.canvasWidthHeight, [this.gl.canvas.width, this.gl.canvas.height]);
30905
- this.gl.uniform1f(this.lineShader.uniforms.thickness, this.opts.rulerWidth);
31030
+ this.gl.uniform1f(this.lineShader.uniforms.thickness, rulerWidth);
30906
31031
  this.gl.uniform4fv(this.lineShader.uniforms.startXYendXY, startXYendXY);
30907
31032
  this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
30908
31033
  const w1cm = -0.1 * (startXYendXY[0] - startXYendXY[2]);
30909
- const b = startXYendXY[1];
30910
- const t = b - 2 * this.opts.rulerWidth;
30911
- const t2 = b - 4 * this.opts.rulerWidth;
31034
+ const b = startXYendXY[1] - Math.floor(0.5 * this.opts.rulerWidth);
31035
+ const t = Math.floor(b - 0.35 * w1cm);
31036
+ const t2 = Math.floor(b - 0.7 * w1cm);
30912
31037
  for (let i = 0; i < 11; i++) {
30913
- const l = startXYendXY[0] + i * w1cm;
31038
+ let l = startXYendXY[0] + i * w1cm;
31039
+ l = Math.max(l, startXYendXY[0] + 0.5 * rulerWidth);
31040
+ l = Math.min(l, startXYendXY[2] - 0.5 * rulerWidth);
30914
31041
  const xyxy = [l, b, l, t];
30915
31042
  if (i % 5 === 0) {
30916
31043
  xyxy[3] = t2;
@@ -30988,6 +31115,24 @@ var Niivue = class {
30988
31115
  // not included in public docs
30989
31116
  // draw line between start/end points and text to report length
30990
31117
  drawMeasurementTool(startXYendXY) {
31118
+ function extendTo(x0, y0, x1, y1, distance) {
31119
+ const x = x0 - x1;
31120
+ const y = y0 - y1;
31121
+ if (x === 0 && y === 0) {
31122
+ return {
31123
+ origin: [x1 + distance, y1],
31124
+ terminus: [x1 + distance, y1]
31125
+ };
31126
+ }
31127
+ const c = Math.sqrt(x * x + y * y);
31128
+ const dX = distance * x / c;
31129
+ const dY = distance * y / c;
31130
+ return {
31131
+ origin: [x0 + dX, y0 + dY],
31132
+ // next to start point
31133
+ terminus: [x1 - dX, y1 - dY]
31134
+ };
31135
+ }
30991
31136
  const gl = this.gl;
30992
31137
  gl.bindVertexArray(this.genericVAO);
30993
31138
  gl.depthFunc(gl.ALWAYS);
@@ -31002,9 +31147,9 @@ var Niivue = class {
31002
31147
  gl.uniform1f(this.lineShader.uniforms.thickness, this.opts.rulerWidth);
31003
31148
  gl.uniform4fv(this.lineShader.uniforms.startXYendXY, startXYendXY);
31004
31149
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
31005
- const color = this.opts.rulerColor;
31006
- color[3] = 1;
31007
- gl.uniform4fv(this.lineShader.uniforms.lineColor, color);
31150
+ const measureLineColor = this.opts.measureLineColor;
31151
+ measureLineColor[3] = 1;
31152
+ gl.uniform4fv(this.lineShader.uniforms.lineColor, measureLineColor);
31008
31153
  const w = this.opts.rulerWidth;
31009
31154
  gl.uniform1f(this.lineShader.uniforms.thickness, w * 2);
31010
31155
  let sXYeXY = [startXYendXY[0], startXYendXY[1] - w, startXYendXY[0], startXYendXY[1] + w];
@@ -31030,8 +31175,30 @@ var Niivue = class {
31030
31175
  if (lenMM > 99) {
31031
31176
  decimals = 0;
31032
31177
  }
31033
- const stringMM = lenMM.toFixed(decimals);
31034
- this.drawTextBetween(startXYendXY, stringMM, 1, color);
31178
+ let stringMM = lenMM.toFixed(decimals);
31179
+ if (this.opts.showMeasureUnits) {
31180
+ stringMM = `${stringMM} mm`;
31181
+ }
31182
+ let textCoords = startXYendXY;
31183
+ const [x0, y0, x1, y1] = startXYendXY;
31184
+ const { origin, terminus } = extendTo(x0, y0, x1, y1, 30);
31185
+ switch (this.opts.measureTextJustify) {
31186
+ case "start":
31187
+ textCoords = [...origin, ...origin.map((point) => point + 1)];
31188
+ break;
31189
+ case "end":
31190
+ textCoords = textCoords = [...terminus, ...terminus.map((point) => point + 1)];
31191
+ break;
31192
+ default:
31193
+ textCoords = startXYendXY;
31194
+ break;
31195
+ }
31196
+ this.drawTextBetween(
31197
+ textCoords,
31198
+ stringMM,
31199
+ this.opts.measureTextHeight / this.opts.textHeight,
31200
+ this.opts.measureTextColor
31201
+ );
31035
31202
  }
31036
31203
  gl.bindVertexArray(this.unusedVAO);
31037
31204
  }
@@ -31889,6 +32056,18 @@ var Niivue = class {
31889
32056
  }
31890
32057
  draw2D(leftTopWidthHeight, axCorSag, customMM = NaN, imageWidthHeight = [NaN, NaN]) {
31891
32058
  const padLeftTop = [NaN, NaN];
32059
+ if (imageWidthHeight[0] === Infinity) {
32060
+ const volScale = this.sliceScale().volScale;
32061
+ let scale = this.scaleSlice(volScale[0], volScale[1], [0, 0], [leftTopWidthHeight[2], leftTopWidthHeight[3]]);
32062
+ if (axCorSag === 1 /* CORONAL */) {
32063
+ scale = this.scaleSlice(volScale[0], volScale[2], [0, 0], [leftTopWidthHeight[2], leftTopWidthHeight[3]]);
32064
+ }
32065
+ if (axCorSag === 2 /* SAGITTAL */) {
32066
+ scale = this.scaleSlice(volScale[1], volScale[2], [0, 0], [leftTopWidthHeight[2], leftTopWidthHeight[3]]);
32067
+ }
32068
+ imageWidthHeight[0] = scale[2];
32069
+ imageWidthHeight[1] = scale[3];
32070
+ }
31892
32071
  if (isNaN(imageWidthHeight[0])) {
31893
32072
  this.draw2DMain(leftTopWidthHeight, axCorSag, customMM);
31894
32073
  } else {
@@ -32934,22 +33113,28 @@ var Niivue = class {
32934
33113
  }
32935
33114
  const gl = this.gl;
32936
33115
  const mm = this.frac2mm(this.scene.crosshairPos, 0, isSliceMM);
33116
+ let radius = 1;
33117
+ const [mn, mx, range] = this.sceneExtentsMinMax(isSliceMM);
33118
+ if (this.volumes.length > 0) {
33119
+ if (!this.back) {
33120
+ throw new Error("back undefined");
33121
+ }
33122
+ radius = 0.5 * Math.min(Math.min(this.back.pixDims[1], this.back.pixDims[2]), this.back.pixDims[3]);
33123
+ } else if (range[0] < 50 || range[0] > 1e3) {
33124
+ radius = range[0] * 0.02;
33125
+ }
33126
+ radius *= this.opts.crosshairWidth;
33127
+ if (this.opts?.crosshairWidthUnit === "percent") {
33128
+ radius = range[0] * this.opts.crosshairWidth * 0.5 * 0.01;
33129
+ }
33130
+ if (this.opts?.crosshairWidthUnit === "mm") {
33131
+ radius = this.opts.crosshairWidth * 0.5;
33132
+ }
32937
33133
  if (this.crosshairs3D === null || this.crosshairs3D.mm[0] !== mm[0] || this.crosshairs3D.mm[1] !== mm[1] || this.crosshairs3D.mm[2] !== mm[2]) {
32938
33134
  if (this.crosshairs3D !== null) {
32939
33135
  gl.deleteBuffer(this.crosshairs3D.indexBuffer);
32940
33136
  gl.deleteBuffer(this.crosshairs3D.vertexBuffer);
32941
33137
  }
32942
- const [mn, mx, range] = this.sceneExtentsMinMax(isSliceMM);
32943
- let radius = 1;
32944
- if (this.volumes.length > 0) {
32945
- if (!this.back) {
32946
- throw new Error("back undefined");
32947
- }
32948
- radius = 0.5 * Math.min(Math.min(this.back.pixDims[1], this.back.pixDims[2]), this.back.pixDims[3]);
32949
- } else if (range[0] < 50 || range[0] > 1e3) {
32950
- radius = range[0] * 0.02;
32951
- }
32952
- radius *= this.opts.crosshairWidth;
32953
33138
  this.crosshairs3D = NiivueObject3D.generateCrosshairs(this.gl, 1, mm, mn, mx, radius, 20, this.opts.crosshairGap);
32954
33139
  this.crosshairs3D.mm = mm;
32955
33140
  }
@@ -32968,6 +33153,7 @@ var Niivue = class {
32968
33153
  );
32969
33154
  }
32970
33155
  gl.uniformMatrix4fv(crosshairsShader.uniforms.mvpMtx, false, mvpMtx);
33156
+ gl.disable(gl.CULL_FACE);
32971
33157
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.crosshairs3D.indexBuffer);
32972
33158
  gl.enable(gl.DEPTH_TEST);
32973
33159
  const color = [...this.opts.crosshairColor];
@@ -33118,9 +33304,9 @@ var Niivue = class {
33118
33304
  }
33119
33305
  // not included in public docs
33120
33306
  // note: we also have a "sliceScale" method, which could be confusing
33121
- scaleSlice(w, h, widthPadPixels = 0, heightPadPixels = 0) {
33122
- const canvasW = this.effectiveCanvasWidth() - widthPadPixels;
33123
- const canvasH = this.effectiveCanvasHeight() - heightPadPixels;
33307
+ scaleSlice(w, h, padPixelsWH = [0, 0], canvasWH = [0, 0]) {
33308
+ const canvasW = canvasWH[0] === 0 ? this.effectiveCanvasWidth() - padPixelsWH[0] : canvasWH[0] - padPixelsWH[0];
33309
+ const canvasH = canvasWH[1] === 0 ? this.effectiveCanvasHeight() - padPixelsWH[1] : canvasWH[1] - padPixelsWH[1];
33124
33310
  let scalePix = canvasW / w;
33125
33311
  if (h * scalePix > canvasH) {
33126
33312
  scalePix = canvasH / h;
@@ -33569,7 +33755,6 @@ var Niivue = class {
33569
33755
  if (this.volumes.length === 0 || typeof this.volumes[0].dims === "undefined") {
33570
33756
  if (this.meshes.length > 0) {
33571
33757
  this.screenSlices = [];
33572
- this.opts.sliceType = 4 /* RENDER */;
33573
33758
  this.draw3D();
33574
33759
  if (this.opts.isColorbar) {
33575
33760
  this.drawColorbar();
@@ -33613,6 +33798,8 @@ var Niivue = class {
33613
33798
  if (this.sliceMosaicString.length > 0) {
33614
33799
  this.drawMosaic(this.sliceMosaicString);
33615
33800
  } else {
33801
+ const heroImageWH = [0, 0];
33802
+ let isHeroImage = false;
33616
33803
  this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
33617
33804
  this.screenSlices = [];
33618
33805
  if (this.opts.sliceType === 0 /* AXIAL */) {
@@ -33622,6 +33809,9 @@ var Niivue = class {
33622
33809
  } else if (this.opts.sliceType === 2 /* SAGITTAL */) {
33623
33810
  this.draw2D([0, 0, 0, 0], 2);
33624
33811
  } else {
33812
+ let padPixelsWH = function(cols, rows) {
33813
+ return [(cols - 1) * pad + cols * innerPad, (rows - 1) * pad + rows * innerPad];
33814
+ };
33625
33815
  let isShowRender = false;
33626
33816
  if (this.opts.multiplanarForceRender) {
33627
33817
  isShowRender = true;
@@ -33648,20 +33838,42 @@ var Niivue = class {
33648
33838
  log.debug("multiplanarPadPixels must be numeric");
33649
33839
  }
33650
33840
  const pad = parseFloat(`${this.opts.multiplanarPadPixels}`);
33651
- const ltwh2x2 = this.scaleSlice(volScale[0] + volScale[1], volScale[1] + volScale[2], pad * 1, pad * 1);
33841
+ let innerPad = this.opts.tileMargin;
33842
+ if (innerPad < 0) {
33843
+ innerPad = 2 * (2 + Math.ceil(Math.max(this.opts.textHeight, 0.01) * Math.min(this.gl.canvas.height, this.gl.canvas.width)));
33844
+ }
33845
+ let canvasWH = [this.effectiveCanvasWidth(), this.effectiveCanvasHeight()];
33846
+ if (this.opts.heroImageFraction > 0 && this.opts.heroImageFraction < 1) {
33847
+ isShowRender = false;
33848
+ isHeroImage = true;
33849
+ if (canvasWH[0] > canvasWH[1] && this.opts.multiplanarLayout !== 3 /* ROW */) {
33850
+ heroImageWH[0] = canvasWH[0] * this.opts.heroImageFraction;
33851
+ } else {
33852
+ heroImageWH[1] = canvasWH[1] * this.opts.heroImageFraction;
33853
+ }
33854
+ canvasWH = [canvasWH[0] - heroImageWH[0], canvasWH[1] - heroImageWH[1]];
33855
+ }
33856
+ const ltwh2x2 = this.scaleSlice(
33857
+ volScale[0] + volScale[1],
33858
+ volScale[1] + volScale[2],
33859
+ padPixelsWH(2, 2),
33860
+ canvasWH
33861
+ );
33652
33862
  const mx = Math.max(Math.max(volScale[1], volScale[2]), volScale[0]);
33653
33863
  const ltwh3x1 = this.scaleSlice(
33654
33864
  volScale[0] + volScale[0] + volScale[1],
33655
33865
  Math.max(volScale[1], volScale[2]),
33656
- pad * 2
33866
+ padPixelsWH(3, 1),
33867
+ canvasWH
33657
33868
  );
33658
33869
  const ltwh4x1 = this.scaleSlice(
33659
33870
  volScale[0] + volScale[0] + volScale[1] + mx,
33660
33871
  Math.max(volScale[1], volScale[2]),
33661
- pad * 3
33872
+ padPixelsWH(4, 1),
33873
+ canvasWH
33662
33874
  );
33663
- const ltwh1x3 = this.scaleSlice(mx, volScale[1] + volScale[2] + volScale[2], 0, pad * 2);
33664
- const ltwh1x4 = this.scaleSlice(mx, volScale[1] + volScale[2] + volScale[2] + mx, 0, pad * 3);
33875
+ const ltwh1x3 = this.scaleSlice(mx, volScale[1] + volScale[2] + volScale[2], padPixelsWH(1, 3), canvasWH);
33876
+ const ltwh1x4 = this.scaleSlice(mx, volScale[1] + volScale[2] + volScale[2] + mx, padPixelsWH(1, 4), canvasWH);
33665
33877
  let isDraw3D = !isDrawPenDown && (maxVols < 2 || !isDrawGraph);
33666
33878
  let isDrawColumn = false;
33667
33879
  let isDrawGrid = false;
@@ -33684,22 +33896,39 @@ var Niivue = class {
33684
33896
  let ltwh = ltwh2x2;
33685
33897
  if (isDrawColumn) {
33686
33898
  ltwh = ltwh1x3;
33687
- if (isShowRender || this.opts.multiplanarShowRender === 2 /* AUTO */ && ltwh1x4[4] >= ltwh1x3[4]) {
33899
+ if (!isHeroImage && (isShowRender || this.opts.multiplanarShowRender === 2 /* AUTO */ && ltwh1x4[4] >= ltwh1x3[4])) {
33688
33900
  ltwh = ltwh1x4;
33689
33901
  } else {
33690
33902
  isDraw3D = false;
33691
33903
  }
33692
33904
  } else if (isDrawRow) {
33693
33905
  ltwh = ltwh3x1;
33694
- if (isShowRender || this.opts.multiplanarShowRender === 2 /* AUTO */ && ltwh4x1[4] >= ltwh3x1[4]) {
33906
+ if (!isHeroImage && (isShowRender || this.opts.multiplanarShowRender === 2 /* AUTO */ && ltwh4x1[4] >= ltwh3x1[4])) {
33695
33907
  ltwh = ltwh4x1;
33696
33908
  } else {
33697
33909
  isDraw3D = false;
33698
33910
  }
33699
33911
  }
33700
- const sX = volScale[0] * ltwh[4];
33701
- const sY = volScale[1] * ltwh[4];
33702
- const sZ = volScale[2] * ltwh[4];
33912
+ if (isHeroImage) {
33913
+ const heroW = heroImageWH[0] === 0 ? this.effectiveCanvasWidth() : heroImageWH[0];
33914
+ const heroH = heroImageWH[1] === 0 ? this.effectiveCanvasHeight() : heroImageWH[1];
33915
+ if (this.opts?.heroSliceType === 0 /* AXIAL */ || this.opts?.heroSliceType === 1 /* CORONAL */ || this.opts?.heroSliceType === 2 /* SAGITTAL */) {
33916
+ this.draw2D([0, 0, heroW, heroH], this.opts.heroSliceType, NaN, [Infinity, Infinity]);
33917
+ } else {
33918
+ const ltwh2 = ltwh.slice();
33919
+ const canvasW = this.effectiveCanvasWidth();
33920
+ if (heroW === canvasW) {
33921
+ ltwh2[0] = 0;
33922
+ }
33923
+ this.draw3D([ltwh2[0], 0, heroW, heroH]);
33924
+ }
33925
+ ltwh[0] += heroImageWH[0];
33926
+ ltwh[1] += heroImageWH[1];
33927
+ isDraw3D = false;
33928
+ }
33929
+ const sX = volScale[0] * ltwh[4] + innerPad;
33930
+ const sY = volScale[1] * ltwh[4] + innerPad;
33931
+ const sZ = volScale[2] * ltwh[4] + innerPad;
33703
33932
  const actualX = actualScale[0] * ltwh[4];
33704
33933
  const actualY = actualScale[1] * ltwh[4];
33705
33934
  const actualZ = actualScale[2] * ltwh[4];
@@ -33726,6 +33955,9 @@ var Niivue = class {
33726
33955
  if (this.opts.multiplanarShowRender === 2 /* AUTO */) {
33727
33956
  isDraw3D = true;
33728
33957
  }
33958
+ if (isHeroImage) {
33959
+ isDraw3D = false;
33960
+ }
33729
33961
  this.draw2D([ltwh[0], ltwh[1] + sZ + pad, sX, sY], 0, NaN, [actualX, actualY]);
33730
33962
  this.draw2D([ltwh[0], ltwh[1], sX, sZ], 1, NaN, [actualX, actualZ]);
33731
33963
  this.draw2D([ltwh[0] + sX + pad, ltwh[1], sY, sZ], 2, NaN, [actualY, actualZ]);