@niivue/niivue
Version:
minimal webgl2 nifti image viewer
275 lines (241 loc) • 7.5 kB
text/typescript
/**
* Wheel controller helper functions for mouse wheel/trackpad event handling.
* This module provides pure functions for scroll wheel interaction logic.
*
* Related to: Mouse wheel scrolling, zooming, ROI resizing, segmentation threshold
*/
import { DRAG_MODE } from '../../nvdocument.js'
/**
* Parameters for calculating scroll amount
*/
export interface CalculateScrollAmountParams {
deltaY: number
invertScrollDirection: boolean
}
/**
* Parameters for checking if ROI selection resize is valid
*/
export interface IsValidRoiResizeParams {
dragMode: DRAG_MODE
dragStart: number[]
dragEnd: number[]
}
/**
* Parameters for calculating updated ROI selection bounds
*/
export interface UpdateRoiSelectionParams {
dragStart: number[]
dragEnd: number[]
delta: number
}
/**
* Result of ROI selection update
*/
export interface RoiSelectionResult {
newDragStart: number[]
newDragEnd: number[]
}
/**
* Parameters for calculating zoom values
*/
export interface CalculateZoomParams {
currentZoom: number
scrollAmount: number
}
/**
* Result of zoom calculation
*/
export interface ZoomResult {
newZoom: number
zoomChange: number
}
/**
* Parameters for calculating pan offset after zoom
*/
export interface CalculatePanOffsetParams {
currentPan: number[]
zoomChange: number
crosshairMM: number[]
}
/**
* Parameters for click-to-segment threshold adjustment
*/
export interface AdjustSegmentThresholdParams {
currentPercent: number
scrollAmount: number
}
/**
* Parameters for determining wheel action
*/
export interface DetermineWheelActionParams {
thumbnailVisible: boolean
mosaicStringLength: number
eventInBounds: boolean
hasBounds: boolean
}
/**
* Result of wheel action determination
*/
export interface WheelActionResult {
shouldProcess: boolean
showBoundsBorder: boolean
}
/**
* Parameters for checking if pan/zoom mode scroll should apply
*/
export interface ShouldZoomParams {
dragMode: DRAG_MODE
isInRenderTile: boolean
}
/**
* Calculate the normalized scroll amount from wheel event
* @param params - Scroll calculation parameters
* @returns Normalized scroll amount (-0.01 or 0.01, possibly inverted)
*/
export function calculateScrollAmount(params: CalculateScrollAmountParams): number {
const { deltaY, invertScrollDirection } = params
let scrollAmount = deltaY < 0 ? -0.01 : 0.01
if (invertScrollDirection) {
scrollAmount = -scrollAmount
}
return scrollAmount
}
/**
* Check if the current drag state allows ROI selection resizing
* @param params - ROI resize validation parameters
* @returns True if ROI resize should proceed
*/
export function isValidRoiResize(params: IsValidRoiResizeParams): boolean {
const { dragMode, dragStart, dragEnd } = params
if (dragMode !== DRAG_MODE.roiSelection) {
return false
}
const dragStartSum = dragStart.reduce((a, b) => a + b, 0)
const dragEndSum = dragEnd.reduce((a, b) => a + b, 0)
return dragStartSum > 0 && dragEndSum > 0
}
/**
* Calculate updated ROI selection bounds based on scroll delta
* @param params - ROI selection update parameters
* @returns New drag start and end positions
*/
export function updateRoiSelection(params: UpdateRoiSelectionParams): RoiSelectionResult {
const { dragStart, dragEnd, delta } = params
const newDragStart = [...dragStart]
const newDragEnd = [...dragEnd]
// Update X bounds
if (dragStart[0] < dragEnd[0]) {
newDragStart[0] = dragStart[0] - delta
newDragEnd[0] = dragEnd[0] + delta
} else {
newDragStart[0] = dragStart[0] + delta
newDragEnd[0] = dragEnd[0] - delta
}
// Update Y bounds
if (dragStart[1] < dragEnd[1]) {
newDragStart[1] = dragStart[1] - delta
newDragEnd[1] = dragEnd[1] + delta
} else {
newDragStart[1] = dragStart[1] + delta
newDragEnd[1] = dragEnd[1] - delta
}
return {
newDragStart,
newDragEnd
}
}
/**
* Calculate the scroll delta direction for ROI resizing
* @param deltaY - Wheel event deltaY
* @returns 1 for scroll down (grow), -1 for scroll up (shrink)
*/
export function getRoiScrollDelta(deltaY: number): number {
return deltaY > 0 ? 1 : -1
}
/**
* Calculate new zoom level and change from scroll
* @param params - Zoom calculation parameters
* @returns New zoom value and the change amount
*/
export function calculateZoom(params: CalculateZoomParams): ZoomResult {
const { currentZoom, scrollAmount } = params
const zoomDirection = scrollAmount < 0 ? 1 : -1
let newZoom = currentZoom * (1.0 + 10 * (0.01 * zoomDirection))
newZoom = Math.round(newZoom * 10) / 10
const zoomChange = currentZoom - newZoom
return {
newZoom,
zoomChange
}
}
/**
* Calculate new pan offset after zoom to keep crosshair in place
* @param params - Pan offset calculation parameters
* @returns New pan values [x, y, z]
*/
export function calculatePanOffsetAfterZoom(params: CalculatePanOffsetParams): number[] {
const { currentPan, zoomChange, crosshairMM } = params
return [currentPan[0] + zoomChange * crosshairMM[0], currentPan[1] + zoomChange * crosshairMM[1], currentPan[2] + zoomChange * crosshairMM[2]]
}
/**
* Adjust click-to-segment threshold based on scroll direction
* @param params - Threshold adjustment parameters
* @returns New threshold percent clamped to [0, 1]
*/
export function adjustSegmentThreshold(params: AdjustSegmentThresholdParams): number {
const { currentPercent, scrollAmount } = params
let newPercent = currentPercent
if (scrollAmount < 0) {
newPercent -= 0.01
newPercent = Math.max(newPercent, 0)
} else {
newPercent += 0.01
newPercent = Math.min(newPercent, 1)
}
return newPercent
}
/**
* Determine if wheel event should be processed based on current state
* @param params - Wheel action determination parameters
* @returns Whether to process and updated bounds border state
*/
export function determineWheelAction(params: DetermineWheelActionParams): WheelActionResult {
const { thumbnailVisible, mosaicStringLength, eventInBounds, hasBounds } = params
// Don't process if thumbnail is visible or in mosaic mode
if (thumbnailVisible || mosaicStringLength > 0) {
return {
shouldProcess: false,
showBoundsBorder: false
}
}
// Don't process if event is outside bounds
if (!eventInBounds) {
return {
shouldProcess: false,
showBoundsBorder: false
}
}
return {
shouldProcess: true,
showBoundsBorder: hasBounds
}
}
/**
* Check if zoom should be applied based on drag mode and position
* @param params - Zoom check parameters
* @returns True if zoom should be applied
*/
export function shouldApplyZoom(params: ShouldZoomParams): boolean {
const { dragMode, isInRenderTile } = params
return dragMode === DRAG_MODE.pan && !isInRenderTile
}
/**
* Get mouse position relative to canvas from wheel event
* @param clientX - Event clientX
* @param clientY - Event clientY
* @param canvasRect - Canvas bounding rect
* @returns Position [x, y] relative to canvas
*/
export function getWheelEventPosition(clientX: number, clientY: number, canvasRect: DOMRect): [number, number] {
return [clientX - canvasRect.left, clientY - canvasRect.top]
}