diff --git a/src/component/1d/Viewer1D.tsx b/src/component/1d/Viewer1D.tsx index 848fcc520..1373b5b5e 100644 --- a/src/component/1d/Viewer1D.tsx +++ b/src/component/1d/Viewer1D.tsx @@ -293,8 +293,8 @@ function Viewer1D({ emptyText = undefined }: Viewer1DProps) { }, [dispatch]); const handleZoom = useCallback( - (event) => { - dispatch({ type: 'SET_ZOOM', payload: { event } }); + (options) => { + dispatch({ type: 'SET_ZOOM', payload: { options } }); }, [dispatch], ); diff --git a/src/component/2d/Viewer2D.tsx b/src/component/2d/Viewer2D.tsx index 50de2a36e..20dcc6407 100644 --- a/src/component/2d/Viewer2D.tsx +++ b/src/component/2d/Viewer2D.tsx @@ -163,18 +163,19 @@ function Viewer2D({ emptyText = undefined }: Viewer2DProps) { [DIMENSION, dispatch], ); - const handleZoom: OnZoom = (event) => { - const { x: startX, y: startY, shiftKey } = event; + const handleZoom: OnZoom = (options) => { + const { x: startX, y: startY, shiftKey } = options; const trackID = getLayoutID(DIMENSION, { startX, startY }); if (trackID) { if ( + (trackID === 'CENTER_2D' && shiftKey) || trackID !== 'CENTER_2D' || (selectedTool === 'phaseCorrectionTwoDimensions' && !shiftKey) ) { - dispatch({ type: 'SET_ZOOM', payload: { event, trackID } }); + dispatch({ type: 'SET_ZOOM', payload: { options, trackID } }); } else { - dispatch({ type: 'SET_2D_LEVEL', payload: event }); + dispatch({ type: 'SET_2D_LEVEL', payload: { options } }); } } }; diff --git a/src/component/EventsTrackers/BrushTracker.tsx b/src/component/EventsTrackers/BrushTracker.tsx index 298ee2088..a172b229a 100644 --- a/src/component/EventsTrackers/BrushTracker.tsx +++ b/src/component/EventsTrackers/BrushTracker.tsx @@ -81,9 +81,12 @@ interface Position { } export type OnClick = (element: React.MouseEvent & Position) => void; export type { OnClick as OnDoubleClick }; -export type OnZoom = ( - event: Pick & Position, -) => void; +export type ZoomOptions = Pick< + React.WheelEvent, + 'deltaY' | 'shiftKey' | 'deltaMode' | 'altKey' +> & + Position; +export type OnZoom = (options: ZoomOptions) => void; export type OnBrush = (state: BrushTrackerContext) => void; interface BrushTrackerProps { @@ -207,8 +210,8 @@ export function BrushTracker({ const x = event.clientX - boundingRect.x; const y = event.clientY - boundingRect.y; - const { deltaY, deltaX, shiftKey, deltaMode } = event; - onZoom({ deltaY: deltaY || deltaX, shiftKey, deltaMode, x, y }); + const { deltaY, deltaX, shiftKey, altKey, deltaMode } = event; + onZoom({ deltaY: deltaY || deltaX, shiftKey, altKey, deltaMode, x, y }); }, [onZoom], ); diff --git a/src/component/reducer/actions/ToolsActions.ts b/src/component/reducer/actions/ToolsActions.ts index 977373d63..ad0819f4f 100644 --- a/src/component/reducer/actions/ToolsActions.ts +++ b/src/component/reducer/actions/ToolsActions.ts @@ -1,4 +1,5 @@ import { v4 } from '@lukeed/uuid'; +import { zoomIdentity } from 'd3'; import { Draft } from 'immer'; import { Spectrum, Spectrum1D, Spectrum2D } from 'nmr-load-save'; import { BaselineCorrectionZone } from 'nmr-processing'; @@ -7,7 +8,8 @@ import { contoursManager } from '../../../data/data2d/Spectrum2D/contours'; import { Nucleus } from '../../../data/types/common/Nucleus'; import { getYScale, getXScale } from '../../1d/utilities/scale'; import { LAYOUT, Layout } from '../../2d/utilities/DimensionLayout'; -import { get2DYScale } from '../../2d/utilities/scale'; +import { get2DXScale, get2DYScale } from '../../2d/utilities/scale'; +import { ZoomOptions } from '../../EventsTrackers/BrushTracker'; import { defaultRangesViewState } from '../../hooks/useActiveSpectrumRangesViewState'; import { Tool, options as Tools } from '../../toolbar/ToolTypes'; import groupByInfoKey from '../../utility/GroupByInfoKey'; @@ -83,7 +85,7 @@ type BrushEndAction = ActionType<'BRUSH_END', BrushBoundary>; type ZoomAction = ActionType< 'SET_ZOOM', - { event: any; trackID?: Layout; selectedTool?: Tool } + { options: ZoomOptions; trackID?: Layout; selectedTool?: Tool } >; type ZoomOutAction = ActionType< @@ -91,10 +93,7 @@ type ZoomOutAction = ActionType< { zoomType?: ZoomType; trackID?: Layout } >; type SetActiveTabAction = ActionType<'SET_ACTIVE_TAB', { tab?: Nucleus }>; -type LevelChangeAction = ActionType< - 'SET_2D_LEVEL', - { deltaY: number; shiftKey: boolean } ->; +type LevelChangeAction = ActionType<'SET_2D_LEVEL', { options: ZoomOptions }>; export type ToolsActions = | ActionType< @@ -323,16 +322,98 @@ function handleBrushEnd(draft: Draft, action: BrushEndAction) { addToBrushHistory(draft, { trackID, xDomain: domainX, yDomain: domainY }); } +interface ZoomWithScroll1DOptions { + zoomOptions: ZoomOptions; + direction?: 'Horizontal'; + dimension: '1D'; +} +interface ZoomWithScroll2DOptions { + zoomOptions: ZoomOptions; + direction: 'Horizontal' | 'Vertical' | 'Both'; + dimension: '2D'; +} + +function zoomWithScroll( + draft: Draft, + options: ZoomWithScroll1DOptions | ZoomWithScroll2DOptions, +) { + const { zoomOptions, direction = 'Horizontal', dimension } = options; + + let scaleX; + let scaleY; + + if (dimension === '1D') { + scaleX = getXScale(draft); + } else { + scaleX = get2DXScale(draft); + scaleY = get2DYScale(draft); + } + + const scaleRatio = toScaleRatio(zoomOptions, { invert: true }); + + if (direction === 'Both' || direction === 'Horizontal') { + const { x } = zoomOptions; + const domain = zoomIdentity + .translate(x, 0) + .scale(scaleRatio) + .translate(-x, 0) + .rescaleX(scaleX) + .domain(); + const { + originDomain: { + xDomain: [x1, x2], + }, + } = draft; + draft.xDomain = [ + domain[0] < x1 ? x1 : domain[0], + domain[1] > x2 ? x2 : domain[1], + ]; + } + + if ( + dimension === '2D' && + (direction === 'Both' || direction === 'Vertical') + ) { + const { y } = zoomOptions; + const domain = zoomIdentity + .translate(y, 0) + .scale(scaleRatio) + .translate(-y, 0) + .rescaleX(scaleY) + .domain(); + const { + originDomain: { + yDomain: [x1, x2], + }, + } = draft; + draft.yDomain = [ + domain[0] < x1 ? x1 : domain[0], + domain[1] > x2 ? x2 : domain[1], + ]; + } +} + function handleZoom(draft: Draft, action: ZoomAction) { - const { event, trackID } = action.payload; + const { options, trackID } = action.payload; const { displayerMode, yDomains, toolOptions: { selectedTool }, } = draft; - const scaleRatio = toScaleRatio(event); + const scaleRatio = toScaleRatio(options); switch (displayerMode) { case '2D': { + const { shiftKey } = options; + + if (selectedTool === 'zoom' && shiftKey) { + zoomWithScroll(draft, { + zoomOptions: options, + dimension: '2D', + direction: 'Both', + }); + return; + } + // change the vertical scale for traces in 2D phase correction if ( selectedTool === 'phaseCorrectionTwoDimensions' && @@ -348,7 +429,7 @@ function handleZoom(draft: Draft, action: ZoomAction) { const id = getSpectrumID(draft, index); if (id) { const domain = yDomains[id]; - yDomains[id] = wheelZoom(event, domain); + yDomains[id] = wheelZoom(options, domain); } } } @@ -358,17 +439,23 @@ function handleZoom(draft: Draft, action: ZoomAction) { case '1D': { const activeSpectra = getActiveSpectra(draft); + const { shiftKey } = options; + if (selectedTool === 'zoom' && shiftKey) { + zoomWithScroll(draft, { zoomOptions: options, dimension: '1D' }); + + return; + } if (!activeSpectra) { // rescale the spectra for (const key of Object.keys(yDomains)) { const domain = yDomains[key]; - yDomains[key] = wheelZoom(event, domain); + yDomains[key] = wheelZoom(options, domain); } return; } - if (activeSpectra.length === 1 && event.shiftKey) { + if (activeSpectra.length === 1 && shiftKey) { switch (selectedTool) { case 'rangePicking': { setRangesViewProperty( @@ -393,7 +480,7 @@ function handleZoom(draft: Draft, action: ZoomAction) { for (const activeSpectrum of activeSpectra) { const domain = yDomains?.[activeSpectrum?.id]; if (domain) { - yDomains[activeSpectrum?.id] = wheelZoom(event, domain); + yDomains[activeSpectrum?.id] = wheelZoom(options, domain); } } } @@ -431,6 +518,7 @@ function zoomOut(draft: Draft, action: ZoomOutAction) { if (zoomValue) { draft.xDomain = zoomValue.xDomain; } else { + draft.xDomain = xDomain; setZoom(draft, { scale: 0.8 }); } break; @@ -620,7 +708,7 @@ function handelSetActiveTab(draft: Draft, action: SetActiveTabAction) { } function levelChangeHandler(draft: Draft, action: LevelChangeAction) { - const { deltaY, shiftKey } = action.payload; + const { deltaY, altKey } = action.payload.options; const { data, view: { @@ -645,7 +733,7 @@ function levelChangeHandler(draft: Draft, action: LevelChangeAction) { for (const spectrum of spectra as Spectrum2D[]) { const contourOptions = spectrum.display.contourOptions; const zoom = contoursManager(spectrum.id, levels, contourOptions); - levels[spectrum.id] = zoom.wheel(deltaY, shiftKey); + levels[spectrum.id] = zoom.wheel(deltaY, altKey); } } catch (error) { // TODO: handle error. diff --git a/src/component/reducer/helper/Zoom1DManager.ts b/src/component/reducer/helper/Zoom1DManager.ts index 8ef9566dd..a54fdbbbd 100644 --- a/src/component/reducer/helper/Zoom1DManager.ts +++ b/src/component/reducer/helper/Zoom1DManager.ts @@ -1,6 +1,7 @@ import { zoomIdentity, scaleLinear } from 'd3'; import { Draft } from 'immer'; +import { ZoomOptions } from '../../EventsTrackers/BrushTracker'; import { State } from '../Reducer'; import { getActiveSpectrum } from './getActiveSpectrum'; @@ -14,27 +15,30 @@ export const ZOOM_TYPES = { export type ZoomType = keyof typeof ZOOM_TYPES; -function toScaleRatio(event: WheelEvent, zoomOptions: ZoomOptions = {}) { +function toScaleRatio( + options: ZoomOptions, + zoomOptions: ScaleRationOptions = {}, +) { const { factor = 1, invert = false } = zoomOptions; const deltaY = - Math.abs(event.deltaY) < 100 ? event.deltaY * 100 : event.deltaY; + Math.abs(options.deltaY) < 100 ? options.deltaY * 100 : options.deltaY; const delta = deltaY * (invert ? -0.001 : 0.001) * factor; const ratio = delta < 0 ? -1 / (delta - 1) : 1 + delta; return ratio; } -interface ZoomOptions { +interface ScaleRationOptions { factor?: number; invert?: boolean; } function wheelZoom( - event: WheelEvent, + options: ZoomOptions, domain: number[], - zoomOptions: ZoomOptions = {}, + scaleOptions: ScaleRationOptions = {}, ): number[] { - const ratio = toScaleRatio(event, zoomOptions); + const ratio = toScaleRatio(options, scaleOptions); const [min, max] = domain; return [min * ratio, max * ratio]; } diff --git a/src/data/data2d/Spectrum2D/contours.ts b/src/data/data2d/Spectrum2D/contours.ts index af7be8050..c221cee7e 100644 --- a/src/data/data2d/Spectrum2D/contours.ts +++ b/src/data/data2d/Spectrum2D/contours.ts @@ -18,7 +18,7 @@ interface ContourOptions { negative: ContourItem; } interface WheelOptions { - shiftKey: boolean; + altKey: boolean; contourOptions: ContourOptions; currentLevel: Level; } @@ -67,20 +67,20 @@ function contoursManager( const currentLevel = spectraLevels[spectrumID]; - const wheel = (value, shiftKey) => - prepareWheel(value, { shiftKey, contourOptions, currentLevel }); + const wheel = (value, altKey) => + prepareWheel(value, { altKey, contourOptions, currentLevel }); const getLevel = () => currentLevel; const checkLevel = () => prepareCheckLevel(currentLevel, contourOptions); return { wheel, getLevel, checkLevel }; } function prepareWheel(value: number, options: WheelOptions) { - const { shiftKey, currentLevel, contourOptions } = options; + const { altKey, currentLevel, contourOptions } = options; const sign = Math.sign(value); const positiveBoundary = contourOptions.positive.contourLevels; const negativeBoundary = contourOptions.negative.contourLevels; - if (shiftKey) { + if (altKey) { if ( (currentLevel.positive === positiveBoundary[0] && sign === -1) || (currentLevel.positive >= positiveBoundary[1] && sign === 1)