@niivue/niivue 0.54.0 → 0.56.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
@@ -723,6 +723,7 @@ type NVConfigOptions = {
723
723
  yoke3Dto2DZoom: boolean;
724
724
  isDepthPickMesh: boolean;
725
725
  isCornerOrientationText: boolean;
726
+ isOrientationTextVisible: boolean;
726
727
  heroImageFraction: number;
727
728
  heroSliceType: SLICE_TYPE;
728
729
  sagittalNoseLeft: boolean;
@@ -1701,6 +1702,11 @@ declare class Niivue {
1701
1702
  VOLUME_ID: number;
1702
1703
  DISTANCE_FROM_CAMERA: number;
1703
1704
  graph: Graph;
1705
+ customLayout: Array<{
1706
+ sliceType: SLICE_TYPE;
1707
+ position: [number, number, number, number];
1708
+ sliceMM?: number;
1709
+ }>;
1704
1710
  meshShaders: Array<{
1705
1711
  Name: string;
1706
1712
  Frag: string;
@@ -2113,6 +2119,12 @@ declare class Niivue {
2113
2119
  * @see {@link https://niivue.github.io/niivue/features/worldspace2.html | live demo usage}
2114
2120
  */
2115
2121
  setCornerOrientationText(isCornerOrientationText: boolean): void;
2122
+ /**
2123
+ * determine if orientation text appears in 2D slice view.
2124
+ * @param isOrientationTextVisible - controls position of text
2125
+ * @example niivue.setIsOrientationTextVisible(false)
2126
+ */
2127
+ setIsOrientationTextVisible(isOrientationTextVisible: boolean): void;
2116
2128
  /**
2117
2129
  * determine proportion of screen real estate devoted to rendering in multiplanar view.
2118
2130
  * @param fraction - proportion of screen devoted to primary (hero) image (0 to disable)
@@ -2120,6 +2132,50 @@ declare class Niivue {
2120
2132
  * @see {@link https://niivue.github.io/niivue/features/layout.html | live demo usage}
2121
2133
  */
2122
2134
  setHeroImage(fraction: number): void;
2135
+ /**
2136
+ * Set a custom slice layout. This overrides the built-in layouts.
2137
+ * @param layout - Array of layout specifications for each slice view
2138
+ * @example
2139
+ * niivue.setCustomLayout([
2140
+ * // Left 50% - Sag
2141
+ * {sliceType: 2, position: [0, 0, 0.5, 1.0]},
2142
+ * // Top right - Cor
2143
+ * {sliceType: 1, position: [0.5, 0, 0.5, 0.5]},
2144
+ * // Bottom right - Ax
2145
+ * {sliceType: 0, position: [0.5, 0.5, 0.5, 0.5]}
2146
+ * ])
2147
+ *
2148
+ * produces:
2149
+ * +----------------+----------------+
2150
+ * | | |
2151
+ * | | coronal |
2152
+ * | | |
2153
+ * | | |
2154
+ * | sagittal +----------------+
2155
+ * | | |
2156
+ * | | axial |
2157
+ * | | |
2158
+ * | | |
2159
+ * +----------------+----------------+
2160
+ */
2161
+ setCustomLayout(layout: Array<{
2162
+ sliceType: SLICE_TYPE;
2163
+ position: [number, number, number, number];
2164
+ sliceMM?: number;
2165
+ }>): void;
2166
+ /**
2167
+ * Clear custom layout and rely on built-in layouts
2168
+ */
2169
+ clearCustomLayout(): void;
2170
+ /**
2171
+ * Get the current custom layout if set
2172
+ * @returns The current custom layout or null if using built-in layouts
2173
+ */
2174
+ getCustomLayout(): Array<{
2175
+ sliceType: SLICE_TYPE;
2176
+ position: [number, number, number, number];
2177
+ sliceMM?: number;
2178
+ }> | null;
2123
2179
  /**
2124
2180
  * control whether 2D slices use radiological or neurological convention.
2125
2181
  * @param isRadiologicalConvention - new display convention
@@ -3062,6 +3118,7 @@ declare class Niivue {
3062
3118
  * @see {@link https://niivue.github.io/niivue/features/mosaics.html | live demo usage}
3063
3119
  */
3064
3120
  drawMosaic(mosaicStr: string): void;
3121
+ calculateWidthHeight(sliceType: number, volScale: number[], containerWidth: number, containerHeight: number): [number, number];
3065
3122
  drawSceneCore(): string | void;
3066
3123
  drawScene(): string | void;
3067
3124
  get gl(): WebGL2RenderingContext;
package/build/index.js CHANGED
@@ -17,7 +17,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
17
17
  import { mat4 as mat49, vec2, vec3 as vec314, vec4 as vec49 } from "gl-matrix";
18
18
 
19
19
  // package.json
20
- var version = "0.54.0";
20
+ var version = "0.56.0";
21
21
 
22
22
  // src/logger.ts
23
23
  var _Log = class _Log {
@@ -20431,6 +20431,7 @@ var DEFAULT_OPTIONS = {
20431
20431
  yoke3Dto2DZoom: false,
20432
20432
  isDepthPickMesh: false,
20433
20433
  isCornerOrientationText: false,
20434
+ isOrientationTextVisible: true,
20434
20435
  heroImageFraction: 0,
20435
20436
  heroSliceType: 4 /* RENDER */,
20436
20437
  sagittalNoseLeft: false,
@@ -25165,6 +25166,7 @@ var Niivue = class {
25165
25166
  normalizeValues: false,
25166
25167
  isRangeCalMinMax: false
25167
25168
  });
25169
+ __publicField(this, "customLayout", []);
25168
25170
  __publicField(this, "meshShaders", [
25169
25171
  {
25170
25172
  Name: "Phong",
@@ -27100,6 +27102,15 @@ var Niivue = class {
27100
27102
  this.opts.isCornerOrientationText = isCornerOrientationText;
27101
27103
  this.updateGLVolume();
27102
27104
  }
27105
+ /**
27106
+ * determine if orientation text appears in 2D slice view.
27107
+ * @param isOrientationTextVisible - controls position of text
27108
+ * @example niivue.setIsOrientationTextVisible(false)
27109
+ */
27110
+ setIsOrientationTextVisible(isOrientationTextVisible) {
27111
+ this.opts.isOrientationTextVisible = isOrientationTextVisible;
27112
+ this.drawScene();
27113
+ }
27103
27114
  /**
27104
27115
  * determine proportion of screen real estate devoted to rendering in multiplanar view.
27105
27116
  * @param fraction - proportion of screen devoted to primary (hero) image (0 to disable)
@@ -27110,6 +27121,65 @@ var Niivue = class {
27110
27121
  this.opts.heroImageFraction = fraction;
27111
27122
  this.drawScene();
27112
27123
  }
27124
+ /**
27125
+ * Set a custom slice layout. This overrides the built-in layouts.
27126
+ * @param layout - Array of layout specifications for each slice view
27127
+ * @example
27128
+ * niivue.setCustomLayout([
27129
+ * // Left 50% - Sag
27130
+ * {sliceType: 2, position: [0, 0, 0.5, 1.0]},
27131
+ * // Top right - Cor
27132
+ * {sliceType: 1, position: [0.5, 0, 0.5, 0.5]},
27133
+ * // Bottom right - Ax
27134
+ * {sliceType: 0, position: [0.5, 0.5, 0.5, 0.5]}
27135
+ * ])
27136
+ *
27137
+ * produces:
27138
+ * +----------------+----------------+
27139
+ * | | |
27140
+ * | | coronal |
27141
+ * | | |
27142
+ * | | |
27143
+ * | sagittal +----------------+
27144
+ * | | |
27145
+ * | | axial |
27146
+ * | | |
27147
+ * | | |
27148
+ * +----------------+----------------+
27149
+ */
27150
+ setCustomLayout(layout) {
27151
+ for (let i = 0; i < layout.length; i++) {
27152
+ const [left1, top1, width1, height1] = layout[i].position;
27153
+ const right1 = left1 + width1;
27154
+ const bottom1 = top1 + height1;
27155
+ for (let j = i + 1; j < layout.length; j++) {
27156
+ const [left2, top2, width2, height2] = layout[j].position;
27157
+ const right2 = left2 + width2;
27158
+ const bottom2 = top2 + height2;
27159
+ const horizontallyOverlaps = left1 < right2 && right1 > left2;
27160
+ const verticallyOverlaps = top1 < bottom2 && bottom1 > top2;
27161
+ if (horizontallyOverlaps && verticallyOverlaps) {
27162
+ throw new Error(`Custom layout is invalid. Tile ${i} overlaps with tile ${j}.`);
27163
+ }
27164
+ }
27165
+ }
27166
+ this.customLayout = layout;
27167
+ this.drawScene();
27168
+ }
27169
+ /**
27170
+ * Clear custom layout and rely on built-in layouts
27171
+ */
27172
+ clearCustomLayout() {
27173
+ this.customLayout = null;
27174
+ this.drawScene();
27175
+ }
27176
+ /**
27177
+ * Get the current custom layout if set
27178
+ * @returns The current custom layout or null if using built-in layouts
27179
+ */
27180
+ getCustomLayout() {
27181
+ return this.customLayout;
27182
+ }
27113
27183
  /**
27114
27184
  * control whether 2D slices use radiological or neurological convention.
27115
27185
  * @param isRadiologicalConvention - new display convention
@@ -33481,7 +33551,7 @@ var Niivue = class {
33481
33551
  if (isNaN(customMM)) {
33482
33552
  this.drawCrosshairs3D(false, 0.15, obj.modelViewProjectionMatrix, true, this.opts.isSliceMM);
33483
33553
  }
33484
- if (isStretchToScreen) {
33554
+ if (isStretchToScreen && this.opts.isOrientationTextVisible) {
33485
33555
  this.drawSliceOrientationText(leftTopWidthHeight, axCorSag);
33486
33556
  }
33487
33557
  this.readyForSync = true;
@@ -33515,7 +33585,7 @@ var Niivue = class {
33515
33585
  if (customMM === Infinity || customMM === -Infinity || axCorSag === 4 /* RENDER */) {
33516
33586
  return;
33517
33587
  }
33518
- if (leftTopWidthHeight[2] !== 0 && leftTopWidthHeight[3] !== 0) {
33588
+ if (leftTopWidthHeight[2] !== 0 && leftTopWidthHeight[3] !== 0 && this.opts.isOrientationTextVisible) {
33519
33589
  this.drawSliceOrientationText(leftTopWidthHeight, axCorSag, padLeftTop);
33520
33590
  }
33521
33591
  }
@@ -35173,6 +35243,36 @@ var Niivue = class {
35173
35243
  }
35174
35244
  this.opts.textHeight = labelSize;
35175
35245
  }
35246
+ calculateWidthHeight(sliceType, volScale, containerWidth, containerHeight) {
35247
+ let xScale, yScale;
35248
+ switch (sliceType) {
35249
+ case 0 /* AXIAL */:
35250
+ xScale = volScale[0];
35251
+ yScale = volScale[1];
35252
+ break;
35253
+ case 1 /* CORONAL */:
35254
+ xScale = volScale[0];
35255
+ yScale = volScale[2];
35256
+ break;
35257
+ case 2 /* SAGITTAL */:
35258
+ xScale = volScale[1];
35259
+ yScale = volScale[2];
35260
+ break;
35261
+ default:
35262
+ return [containerWidth, containerHeight];
35263
+ }
35264
+ const aspectRatio = xScale / yScale;
35265
+ const containerAspect = containerWidth / containerHeight;
35266
+ let actualWidth, actualHeight;
35267
+ if (aspectRatio > containerAspect) {
35268
+ actualWidth = containerWidth;
35269
+ actualHeight = containerWidth / aspectRatio;
35270
+ } else {
35271
+ actualHeight = containerHeight;
35272
+ actualWidth = containerHeight * aspectRatio;
35273
+ }
35274
+ return [actualWidth, actualHeight];
35275
+ }
35176
35276
  // not included in public docs
35177
35277
  drawSceneCore() {
35178
35278
  if (!this.initialized) {
@@ -35198,9 +35298,6 @@ var Niivue = class {
35198
35298
  this.drawLoadingText(this.opts.loadingText);
35199
35299
  return;
35200
35300
  }
35201
- if (this.back === null) {
35202
- return;
35203
- }
35204
35301
  if (this.uiData.isDragging && this.scene.clipPlaneDepthAziElev[0] < 1.8 && this.inRenderTile(this.uiData.dragStart[0], this.uiData.dragStart[1]) >= 0) {
35205
35302
  const x = this.uiData.dragStart[0] - this.uiData.dragEnd[0];
35206
35303
  const y = this.uiData.dragStart[1] - this.uiData.dragEnd[1];
@@ -35236,7 +35333,43 @@ var Niivue = class {
35236
35333
  let isHeroImage = false;
35237
35334
  this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
35238
35335
  this.screenSlices = [];
35239
- if (this.opts.sliceType === 0 /* AXIAL */) {
35336
+ if (this.customLayout && this.customLayout.length > 0) {
35337
+ this.screenSlices = [];
35338
+ this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
35339
+ const { volScale } = this.sliceScale();
35340
+ const canvasWH = [this.effectiveCanvasWidth(), this.effectiveCanvasHeight()];
35341
+ for (const view of this.customLayout) {
35342
+ const { sliceType, position, sliceMM } = view;
35343
+ const leftTopWidthHeight = position.slice();
35344
+ if (position[0] >= 0 && position[0] <= 1 && position[2] <= 1) {
35345
+ leftTopWidthHeight[0] = position[0] * canvasWH[0];
35346
+ leftTopWidthHeight[2] = position[2] * canvasWH[0];
35347
+ }
35348
+ if (position[1] >= 0 && position[1] <= 1 && position[3] <= 1) {
35349
+ leftTopWidthHeight[1] = position[1] * canvasWH[1];
35350
+ leftTopWidthHeight[3] = position[3] * canvasWH[1];
35351
+ }
35352
+ if (leftTopWidthHeight[0] + leftTopWidthHeight[2] > canvasWH[0]) {
35353
+ log.warn("adjusting slice width because it would have been clipped");
35354
+ leftTopWidthHeight[2] = canvasWH[0] - leftTopWidthHeight[0];
35355
+ }
35356
+ if (leftTopWidthHeight[1] + leftTopWidthHeight[3] > canvasWH[1]) {
35357
+ log.warn("adjusting slice height because it would have been clipped");
35358
+ leftTopWidthHeight[3] = canvasWH[1] - leftTopWidthHeight[1];
35359
+ }
35360
+ if (sliceType === 4 /* RENDER */) {
35361
+ this.draw3D(leftTopWidthHeight);
35362
+ } else if (sliceType === 0 /* AXIAL */ || sliceType === 1 /* CORONAL */ || sliceType === 2 /* SAGITTAL */) {
35363
+ const actualDimensions = this.calculateWidthHeight(
35364
+ sliceType,
35365
+ volScale,
35366
+ leftTopWidthHeight[2],
35367
+ leftTopWidthHeight[3]
35368
+ );
35369
+ this.draw2D(leftTopWidthHeight, sliceType, sliceMM ?? NaN, actualDimensions);
35370
+ }
35371
+ }
35372
+ } else if (this.opts.sliceType === 0 /* AXIAL */) {
35240
35373
  this.draw2D([0, 0, 0, 0], 0);
35241
35374
  } else if (this.opts.sliceType === 1 /* CORONAL */) {
35242
35375
  this.draw2D([0, 0, 0, 0], 1);