import type { IntegralsViewState, PeaksViewState, RangesViewState, } from '@zakodium/nmrium-core'; import { scaleLinear } from 'd3-scale'; import { zoomIdentity } from 'd3-zoom'; import type { Draft } from 'immer'; import { isSpectrum1D } from '../../../data/data1d/Spectrum1D/index.js'; import { insetMargin } from '../../1d/inset/InsetProvider.js'; import type { Inset, InsetBounding, InsetView, } from '../../1d/inset/SpectraInsets.js'; import { getInsetXScale } from '../../1d/utilities/scale.js'; import type { ZoomOptions } from '../../EventsTrackers/BrushTracker.js'; import { defaultIntegralsViewState } from '../../hooks/useActiveSpectrumIntegralsViewState.js'; import { defaultPeaksViewState } from '../../hooks/useActiveSpectrumPeaksViewState.js'; import { getDefaultRangesViewState } from '../../hooks/useActiveSpectrumRangesViewState.js'; import { convertPercentToPixel, convertPixelToPercent, } from '../../hooks/useSVGUnitConverter.js'; import type { FilterType } from '../../utility/filterType.js'; import type { SpectraDirection, State } from '../Reducer.js'; import type { ZoomType } from '../helper/Zoom1DManager.js'; import { toScaleRatio, wheelZoom } from '../helper/Zoom1DManager.js'; import { preparePop } from '../helper/ZoomHistoryManager.js'; import getRange from '../helper/getRange.js'; import { getSpectrum } from '../helper/getSpectrum.js'; import type { ActionType } from '../types/ActionType.js'; import type { MoveOptions } from './DomainActions.js'; import { moveOverAxis } from './DomainActions.js'; interface BrushInsetBoundary { startX: number; endX: number; } type AddInsetAction = ActionType<'ADD_INSET', { startX: number; endX: number }>; type DeleteInsetAction = ActionType<'DELETE_INSET', { insetKey: string }>; type ChangeInsetBoundingAction = ActionType< 'CHANGE_INSET_BOUNDING', { insetKey: string; bounding: Partial } >; type MoveInsetAction = ActionType< 'MOVE_INSET', MoveOptions & { insetKey: string } >; type BrushEndInsetAction = ActionType< 'BRUSH_END_INSET', BrushInsetBoundary & { insetKey: string } >; type ZoomInsetAction = ActionType< 'SET_INSET_ZOOM', { options: ZoomOptions; insetKey: string } >; type ZoomOutInsetAction = ActionType< 'FULL_INSET_ZOOM_OUT', { zoomType?: ZoomType; insetKey: string } >; interface ToggleViewOptions { insetKey: string; key: keyof FilterType; value?: boolean; } type ToggleInsetRangesViewAction = ActionType< 'TOGGLE_INSET_RANGES_VIEW_PROPERTY', ToggleViewOptions >; type ToggleInsetIntegralsViewAction = ActionType< 'TOGGLE_INSET_INTEGRALS_VIEW_PROPERTY', ToggleViewOptions >; type ToggleInsetPeaksViewAction = ActionType< 'TOGGLE_INSET_PEAKS_VIEW_PROPERTY', ToggleViewOptions >; type ToggleInsetDisplayingPeaksModeAction = ActionType< 'TOGGLE_INSET_PEAKS_DISPLAYING_MODE', { insetKey: string; target: 'peaks' | 'ranges'; } >; export type InsetsActions = | AddInsetAction | DeleteInsetAction | ChangeInsetBoundingAction | MoveInsetAction | BrushEndInsetAction | ZoomInsetAction | ZoomOutInsetAction | ToggleInsetRangesViewAction | ToggleInsetIntegralsViewAction | ToggleInsetPeaksViewAction | ToggleInsetDisplayingPeaksModeAction; function handleAddInset(draft: Draft, action: AddInsetAction) { const { startX, endX } = action.payload; const spectrum = getSpectrum(draft); if (!isSpectrum1D(spectrum)) return; const { yDomain, view: { spectra: { activeTab: nucleus }, }, width: baseWidth, height: baseHeight, } = draft; const { id: spectrumKey } = spectrum; const xDomain = getRange(draft, { startX, endX }); const width = Math.max(100, Math.abs(startX - endX)); const x = Math.min(startX, endX); const inset: Inset = { id: crypto.randomUUID(), spectrumKey, bounding: { x: convertPixelToPercent(x, baseWidth), y: convertPixelToPercent(50, baseHeight), width: convertPixelToPercent(width, baseWidth), height: convertPixelToPercent(150, baseHeight), }, xDomain, yDomain, zoomHistory: [{ xDomain, yDomain }], view: { ranges: getDefaultRangesViewState(nucleus), peaks: { ...defaultPeaksViewState }, integrals: { ...defaultIntegralsViewState }, }, }; if (draft.insets?.[nucleus]) { draft.insets[nucleus].push(inset); } else { draft.insets[nucleus] = [inset]; } } function handleDeleteInset(draft: Draft, action: DeleteInsetAction) { const { insetKey } = action.payload; const { view: { spectra: { activeTab }, }, } = draft; if (!activeTab) return; draft.insets[activeTab] = draft.insets[activeTab].filter( (inset) => inset.id !== insetKey, ); } function getInset(draft: Draft, insetKey: string) { const { view: { spectra: { activeTab }, }, } = draft; const insets = draft.insets[activeTab]; return insets.find((inset) => inset.id === insetKey); } function handleChangeInsetBounding( draft: Draft, action: ChangeInsetBoundingAction, ) { const { insetKey, bounding } = action.payload; const inset = getInset(draft, insetKey); if (!inset) return; inset.bounding = { ...inset.bounding, ...bounding }; } function handleMoveInset(draft: Draft, action: MoveInsetAction) { const { insetKey, shiftX, shiftY } = action.payload; const inset = getInset(draft, insetKey); if (!inset) return; const originXDomain = draft.originDomain.xDomains[inset.spectrumKey]; const originYDomain = draft.originDomain.yDomains[inset.spectrumKey]; const { xDomain, yDomain } = moveOverAxis( { shiftX, shiftY }, { xDomain: inset.xDomain, yDomain: inset.yDomain }, { xDomain: originXDomain, yDomain: originYDomain }, ); inset.xDomain = xDomain; inset.yDomain = yDomain; } function getXScale( inset: Inset, options: { baseSize: number; mode: SpectraDirection }, ) { const { xDomain: currentXDomain, bounding: { width }, } = inset; const { mode, baseSize } = options; return getInsetXScale({ width: convertPercentToPixel(width, baseSize), xDomain: currentXDomain, mode, margin: insetMargin, }); } function handleInsetBrushEnd(draft: Draft, action: BrushEndInsetAction) { const { insetKey, ...options } = action.payload; const inset = getInset(draft, insetKey); if (!inset) return; const startX = Math.min(options.startX, options.endX); const endX = Math.max(options.startX, options.endX); inset.xDomain = [startX, endX]; inset.zoomHistory.push({ xDomain: [startX, endX], yDomain: inset.yDomain }); } interface ZoomWithScroll1DOptions { zoomOptions: ZoomOptions; inset: Inset; } function zoomWithScroll(draft: Draft, options: ZoomWithScroll1DOptions) { const { zoomOptions, inset } = options; const { width: baseSize } = draft; const { originDomain, mode } = draft; const scaleX = getXScale(inset, { baseSize, mode }); const { invertScroll, deltaX, deltaY } = zoomOptions; const scaleRatio = toScaleRatio({ delta: deltaY || deltaX, invertScroll }); const { x } = zoomOptions; const domain = zoomIdentity .translate(x, 0) .scale(scaleRatio) .translate(-x, 0) .rescaleX(scaleX) .domain(); const [x1, x2] = originDomain.xDomains[inset.spectrumKey]; inset.xDomain = [Math.max(domain[0], x1), Math.min(domain[1], x2)]; } function handleInsetZoom(draft: Draft, action: ZoomInsetAction) { const { options, insetKey } = action.payload; const { toolOptions: { selectedTool }, } = draft; const { altKey, shiftKey, deltaX, deltaY, invertScroll } = options; const inset = getInset(draft, insetKey); if (!inset) return; // Horizontal zoom in/out 1d spectra by mouse wheel if (shiftKey) { zoomWithScroll(draft, { zoomOptions: options, inset }); return; } if (altKey) { // rescale the integral in ranges and integrals const { view } = inset; const scaleRatio = toScaleRatio({ delta: deltaY | deltaX, invertScroll }); if (selectedTool === 'rangePicking') { view.ranges.integralsScaleRatio *= scaleRatio; return; } if (selectedTool === 'integral') { view.integrals.scaleRatio *= scaleRatio; return; } return; } inset.yDomain = wheelZoom(options, inset.yDomain); } function setZoom( draft: Draft, options: { scale?: number; inset: Inset; }, ) { const { scale = 1, inset } = options; const { bounding: { height }, } = inset; const { originDomain } = draft; const originalYDomain = originDomain.yDomains[inset.spectrumKey]; const scaleValue = scaleLinear(originalYDomain, [ height - insetMargin.bottom, insetMargin.top, ]); const t = zoomIdentity .translate(0, scaleValue(0)) .scale(scale) .translate(0, -scaleValue(0)); inset.yDomain = t.rescaleY(scaleValue).domain(); } function handleInsetZoomOut(draft: Draft, action: ZoomOutInsetAction) { const { zoomType, insetKey } = action?.payload || {}; const inset = getInset(draft, insetKey); if (!inset) return; const originXDomain = draft.originDomain.xDomains[inset.spectrumKey]; const pop = preparePop(inset.zoomHistory); switch (zoomType) { case 'HORIZONTAL': { inset.xDomain = originXDomain; inset.zoomHistory = []; break; } case 'VERTICAL': setZoom(draft, { scale: 0.8, inset }); break; case 'BIDIRECTIONAL': { const zoomValue = pop(); if (zoomValue) { inset.xDomain = zoomValue.xDomain; draft.yDomain = zoomValue.yDomain; const ids = Object.keys(draft.yDomains); for (const id of ids) { draft.yDomains[id] = zoomValue.yDomain; } } else { inset.xDomain = originXDomain; setZoom(draft, { scale: 0.8, inset }); } break; } default: { inset.xDomain = originXDomain; setZoom(draft, { scale: 0.8, inset }); inset.zoomHistory = []; break; } } } function toggleViewProperty( draft: Draft, insetKey: string, key: `${T}.${Extract, string>}`, value?: boolean, ) { const inset = getInset(draft, insetKey); if (!inset) return; const [baseKey, toggleProperty] = key.split('.'); const target = (inset.view as any)[baseKey]; if (typeof value === 'boolean') { target[toggleProperty] = value; } else { target[toggleProperty] = !target[toggleProperty]; } } function handleToggleInsetRangesViewProperty( draft: Draft, action: ToggleInsetRangesViewAction, ) { const { insetKey, key, value } = action.payload; toggleViewProperty(draft, insetKey, `ranges.${key}`, value); } function handleToggleInsetIntegralsViewProperty( draft: Draft, action: ToggleInsetIntegralsViewAction, ) { const { insetKey, key, value } = action.payload; toggleViewProperty(draft, insetKey, `integrals.${key}`, value); } function handleToggleInsetPeaksViewProperty( draft: Draft, action: ToggleInsetPeaksViewAction, ) { const { insetKey, key, value } = action.payload; toggleViewProperty(draft, insetKey, `peaks.${key}`, value); } function handleToggleInsetDisplayingPeaksMode( draft: Draft, action: ToggleInsetDisplayingPeaksModeAction, ) { const { insetKey, target } = action.payload; const inset = getInset(draft, insetKey); if (!inset) return; const targetView = inset.view[target]; targetView.displayingMode = targetView.displayingMode === 'single' ? 'spread' : 'single'; } export { handleAddInset, handleChangeInsetBounding, handleDeleteInset, handleInsetBrushEnd, handleInsetZoom, handleInsetZoomOut, handleMoveInset, handleToggleInsetDisplayingPeaksMode, handleToggleInsetIntegralsViewProperty, handleToggleInsetPeaksViewProperty, handleToggleInsetRangesViewProperty, };