@niivue/niivue 0.55.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
@@ -1702,6 +1702,11 @@ declare class Niivue {
1702
1702
  VOLUME_ID: number;
1703
1703
  DISTANCE_FROM_CAMERA: number;
1704
1704
  graph: Graph;
1705
+ customLayout: Array<{
1706
+ sliceType: SLICE_TYPE;
1707
+ position: [number, number, number, number];
1708
+ sliceMM?: number;
1709
+ }>;
1705
1710
  meshShaders: Array<{
1706
1711
  Name: string;
1707
1712
  Frag: string;
@@ -2127,6 +2132,50 @@ declare class Niivue {
2127
2132
  * @see {@link https://niivue.github.io/niivue/features/layout.html | live demo usage}
2128
2133
  */
2129
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;
2130
2179
  /**
2131
2180
  * control whether 2D slices use radiological or neurological convention.
2132
2181
  * @param isRadiologicalConvention - new display convention
@@ -3069,6 +3118,7 @@ declare class Niivue {
3069
3118
  * @see {@link https://niivue.github.io/niivue/features/mosaics.html | live demo usage}
3070
3119
  */
3071
3120
  drawMosaic(mosaicStr: string): void;
3121
+ calculateWidthHeight(sliceType: number, volScale: number[], containerWidth: number, containerHeight: number): [number, number];
3072
3122
  drawSceneCore(): string | void;
3073
3123
  drawScene(): string | void;
3074
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.55.0";
20
+ var version = "0.56.0";
21
21
 
22
22
  // src/logger.ts
23
23
  var _Log = class _Log {
@@ -25166,6 +25166,7 @@ var Niivue = class {
25166
25166
  normalizeValues: false,
25167
25167
  isRangeCalMinMax: false
25168
25168
  });
25169
+ __publicField(this, "customLayout", []);
25169
25170
  __publicField(this, "meshShaders", [
25170
25171
  {
25171
25172
  Name: "Phong",
@@ -27120,6 +27121,65 @@ var Niivue = class {
27120
27121
  this.opts.heroImageFraction = fraction;
27121
27122
  this.drawScene();
27122
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
+ }
27123
27183
  /**
27124
27184
  * control whether 2D slices use radiological or neurological convention.
27125
27185
  * @param isRadiologicalConvention - new display convention
@@ -35183,6 +35243,36 @@ var Niivue = class {
35183
35243
  }
35184
35244
  this.opts.textHeight = labelSize;
35185
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
+ }
35186
35276
  // not included in public docs
35187
35277
  drawSceneCore() {
35188
35278
  if (!this.initialized) {
@@ -35208,9 +35298,6 @@ var Niivue = class {
35208
35298
  this.drawLoadingText(this.opts.loadingText);
35209
35299
  return;
35210
35300
  }
35211
- if (this.back === null) {
35212
- return;
35213
- }
35214
35301
  if (this.uiData.isDragging && this.scene.clipPlaneDepthAziElev[0] < 1.8 && this.inRenderTile(this.uiData.dragStart[0], this.uiData.dragStart[1]) >= 0) {
35215
35302
  const x = this.uiData.dragStart[0] - this.uiData.dragEnd[0];
35216
35303
  const y = this.uiData.dragStart[1] - this.uiData.dragEnd[1];
@@ -35246,7 +35333,43 @@ var Niivue = class {
35246
35333
  let isHeroImage = false;
35247
35334
  this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
35248
35335
  this.screenSlices = [];
35249
- 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 */) {
35250
35373
  this.draw2D([0, 0, 0, 0], 0);
35251
35374
  } else if (this.opts.sliceType === 1 /* CORONAL */) {
35252
35375
  this.draw2D([0, 0, 0, 0], 1);