@niivue/niivue 0.46.0 → 0.47.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.d.ts CHANGED
@@ -614,6 +614,7 @@ type NVConfigOptions = {
614
614
  textHeight: number;
615
615
  colorbarHeight: number;
616
616
  crosshairWidth: number;
617
+ crosshairWidthUnit: 'voxels' | 'mm' | 'percent';
617
618
  crosshairGap: number;
618
619
  rulerWidth: number;
619
620
  show3Dcrosshair: boolean;
@@ -638,6 +639,7 @@ type NVConfigOptions = {
638
639
  isRuler: boolean;
639
640
  isColorbar: boolean;
640
641
  isOrientCube: boolean;
642
+ tileMargin: number;
641
643
  multiplanarPadPixels: number;
642
644
  multiplanarForceRender: boolean;
643
645
  multiplanarEqualSize: boolean;
@@ -648,6 +650,8 @@ type NVConfigOptions = {
648
650
  yoke3Dto2DZoom: boolean;
649
651
  isDepthPickMesh: boolean;
650
652
  isCornerOrientationText: boolean;
653
+ heroImageFraction: number;
654
+ heroSliceType: SLICE_TYPE;
651
655
  sagittalNoseLeft: boolean;
652
656
  isSliceMM: boolean;
653
657
  isV1SliceShader: boolean;
@@ -688,6 +692,11 @@ type NVConfigOptions = {
688
692
  selectionBoxLineThickness: number;
689
693
  selectionBoxIsOutline: boolean;
690
694
  scrollRequiresFocus: boolean;
695
+ showMeasureUnits: boolean;
696
+ measureTextJustify: 'start' | 'center' | 'end';
697
+ measureTextColor: number[];
698
+ measureLineColor: number[];
699
+ measureTextHeight: number;
691
700
  };
692
701
  declare const DEFAULT_OPTIONS: NVConfigOptions;
693
702
  type SceneData = {
@@ -2205,6 +2214,13 @@ declare class Niivue {
2205
2214
  * @see {@link https://niivue.github.io/niivue/features/worldspace2.html | live demo usage}
2206
2215
  */
2207
2216
  setCornerOrientationText(isCornerOrientationText: boolean): void;
2217
+ /**
2218
+ * determine proportion of screen real estate devoted to rendering in multiplanar view.
2219
+ * @param fraction - proportion of screen devoted to primary (hero) image (0 to disable)
2220
+ * @example niivue.setHeroImage(0.5)
2221
+ * @see {@link https://niivue.github.io/niivue/features/layout.html | live demo usage}
2222
+ */
2223
+ setHeroImage(fraction: number): void;
2208
2224
  /**
2209
2225
  * control whether 2D slices use radiological or neurological convention.
2210
2226
  * @param isRadiologicalConvention - new display convention
@@ -3028,7 +3044,7 @@ declare class Niivue {
3028
3044
  sumBitmap(img: Uint8Array): number;
3029
3045
  mouseClick(x: number, y: number, posChange?: number, isDelta?: boolean): void;
3030
3046
  drawRuler(): void;
3031
- drawRuler10cm(startXYendXY: number[]): void;
3047
+ drawRuler10cm(startXYendXY: number[], rulerColor: number[], rulerWidth?: number): void;
3032
3048
  screenXY2mm(x: number, y: number, forceSlice?: number): vec4;
3033
3049
  dragForPanZoom(startXYendXY: number[]): void;
3034
3050
  dragForCenterButton(startXYendXY: number[]): void;
@@ -3119,7 +3135,7 @@ declare class Niivue {
3119
3135
  frac2mm(frac: vec3, volIdx?: number, isForceSliceMM?: boolean): vec4;
3120
3136
  screenXY2TextureFrac(x: number, y: number, i: number, restrict0to1?: boolean): vec3;
3121
3137
  canvasPos2frac(canvasPos: number[]): vec3;
3122
- scaleSlice(w: number, h: number, widthPadPixels?: number, heightPadPixels?: number): number[];
3138
+ scaleSlice(w: number, h: number, padPixelsWH?: [number, number], canvasWH?: [number, number]): number[];
3123
3139
  drawThumbnail(): void;
3124
3140
  drawLine(startXYendXY: number[], thickness?: number, lineColor?: number[]): void;
3125
3141
  draw3DLine(startXY: vec2, endXYZ: vec3, thickness?: number, lineColor?: number[]): void;
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.47.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;
@@ -22309,6 +22315,7 @@ var DEFAULT_OPTIONS = {
22309
22315
  textHeight: 0.06,
22310
22316
  colorbarHeight: 0.05,
22311
22317
  crosshairWidth: 1,
22318
+ crosshairWidthUnit: "voxels",
22312
22319
  crosshairGap: 0,
22313
22320
  rulerWidth: 4,
22314
22321
  show3Dcrosshair: false,
@@ -22334,6 +22341,7 @@ var DEFAULT_OPTIONS = {
22334
22341
  isRuler: false,
22335
22342
  isColorbar: false,
22336
22343
  isOrientCube: false,
22344
+ tileMargin: 0,
22337
22345
  multiplanarPadPixels: 0,
22338
22346
  // @deprecated
22339
22347
  multiplanarForceRender: false,
@@ -22346,6 +22354,8 @@ var DEFAULT_OPTIONS = {
22346
22354
  yoke3Dto2DZoom: false,
22347
22355
  isDepthPickMesh: false,
22348
22356
  isCornerOrientationText: false,
22357
+ heroImageFraction: 0,
22358
+ heroSliceType: 4 /* RENDER */,
22349
22359
  sagittalNoseLeft: false,
22350
22360
  isSliceMM: false,
22351
22361
  isV1SliceShader: false,
@@ -22394,8 +22404,17 @@ var DEFAULT_OPTIONS = {
22394
22404
  clickToSegmentIs2D: false,
22395
22405
  selectionBoxLineThickness: 4,
22396
22406
  selectionBoxIsOutline: false,
22397
- scrollRequiresFocus: false
22407
+ scrollRequiresFocus: false,
22398
22408
  // determines if the cavas need to be focused to scroll
22409
+ showMeasureUnits: true,
22410
+ // e.g. 20.2 vs 20.2 mm
22411
+ measureTextJustify: "center",
22412
+ // start, center, end
22413
+ measureTextColor: [1, 0, 0, 1],
22414
+ // red
22415
+ measureLineColor: [1, 0, 0, 1],
22416
+ // red
22417
+ measureTextHeight: 0.03
22399
22418
  };
22400
22419
  var INITIAL_SCENE_DATA = {
22401
22420
  gamma: 1,
@@ -23053,8 +23072,27 @@ var NVConnectome = class _NVConnectome extends NVMesh2 {
23053
23072
  const nodes = this.nodes;
23054
23073
  if (nodes && nodes.length > 0) {
23055
23074
  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;
23075
+ let min, max;
23076
+ if (typeof this.nodeMinColor !== "undefined" && isFinite(this.nodeMinColor)) {
23077
+ min = this.nodeMinColor;
23078
+ } else {
23079
+ min = nodes[0].colorValue;
23080
+ for (let i = 1; i < nodes.length; i++) {
23081
+ if (nodes[i].colorValue < min) {
23082
+ min = nodes[i].colorValue;
23083
+ }
23084
+ }
23085
+ }
23086
+ if (typeof this.nodeMaxColor !== "undefined" && isFinite(this.nodeMaxColor)) {
23087
+ max = this.nodeMaxColor;
23088
+ } else {
23089
+ max = nodes[0].colorValue;
23090
+ for (let i = 1; i < nodes.length; i++) {
23091
+ if (nodes[i].colorValue > max) {
23092
+ max = nodes[i].colorValue;
23093
+ }
23094
+ }
23095
+ }
23058
23096
  const lut = cmapper.colormap(this.nodeColormap, this.colormapInvert);
23059
23097
  const lutNeg = cmapper.colormap(this.nodeColormapNegative, this.colormapInvert);
23060
23098
  const hasNeg = "nodeColormapNegative" in this;
@@ -23185,14 +23223,29 @@ var NVConnectome = class _NVConnectome extends NVMesh2 {
23185
23223
  let lut = cmapper.colormap(this.nodeColormap, this.colormapInvert);
23186
23224
  let lutNeg = cmapper.colormap(this.nodeColormapNegative, this.colormapInvert);
23187
23225
  let hasNeg = "nodeColormapNegative" in this;
23188
- if (this.nodeMinColor === void 0 || this.nodeMaxColor === void 0) {
23189
- throw new Error("nodeMinColor or nodeMaxColor is undefined");
23226
+ if (this.nodeMinColor === void 0) {
23227
+ this.nodeMinColor = NaN;
23228
+ }
23229
+ if (this.nodeMaxColor === void 0) {
23230
+ this.nodeMaxColor = NaN;
23231
+ }
23232
+ if (this.edgeMin === void 0) {
23233
+ this.edgeMin = NaN;
23190
23234
  }
23191
- if (this.edgeMin === void 0 || this.edgeMax === void 0) {
23192
- throw new Error("edgeMin or edgeMax undefined");
23235
+ if (this.edgeMax === void 0) {
23236
+ this.edgeMax = NaN;
23193
23237
  }
23194
23238
  let min = this.nodeMinColor;
23195
23239
  let max = this.nodeMaxColor;
23240
+ if (!isFinite(min) || !isFinite(min)) {
23241
+ const nodes2 = this.nodes;
23242
+ min = nodes2[0].colorValue;
23243
+ max = nodes2[0].colorValue;
23244
+ for (let i = 0; i < nodes2.length; i++) {
23245
+ min = Math.min(min, nodes2[i].colorValue);
23246
+ max = Math.max(max, nodes2[i].colorValue);
23247
+ }
23248
+ }
23196
23249
  const nodes = this.nodes;
23197
23250
  const nNode = nodes.length;
23198
23251
  for (let i = 0; i < nNode; i++) {
@@ -23225,35 +23278,45 @@ var NVConnectome = class _NVConnectome extends NVMesh2 {
23225
23278
  lut = cmapper.colormap(this.edgeColormap, this.colormapInvert);
23226
23279
  lutNeg = cmapper.colormap(this.edgeColormapNegative, this.colormapInvert);
23227
23280
  hasNeg = "edgeColormapNegative" in this;
23228
- min = this.edgeMin;
23229
- max = this.edgeMax;
23230
23281
  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) {
23282
+ if (edges !== void 0 && edges.length > 0) {
23283
+ min = this.edgeMin;
23284
+ max = this.edgeMax;
23285
+ if (!isFinite(min) || !isFinite(min)) {
23286
+ min = edges[0].colorValue;
23287
+ max = edges[0].colorValue;
23288
+ for (let i = 0; i < edges.length; i++) {
23289
+ min = Math.min(min, edges[i].colorValue);
23290
+ max = Math.max(max, edges[i].colorValue);
23291
+ }
23292
+ }
23293
+ for (const edge of edges) {
23294
+ let color = edge.colorValue;
23295
+ const isNeg = hasNeg && color < 0;
23296
+ if (isNeg) {
23297
+ color = -color;
23298
+ }
23299
+ const radius = color * this.edgeScale;
23300
+ if (radius <= 0) {
23243
23301
  continue;
23244
23302
  }
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];
23303
+ if (min < max) {
23304
+ if (color < min) {
23305
+ continue;
23306
+ }
23307
+ color = (color - min) / (max - min);
23308
+ } else {
23309
+ color = 1;
23310
+ }
23311
+ color = Math.round(Math.max(Math.min(255, color * 255))) * 4;
23312
+ let rgba = [lut[color], lut[color + 1], lut[color + 2], 255];
23313
+ if (isNeg) {
23314
+ rgba = [lutNeg[color], lutNeg[color + 1], lutNeg[color + 2], 255];
23315
+ }
23316
+ const pti = vec39.fromValues(nodes[edge.first].x, nodes[edge.first].y, nodes[edge.first].z);
23317
+ const ptj = vec39.fromValues(nodes[edge.second].x, nodes[edge.second].y, nodes[edge.second].z);
23318
+ NiivueObject3D.makeColoredCylinder(pts, tris, rgba255, pti, ptj, radius, rgba);
23253
23319
  }
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
23320
  }
23258
23321
  const pts32 = new Float32Array(pts);
23259
23322
  const tris32 = new Uint32Array(tris);
@@ -25922,6 +25985,16 @@ var Niivue = class {
25922
25985
  this.opts.isCornerOrientationText = isCornerOrientationText;
25923
25986
  this.updateGLVolume();
25924
25987
  }
25988
+ /**
25989
+ * determine proportion of screen real estate devoted to rendering in multiplanar view.
25990
+ * @param fraction - proportion of screen devoted to primary (hero) image (0 to disable)
25991
+ * @example niivue.setHeroImage(0.5)
25992
+ * @see {@link https://niivue.github.io/niivue/features/layout.html | live demo usage}
25993
+ */
25994
+ setHeroImage(fraction) {
25995
+ this.opts.heroImageFraction = fraction;
25996
+ this.drawScene();
25997
+ }
25925
25998
  /**
25926
25999
  * control whether 2D slices use radiological or neurological convention.
25927
26000
  * @param isRadiologicalConvention - new display convention
@@ -30887,30 +30960,39 @@ var Niivue = class {
30887
30960
  }
30888
30961
  const frac10cm = 100 / fovMM[0];
30889
30962
  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;
30963
+ const pix1cm = Math.max(Math.round(pix10cm * 0.1), 2);
30964
+ const pixLeft = Math.floor(ltwh[0] + 0.5 * ltwh[2] - 0.5 * pix10cm);
30965
+ const thick = Number(this.opts.rulerWidth);
30966
+ const pixTop = Math.floor(ltwh[1] + ltwh[3] - pix1cm) + 0.5 * thick;
30892
30967
  const startXYendXY = [pixLeft, pixTop, pixLeft + pix10cm, pixTop];
30893
- this.drawRuler10cm(startXYendXY);
30968
+ let outlineColor = [0, 0, 0, 1];
30969
+ if (this.opts.rulerColor[0] + this.opts.rulerColor[1] + this.opts.rulerColor[2] < 0.8) {
30970
+ outlineColor = [1, 1, 1, 1];
30971
+ }
30972
+ this.drawRuler10cm(startXYendXY, outlineColor, thick + 1);
30973
+ this.drawRuler10cm(startXYendXY, this.opts.rulerColor, thick);
30894
30974
  }
30895
30975
  // not included in public docs
30896
30976
  // draw 10cm ruler at desired coordinates
30897
- drawRuler10cm(startXYendXY) {
30977
+ drawRuler10cm(startXYendXY, rulerColor, rulerWidth = 1) {
30898
30978
  if (!this.lineShader) {
30899
30979
  throw new Error("lineShader undefined");
30900
30980
  }
30901
30981
  this.gl.bindVertexArray(this.genericVAO);
30902
30982
  this.lineShader.use(this.gl);
30903
- this.gl.uniform4fv(this.lineShader.uniforms.lineColor, this.opts.rulerColor);
30983
+ this.gl.uniform4fv(this.lineShader.uniforms.lineColor, rulerColor);
30904
30984
  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);
30985
+ this.gl.uniform1f(this.lineShader.uniforms.thickness, rulerWidth);
30906
30986
  this.gl.uniform4fv(this.lineShader.uniforms.startXYendXY, startXYendXY);
30907
30987
  this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
30908
30988
  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;
30989
+ const b = startXYendXY[1] - Math.floor(0.5 * this.opts.rulerWidth);
30990
+ const t = Math.floor(b - 0.35 * w1cm);
30991
+ const t2 = Math.floor(b - 0.7 * w1cm);
30912
30992
  for (let i = 0; i < 11; i++) {
30913
- const l = startXYendXY[0] + i * w1cm;
30993
+ let l = startXYendXY[0] + i * w1cm;
30994
+ l = Math.max(l, startXYendXY[0] + 0.5 * rulerWidth);
30995
+ l = Math.min(l, startXYendXY[2] - 0.5 * rulerWidth);
30914
30996
  const xyxy = [l, b, l, t];
30915
30997
  if (i % 5 === 0) {
30916
30998
  xyxy[3] = t2;
@@ -30988,6 +31070,24 @@ var Niivue = class {
30988
31070
  // not included in public docs
30989
31071
  // draw line between start/end points and text to report length
30990
31072
  drawMeasurementTool(startXYendXY) {
31073
+ function extendTo(x0, y0, x1, y1, distance) {
31074
+ const x = x0 - x1;
31075
+ const y = y0 - y1;
31076
+ if (x === 0 && y === 0) {
31077
+ return {
31078
+ origin: [x1 + distance, y1],
31079
+ terminus: [x1 + distance, y1]
31080
+ };
31081
+ }
31082
+ const c = Math.sqrt(x * x + y * y);
31083
+ const dX = distance * x / c;
31084
+ const dY = distance * y / c;
31085
+ return {
31086
+ origin: [x0 + dX, y0 + dY],
31087
+ // next to start point
31088
+ terminus: [x1 - dX, y1 - dY]
31089
+ };
31090
+ }
30991
31091
  const gl = this.gl;
30992
31092
  gl.bindVertexArray(this.genericVAO);
30993
31093
  gl.depthFunc(gl.ALWAYS);
@@ -31002,9 +31102,9 @@ var Niivue = class {
31002
31102
  gl.uniform1f(this.lineShader.uniforms.thickness, this.opts.rulerWidth);
31003
31103
  gl.uniform4fv(this.lineShader.uniforms.startXYendXY, startXYendXY);
31004
31104
  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);
31105
+ const measureLineColor = this.opts.measureLineColor;
31106
+ measureLineColor[3] = 1;
31107
+ gl.uniform4fv(this.lineShader.uniforms.lineColor, measureLineColor);
31008
31108
  const w = this.opts.rulerWidth;
31009
31109
  gl.uniform1f(this.lineShader.uniforms.thickness, w * 2);
31010
31110
  let sXYeXY = [startXYendXY[0], startXYendXY[1] - w, startXYendXY[0], startXYendXY[1] + w];
@@ -31030,8 +31130,30 @@ var Niivue = class {
31030
31130
  if (lenMM > 99) {
31031
31131
  decimals = 0;
31032
31132
  }
31033
- const stringMM = lenMM.toFixed(decimals);
31034
- this.drawTextBetween(startXYendXY, stringMM, 1, color);
31133
+ let stringMM = lenMM.toFixed(decimals);
31134
+ if (this.opts.showMeasureUnits) {
31135
+ stringMM = `${stringMM} mm`;
31136
+ }
31137
+ let textCoords = startXYendXY;
31138
+ const [x0, y0, x1, y1] = startXYendXY;
31139
+ const { origin, terminus } = extendTo(x0, y0, x1, y1, 30);
31140
+ switch (this.opts.measureTextJustify) {
31141
+ case "start":
31142
+ textCoords = [...origin, ...origin.map((point) => point + 1)];
31143
+ break;
31144
+ case "end":
31145
+ textCoords = textCoords = [...terminus, ...terminus.map((point) => point + 1)];
31146
+ break;
31147
+ default:
31148
+ textCoords = startXYendXY;
31149
+ break;
31150
+ }
31151
+ this.drawTextBetween(
31152
+ textCoords,
31153
+ stringMM,
31154
+ this.opts.measureTextHeight / this.opts.textHeight,
31155
+ this.opts.measureTextColor
31156
+ );
31035
31157
  }
31036
31158
  gl.bindVertexArray(this.unusedVAO);
31037
31159
  }
@@ -31889,6 +32011,18 @@ var Niivue = class {
31889
32011
  }
31890
32012
  draw2D(leftTopWidthHeight, axCorSag, customMM = NaN, imageWidthHeight = [NaN, NaN]) {
31891
32013
  const padLeftTop = [NaN, NaN];
32014
+ if (imageWidthHeight[0] === Infinity) {
32015
+ const volScale = this.sliceScale().volScale;
32016
+ let scale = this.scaleSlice(volScale[0], volScale[1], [0, 0], [leftTopWidthHeight[2], leftTopWidthHeight[3]]);
32017
+ if (axCorSag === 1 /* CORONAL */) {
32018
+ scale = this.scaleSlice(volScale[0], volScale[2], [0, 0], [leftTopWidthHeight[2], leftTopWidthHeight[3]]);
32019
+ }
32020
+ if (axCorSag === 2 /* SAGITTAL */) {
32021
+ scale = this.scaleSlice(volScale[1], volScale[2], [0, 0], [leftTopWidthHeight[2], leftTopWidthHeight[3]]);
32022
+ }
32023
+ imageWidthHeight[0] = scale[2];
32024
+ imageWidthHeight[1] = scale[3];
32025
+ }
31892
32026
  if (isNaN(imageWidthHeight[0])) {
31893
32027
  this.draw2DMain(leftTopWidthHeight, axCorSag, customMM);
31894
32028
  } else {
@@ -32934,22 +33068,28 @@ var Niivue = class {
32934
33068
  }
32935
33069
  const gl = this.gl;
32936
33070
  const mm = this.frac2mm(this.scene.crosshairPos, 0, isSliceMM);
33071
+ let radius = 1;
33072
+ const [mn, mx, range] = this.sceneExtentsMinMax(isSliceMM);
33073
+ if (this.volumes.length > 0) {
33074
+ if (!this.back) {
33075
+ throw new Error("back undefined");
33076
+ }
33077
+ radius = 0.5 * Math.min(Math.min(this.back.pixDims[1], this.back.pixDims[2]), this.back.pixDims[3]);
33078
+ } else if (range[0] < 50 || range[0] > 1e3) {
33079
+ radius = range[0] * 0.02;
33080
+ }
33081
+ radius *= this.opts.crosshairWidth;
33082
+ if (this.opts?.crosshairWidthUnit === "percent") {
33083
+ radius = range[0] * this.opts.crosshairWidth * 0.5 * 0.01;
33084
+ }
33085
+ if (this.opts?.crosshairWidthUnit === "mm") {
33086
+ radius = this.opts.crosshairWidth * 0.5;
33087
+ }
32937
33088
  if (this.crosshairs3D === null || this.crosshairs3D.mm[0] !== mm[0] || this.crosshairs3D.mm[1] !== mm[1] || this.crosshairs3D.mm[2] !== mm[2]) {
32938
33089
  if (this.crosshairs3D !== null) {
32939
33090
  gl.deleteBuffer(this.crosshairs3D.indexBuffer);
32940
33091
  gl.deleteBuffer(this.crosshairs3D.vertexBuffer);
32941
33092
  }
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
33093
  this.crosshairs3D = NiivueObject3D.generateCrosshairs(this.gl, 1, mm, mn, mx, radius, 20, this.opts.crosshairGap);
32954
33094
  this.crosshairs3D.mm = mm;
32955
33095
  }
@@ -32968,6 +33108,7 @@ var Niivue = class {
32968
33108
  );
32969
33109
  }
32970
33110
  gl.uniformMatrix4fv(crosshairsShader.uniforms.mvpMtx, false, mvpMtx);
33111
+ gl.disable(gl.CULL_FACE);
32971
33112
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.crosshairs3D.indexBuffer);
32972
33113
  gl.enable(gl.DEPTH_TEST);
32973
33114
  const color = [...this.opts.crosshairColor];
@@ -33118,9 +33259,9 @@ var Niivue = class {
33118
33259
  }
33119
33260
  // not included in public docs
33120
33261
  // 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;
33262
+ scaleSlice(w, h, padPixelsWH = [0, 0], canvasWH = [0, 0]) {
33263
+ const canvasW = canvasWH[0] === 0 ? this.effectiveCanvasWidth() - padPixelsWH[0] : canvasWH[0] - padPixelsWH[0];
33264
+ const canvasH = canvasWH[1] === 0 ? this.effectiveCanvasHeight() - padPixelsWH[1] : canvasWH[1] - padPixelsWH[1];
33124
33265
  let scalePix = canvasW / w;
33125
33266
  if (h * scalePix > canvasH) {
33126
33267
  scalePix = canvasH / h;
@@ -33569,7 +33710,6 @@ var Niivue = class {
33569
33710
  if (this.volumes.length === 0 || typeof this.volumes[0].dims === "undefined") {
33570
33711
  if (this.meshes.length > 0) {
33571
33712
  this.screenSlices = [];
33572
- this.opts.sliceType = 4 /* RENDER */;
33573
33713
  this.draw3D();
33574
33714
  if (this.opts.isColorbar) {
33575
33715
  this.drawColorbar();
@@ -33613,6 +33753,8 @@ var Niivue = class {
33613
33753
  if (this.sliceMosaicString.length > 0) {
33614
33754
  this.drawMosaic(this.sliceMosaicString);
33615
33755
  } else {
33756
+ const heroImageWH = [0, 0];
33757
+ let isHeroImage = false;
33616
33758
  this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
33617
33759
  this.screenSlices = [];
33618
33760
  if (this.opts.sliceType === 0 /* AXIAL */) {
@@ -33622,6 +33764,9 @@ var Niivue = class {
33622
33764
  } else if (this.opts.sliceType === 2 /* SAGITTAL */) {
33623
33765
  this.draw2D([0, 0, 0, 0], 2);
33624
33766
  } else {
33767
+ let padPixelsWH = function(cols, rows) {
33768
+ return [(cols - 1) * pad + cols * innerPad, (rows - 1) * pad + rows * innerPad];
33769
+ };
33625
33770
  let isShowRender = false;
33626
33771
  if (this.opts.multiplanarForceRender) {
33627
33772
  isShowRender = true;
@@ -33648,20 +33793,42 @@ var Niivue = class {
33648
33793
  log.debug("multiplanarPadPixels must be numeric");
33649
33794
  }
33650
33795
  const pad = parseFloat(`${this.opts.multiplanarPadPixels}`);
33651
- const ltwh2x2 = this.scaleSlice(volScale[0] + volScale[1], volScale[1] + volScale[2], pad * 1, pad * 1);
33796
+ let innerPad = this.opts.tileMargin;
33797
+ if (innerPad < 0) {
33798
+ innerPad = 2 * (2 + Math.ceil(Math.max(this.opts.textHeight, 0.01) * Math.min(this.gl.canvas.height, this.gl.canvas.width)));
33799
+ }
33800
+ let canvasWH = [this.effectiveCanvasWidth(), this.effectiveCanvasHeight()];
33801
+ if (this.opts.heroImageFraction > 0 && this.opts.heroImageFraction < 1) {
33802
+ isShowRender = false;
33803
+ isHeroImage = true;
33804
+ if (canvasWH[0] > canvasWH[1] && this.opts.multiplanarLayout !== 3 /* ROW */) {
33805
+ heroImageWH[0] = canvasWH[0] * this.opts.heroImageFraction;
33806
+ } else {
33807
+ heroImageWH[1] = canvasWH[1] * this.opts.heroImageFraction;
33808
+ }
33809
+ canvasWH = [canvasWH[0] - heroImageWH[0], canvasWH[1] - heroImageWH[1]];
33810
+ }
33811
+ const ltwh2x2 = this.scaleSlice(
33812
+ volScale[0] + volScale[1],
33813
+ volScale[1] + volScale[2],
33814
+ padPixelsWH(2, 2),
33815
+ canvasWH
33816
+ );
33652
33817
  const mx = Math.max(Math.max(volScale[1], volScale[2]), volScale[0]);
33653
33818
  const ltwh3x1 = this.scaleSlice(
33654
33819
  volScale[0] + volScale[0] + volScale[1],
33655
33820
  Math.max(volScale[1], volScale[2]),
33656
- pad * 2
33821
+ padPixelsWH(3, 1),
33822
+ canvasWH
33657
33823
  );
33658
33824
  const ltwh4x1 = this.scaleSlice(
33659
33825
  volScale[0] + volScale[0] + volScale[1] + mx,
33660
33826
  Math.max(volScale[1], volScale[2]),
33661
- pad * 3
33827
+ padPixelsWH(4, 1),
33828
+ canvasWH
33662
33829
  );
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);
33830
+ const ltwh1x3 = this.scaleSlice(mx, volScale[1] + volScale[2] + volScale[2], padPixelsWH(1, 3), canvasWH);
33831
+ const ltwh1x4 = this.scaleSlice(mx, volScale[1] + volScale[2] + volScale[2] + mx, padPixelsWH(1, 4), canvasWH);
33665
33832
  let isDraw3D = !isDrawPenDown && (maxVols < 2 || !isDrawGraph);
33666
33833
  let isDrawColumn = false;
33667
33834
  let isDrawGrid = false;
@@ -33684,22 +33851,39 @@ var Niivue = class {
33684
33851
  let ltwh = ltwh2x2;
33685
33852
  if (isDrawColumn) {
33686
33853
  ltwh = ltwh1x3;
33687
- if (isShowRender || this.opts.multiplanarShowRender === 2 /* AUTO */ && ltwh1x4[4] >= ltwh1x3[4]) {
33854
+ if (!isHeroImage && (isShowRender || this.opts.multiplanarShowRender === 2 /* AUTO */ && ltwh1x4[4] >= ltwh1x3[4])) {
33688
33855
  ltwh = ltwh1x4;
33689
33856
  } else {
33690
33857
  isDraw3D = false;
33691
33858
  }
33692
33859
  } else if (isDrawRow) {
33693
33860
  ltwh = ltwh3x1;
33694
- if (isShowRender || this.opts.multiplanarShowRender === 2 /* AUTO */ && ltwh4x1[4] >= ltwh3x1[4]) {
33861
+ if (!isHeroImage && (isShowRender || this.opts.multiplanarShowRender === 2 /* AUTO */ && ltwh4x1[4] >= ltwh3x1[4])) {
33695
33862
  ltwh = ltwh4x1;
33696
33863
  } else {
33697
33864
  isDraw3D = false;
33698
33865
  }
33699
33866
  }
33700
- const sX = volScale[0] * ltwh[4];
33701
- const sY = volScale[1] * ltwh[4];
33702
- const sZ = volScale[2] * ltwh[4];
33867
+ if (isHeroImage) {
33868
+ const heroW = heroImageWH[0] === 0 ? this.effectiveCanvasWidth() : heroImageWH[0];
33869
+ const heroH = heroImageWH[1] === 0 ? this.effectiveCanvasHeight() : heroImageWH[1];
33870
+ if (this.opts?.heroSliceType === 0 /* AXIAL */ || this.opts?.heroSliceType === 1 /* CORONAL */ || this.opts?.heroSliceType === 2 /* SAGITTAL */) {
33871
+ this.draw2D([0, 0, heroW, heroH], this.opts.heroSliceType, NaN, [Infinity, Infinity]);
33872
+ } else {
33873
+ const ltwh2 = ltwh.slice();
33874
+ const canvasW = this.effectiveCanvasWidth();
33875
+ if (heroW === canvasW) {
33876
+ ltwh2[0] = 0;
33877
+ }
33878
+ this.draw3D([ltwh2[0], 0, heroW, heroH]);
33879
+ }
33880
+ ltwh[0] += heroImageWH[0];
33881
+ ltwh[1] += heroImageWH[1];
33882
+ isDraw3D = false;
33883
+ }
33884
+ const sX = volScale[0] * ltwh[4] + innerPad;
33885
+ const sY = volScale[1] * ltwh[4] + innerPad;
33886
+ const sZ = volScale[2] * ltwh[4] + innerPad;
33703
33887
  const actualX = actualScale[0] * ltwh[4];
33704
33888
  const actualY = actualScale[1] * ltwh[4];
33705
33889
  const actualZ = actualScale[2] * ltwh[4];
@@ -33726,6 +33910,9 @@ var Niivue = class {
33726
33910
  if (this.opts.multiplanarShowRender === 2 /* AUTO */) {
33727
33911
  isDraw3D = true;
33728
33912
  }
33913
+ if (isHeroImage) {
33914
+ isDraw3D = false;
33915
+ }
33729
33916
  this.draw2D([ltwh[0], ltwh[1] + sZ + pad, sX, sY], 0, NaN, [actualX, actualY]);
33730
33917
  this.draw2D([ltwh[0], ltwh[1], sX, sZ], 1, NaN, [actualX, actualZ]);
33731
33918
  this.draw2D([ltwh[0] + sX + pad, ltwh[1], sY, sZ], 2, NaN, [actualY, actualZ]);