@niivue/niivue
Version:
minimal webgl2 nifti image viewer
293 lines (266 loc) • 8.59 kB
text/typescript
/**
* Volume management functions for handling volume arrays and core volume operations.
* This module provides pure functions for volume array manipulation.
*
* Related modules:
* - VolumeTexture.ts - Texture-related operations
* - VolumeColormap.ts - Colormap operations
* - VolumeModulation.ts - Modulation operations
*/
import { NVImage } from '@/nvimage'
/**
* Result of adding a volume
*/
export interface AddVolumeResult {
volumes: NVImage[]
index: number
}
/**
* Result of removing a volume
*/
export interface RemoveVolumeResult {
volumes: NVImage[]
removed: NVImage | null
}
/**
* Result of setting/reordering a volume
*/
export interface SetVolumeResult {
volumes: NVImage[]
back: NVImage | null
overlays: NVImage[]
}
/**
* Add a volume to the volumes array
* @param volumes - Current volumes array
* @param volume - Volume to add
* @returns New volumes array and index where volume was added
*/
export function addVolume(volumes: NVImage[], volume: NVImage): AddVolumeResult {
const newVolumes = [...volumes, volume]
const index = newVolumes.length === 1 ? 0 : newVolumes.length - 1
return { volumes: newVolumes, index }
}
/**
* Get the index of a volume by its unique ID
* @param volumes - Volumes array to search
* @param id - Volume ID to find
* @returns Index of volume, or -1 if not found
*/
export function getVolumeIndexByID(volumes: NVImage[], id: string): number {
for (let i = 0; i < volumes.length; i++) {
if (volumes[i].id === id) {
return i
}
}
return -1
}
/**
* Get the index of an overlay (non-background volume) by its ID
* @param volumes - Volumes array to search
* @param id - Volume ID to find
* @returns Index in overlays array (volumes[1+]), or -1 if not found
*/
export function getOverlayIndexByID(volumes: NVImage[], id: string): number {
const overlays = volumes.slice(1)
for (let i = 0; i < overlays.length; i++) {
if (overlays[i].id === id) {
return i
}
}
return -1
}
/**
* Reorder a volume to a new index position
* @param volumes - Current volumes array
* @param volume - Volume to reorder
* @param toIndex - Target index (0 for background, -1 to remove, or valid index)
* @returns New volumes array with updated back and overlays
*/
export function setVolume(volumes: NVImage[], volume: NVImage, toIndex = 0): SetVolumeResult {
const numberOfLoadedImages = volumes.length
if (toIndex > numberOfLoadedImages) {
return {
volumes,
back: volumes.length > 0 ? volumes[0] : null,
overlays: volumes.slice(1)
}
}
const volIndex = getVolumeIndexByID(volumes, volume.id)
const newVolumes = [...volumes]
if (toIndex === 0) {
// Move to background
newVolumes.splice(volIndex, 1)
newVolumes.unshift(volume)
} else if (toIndex < 0) {
// Remove volume
newVolumes.splice(volIndex, 1)
} else {
// Move to specific index
newVolumes.splice(volIndex, 1)
newVolumes.splice(toIndex, 0, volume)
}
return {
volumes: newVolumes,
back: newVolumes.length > 0 ? newVolumes[0] : null,
overlays: newVolumes.slice(1)
}
}
/**
* Remove a volume from the volumes array
* @param volumes - Current volumes array
* @param volume - Volume to remove
* @returns New volumes array and the removed volume
*/
export function removeVolume(volumes: NVImage[], volume: NVImage): RemoveVolumeResult {
const result = setVolume(volumes, volume, -1)
return {
volumes: result.volumes,
removed: volume
}
}
/**
* Remove a volume by its index
* @param volumes - Current volumes array
* @param index - Index of volume to remove
* @returns New volumes array and the removed volume
*/
export function removeVolumeByIndex(volumes: NVImage[], index: number): RemoveVolumeResult {
if (index >= volumes.length) {
throw new Error('Index of volume out of bounds')
}
return removeVolume(volumes, volumes[index])
}
/**
* Move a volume to the bottom (background) of the stack
* @param volumes - Current volumes array
* @param volume - Volume to move
* @returns New volumes array with updated order
*/
export function moveVolumeToBottom(volumes: NVImage[], volume: NVImage): SetVolumeResult {
return setVolume(volumes, volume, 0)
}
/**
* Move a volume up one position in the stack
* @param volumes - Current volumes array
* @param volume - Volume to move
* @returns New volumes array with updated order
*/
export function moveVolumeUp(volumes: NVImage[], volume: NVImage): SetVolumeResult {
const volIdx = getVolumeIndexByID(volumes, volume.id)
return setVolume(volumes, volume, volIdx + 1)
}
/**
* Move a volume down one position in the stack
* @param volumes - Current volumes array
* @param volume - Volume to move
* @returns New volumes array with updated order
*/
export function moveVolumeDown(volumes: NVImage[], volume: NVImage): SetVolumeResult {
const volIdx = getVolumeIndexByID(volumes, volume.id)
return setVolume(volumes, volume, volIdx - 1)
}
/**
* Move a volume to the top of the stack
* @param volumes - Current volumes array
* @param volume - Volume to move
* @returns New volumes array with updated order
*/
export function moveVolumeToTop(volumes: NVImage[], volume: NVImage): SetVolumeResult {
return setVolume(volumes, volume, volumes.length - 1)
}
/**
* Set the opacity of a volume
* @param volumes - Current volumes array
* @param volIdx - Index of volume to modify
* @param newOpacity - New opacity value (0-1)
* @returns Same volumes array (volume modified in place)
*/
export function setOpacity(volumes: NVImage[], volIdx: number, newOpacity: number): NVImage[] {
volumes[volIdx].opacity = newOpacity
return volumes
}
/**
* Clone a volume
* @param volumes - Current volumes array
* @param index - Index of volume to clone
* @returns Cloned volume
*/
export function cloneVolume(volumes: NVImage[], index: number): NVImage {
return volumes[index].clone()
}
/**
* Set the active frame for a 4D volume
* @param volumes - Current volumes array
* @param id - Volume ID
* @param frame4D - Frame number to set (will be clamped to valid range)
* @returns Same volumes array (volume modified in place)
*/
export function setFrame4D(volumes: NVImage[], id: string, frame4D: number): NVImage[] {
const idx = getVolumeIndexByID(volumes, id)
if (idx < 0) {
return volumes
}
const volume = volumes[idx]
// Clamp frame to valid range
let clampedFrame = frame4D
if (clampedFrame > volume.nFrame4D! - 1) {
clampedFrame = volume.nFrame4D! - 1
}
if (clampedFrame < 0) {
clampedFrame = 0
}
// No change needed
if (clampedFrame === volume.frame4D) {
return volumes
}
volume.frame4D = clampedFrame
return volumes
}
/**
* Get the active frame for a 4D volume
* @param volumes - Current volumes array
* @param id - Volume ID
* @returns Current frame number
*/
export function getFrame4D(volumes: NVImage[], id: string): number {
const idx = getVolumeIndexByID(volumes, id)
if (idx < 0) {
return 0
}
return volumes[idx].frame4D!
}
/**
* Generate RGBA data for a volume overlay (for testing/demo purposes)
* @param volume - Volume to generate data for
* @returns RGBA data as Uint8ClampedArray
*/
export function overlayRGBA(volume: NVImage): Uint8ClampedArray {
const hdr = volume.hdr!
const vox = hdr.dims[1] * hdr.dims[2] * hdr.dims[3]
const imgRGBA = new Uint8ClampedArray(vox * 4)
const radius = 0.2 * Math.min(Math.min(hdr.dims[1], hdr.dims[2]), hdr.dims[3])
const halfX = 0.5 * hdr.dims[1]
const halfY = 0.5 * hdr.dims[2]
const halfZ = 0.5 * hdr.dims[3]
let j = 0
for (let z = 0; z < hdr.dims[3]; z++) {
for (let y = 0; y < hdr.dims[2]; y++) {
for (let x = 0; x < hdr.dims[1]; x++) {
const dx = Math.abs(x - halfX)
const dy = Math.abs(y - halfY)
const dz = Math.abs(z - halfZ)
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz)
let v = 0
if (dist < radius) {
v = 255
}
imgRGBA[j++] = 0 // Red
imgRGBA[j++] = v // Green
imgRGBA[j++] = 0 // Blue
imgRGBA[j++] = v * 0.5 // Alpha
}
}
}
return imgRGBA
}