@niivue/niivue
Version:
minimal webgl2 nifti image viewer
295 lines (257 loc) • 9.34 kB
text/typescript
/**
* Clip plane manager helper functions for 3D clipping plane operations.
* This module provides pure functions for clip plane calculations and management.
*
*/
import { sph2cartDeg } from '../rendering/VolumeRenderer.js'
/**
* Default clip plane values when no clipping is active.
* The depth value of 2 effectively disables clipping.
*/
export const DEFAULT_CLIP_PLANE: [number, number, number, number] = [0, 0, 0, 2]
/**
* Default depth/azimuth/elevation values when no clipping is active.
* Depth of 2 means no clip plane.
*/
export const DEFAULT_DEPTH_AZI_ELEV: [number, number, number] = [2, 0, 0]
/**
* Threshold for determining if a clip plane is active.
* If depth < this value, the clip plane is considered active.
*/
export const CLIP_PLANE_ACTIVE_THRESHOLD = 1.8
/**
* Parameters for converting depth/azimuth/elevation to clip plane format
*/
export interface DepthAziElevToClipPlaneParams {
depth: number
azimuth: number
elevation: number
}
/**
* Convert depth/azimuth/elevation to clip plane [x, y, z, d] format.
* Uses spherical to Cartesian conversion with 180-degree azimuth offset.
* @param params - Depth, azimuth, and elevation values
* @returns Clip plane as [x, y, z, depth] array
*/
export function depthAziElevToClipPlane(params: DepthAziElevToClipPlaneParams): [number, number, number, number] {
const { depth, azimuth, elevation } = params
const v = sph2cartDeg(azimuth + 180, elevation)
return [v[0], v[1], v[2], depth]
}
/**
* Parameters for converting depth/azimuth/elevation for setClipPlanes (no 180-degree offset)
*/
export interface DepthAziElevToClipPlaneNoOffsetParams {
depth: number
azimuth: number
elevation: number
}
/**
* Convert depth/azimuth/elevation to clip plane format without 180-degree offset.
* Used by setClipPlanes which expects a different orientation convention.
* @param params - Depth, azimuth, and elevation values
* @returns Clip plane as [x, y, z, -depth] array (note: depth is negated for shader)
*/
export function depthAziElevToClipPlaneNoOffset(params: DepthAziElevToClipPlaneNoOffsetParams): [number, number, number, number] {
const { depth, azimuth, elevation } = params
const n = sph2cartDeg(azimuth, elevation)
return [n[0], n[1], n[2], -depth] // depth negated for shader
}
/**
* Parameters for calculating clip plane drag rotation
*/
export interface CalculateClipPlaneDragParams {
startDepthAziElev: number[]
dragDeltaX: number
dragDeltaY: number
}
/**
* Result of clip plane drag calculation
*/
export interface ClipPlaneDragResult {
depthAziElev: [number, number, number]
changed: boolean
}
/**
* Calculate new depth/azimuth/elevation from drag deltas.
* Azimuth is adjusted by -deltaX and wrapped to 0-360.
* Elevation is adjusted by +deltaY (unclamped).
* @param params - Starting values and drag deltas
* @returns New depth/azimuth/elevation and whether values changed
*/
export function calculateClipPlaneDrag(params: CalculateClipPlaneDragParams): ClipPlaneDragResult {
const { startDepthAziElev, dragDeltaX, dragDeltaY } = params
const newDepthAziElev: [number, number, number] = [startDepthAziElev[0], (startDepthAziElev[1] - dragDeltaX) % 360, startDepthAziElev[2] + dragDeltaY]
const changed = newDepthAziElev[1] !== startDepthAziElev[1] || newDepthAziElev[2] !== startDepthAziElev[2]
return {
depthAziElev: newDepthAziElev,
changed
}
}
/**
* Parameters for ensuring clip plane arrays exist
*/
export interface EnsureClipPlaneArraysParams {
clipPlanes: number[][] | undefined
clipPlaneDepthAziElevs: number[][] | undefined
index: number
}
/**
* Result of ensuring clip plane arrays
*/
export interface EnsureClipPlaneArraysResult {
clipPlanes: number[][]
clipPlaneDepthAziElevs: number[][]
}
/**
* Ensure clip plane arrays exist and have enough elements for the given index.
* Creates arrays if they don't exist and fills with default values up to the index.
* @param params - Current arrays and required index
* @returns Updated arrays guaranteed to have elements up to index
*/
export function ensureClipPlaneArrays(params: EnsureClipPlaneArraysParams): EnsureClipPlaneArraysResult {
const { index } = params
const clipPlanes = params.clipPlanes ? [...params.clipPlanes] : []
const clipPlaneDepthAziElevs = params.clipPlaneDepthAziElevs ? [...params.clipPlaneDepthAziElevs] : []
while (clipPlanes.length <= index) {
clipPlanes.push([...DEFAULT_CLIP_PLANE])
}
while (clipPlaneDepthAziElevs.length <= index) {
clipPlaneDepthAziElevs.push([...DEFAULT_DEPTH_AZI_ELEV])
}
return {
clipPlanes,
clipPlaneDepthAziElevs
}
}
/**
* Check if a clip plane is active (depth below threshold).
* @param depth - The depth value of the clip plane
* @param threshold - Optional custom threshold (default: 1.8)
* @returns True if the clip plane is active
*/
export function isClipPlaneActive(depth: number, threshold = CLIP_PLANE_ACTIVE_THRESHOLD): boolean {
return depth < threshold
}
/**
* Parameters for converting multiple depth/azimuth/elevation arrays to clip planes
*/
export interface ConvertMultipleClipPlanesParams {
depthAziElevs: number[][]
}
/**
* Result of converting multiple clip planes
*/
export interface ConvertMultipleClipPlanesResult {
clipPlanes: number[][]
clipPlaneDepthAziElevs: number[][]
}
/**
* Convert an array of depth/azimuth/elevation values to clip plane format.
* Used by setClipPlanes to batch-process multiple clip planes.
* @param params - Array of depth/azimuth/elevation values
* @returns Parallel arrays of clip planes and their depth/azi/elev values
*/
export function convertMultipleClipPlanes(params: ConvertMultipleClipPlanesParams): ConvertMultipleClipPlanesResult {
const { depthAziElevs } = params
const clipPlanes: number[][] = []
const clipPlaneDepthAziElevs: number[][] = []
for (const dae of depthAziElevs) {
const plane = depthAziElevToClipPlaneNoOffset({
depth: dae[0],
azimuth: dae[1],
elevation: dae[2]
})
clipPlanes.push(plane)
clipPlaneDepthAziElevs.push([...dae])
}
return {
clipPlanes,
clipPlaneDepthAziElevs
}
}
/**
* Parameters for checking if clip plane drag should update the view
*/
export interface ShouldUpdateClipPlaneDragParams {
isDragging: boolean
activeClipPlaneDepth: number
dragStartTileIndex: number
}
/**
* Check if clip plane drag should update the view.
* @param params - Dragging state, clip plane depth, and drag start tile index
* @returns True if clip plane drag should be processed
*/
export function shouldUpdateClipPlaneDrag(params: ShouldUpdateClipPlaneDragParams): boolean {
const { isDragging, activeClipPlaneDepth, dragStartTileIndex } = params
return isDragging && isClipPlaneActive(activeClipPlaneDepth) && dragStartTileIndex >= 0
}
/**
* Create a default clip plane (disabled state).
* @returns Default clip plane array [0, 0, 0, 2]
*/
export function createDefaultClipPlane(): [number, number, number, number] {
return [...DEFAULT_CLIP_PLANE]
}
/**
* Create default depth/azimuth/elevation values (disabled state).
* @returns Default depth/azi/elev array [2, 0, 0]
*/
export function createDefaultDepthAziElev(): [number, number, number] {
return [...DEFAULT_DEPTH_AZI_ELEV]
}
/**
* Parameters for updating a single clip plane at a given index
*/
export interface UpdateClipPlaneAtIndexParams {
clipPlanes: number[][]
clipPlaneDepthAziElevs: number[][]
index: number
depthAzimuthElevation: number[]
}
/**
* Result of updating a clip plane at an index
*/
export interface UpdateClipPlaneAtIndexResult {
clipPlanes: number[][]
clipPlaneDepthAziElevs: number[][]
clipPlane: [number, number, number, number]
}
/**
* Update a clip plane at a specific index.
* Ensures arrays are large enough, then updates the values.
* @param params - Current arrays, index, and new depth/azimuth/elevation
* @returns Updated arrays and the computed clip plane
*/
export function updateClipPlaneAtIndex(params: UpdateClipPlaneAtIndexParams): UpdateClipPlaneAtIndexResult {
const { index, depthAzimuthElevation } = params
// Ensure arrays are large enough
const { clipPlanes, clipPlaneDepthAziElevs } = ensureClipPlaneArrays({
clipPlanes: params.clipPlanes,
clipPlaneDepthAziElevs: params.clipPlaneDepthAziElevs,
index
})
// Calculate the clip plane
const clipPlane = depthAziElevToClipPlane({
depth: depthAzimuthElevation[0],
azimuth: depthAzimuthElevation[1],
elevation: depthAzimuthElevation[2]
})
// Update the arrays
clipPlanes[index] = clipPlane
clipPlaneDepthAziElevs[index] = [...depthAzimuthElevation]
return {
clipPlanes,
clipPlaneDepthAziElevs,
clipPlane
}
}
/**
* Validate depth/azimuth/elevation input.
* @param depthAzimuthElevation - Input to validate
* @returns True if valid (non-empty array)
*/
export function isValidDepthAziElev(depthAzimuthElevation: number[] | undefined | null): boolean {
return !!depthAzimuthElevation && depthAzimuthElevation.length > 0
}