From 3b11b294b529cfed89272254bb116fa2b62d4a66 Mon Sep 17 00:00:00 2001 From: hamed musallam Date: Thu, 30 Apr 2026 10:53:54 +0200 Subject: [PATCH 1/6] feat: change peaks shape --- src/component/modal/EditPeakShapeModal.tsx | 62 ++++++++++++------- .../panels/PeaksPanel/PeaksPanel.tsx | 47 +++++++------- src/component/reducer/actions/PeaksActions.ts | 18 ++++-- .../Spectrum1D/peaks/autoPeakPicking.ts | 2 +- 4 files changed, 76 insertions(+), 53 deletions(-) diff --git a/src/component/modal/EditPeakShapeModal.tsx b/src/component/modal/EditPeakShapeModal.tsx index 0c0b7b4660..f059995d9d 100644 --- a/src/component/modal/EditPeakShapeModal.tsx +++ b/src/component/modal/EditPeakShapeModal.tsx @@ -1,12 +1,13 @@ import { DialogFooter } from '@blueprintjs/core'; +import styled from '@emotion/styled'; import { yupResolver } from '@hookform/resolvers/yup'; import type { Peak1D } from '@zakodium/nmr-types'; import { useState } from 'react'; import { useForm } from 'react-hook-form'; +import { Button } from 'react-science/ui'; import * as Yup from 'yup'; import { useDispatch } from '../context/DispatchContext.js'; -import ActionButtons from '../elements/ActionButtons.js'; import type { LabelStyle } from '../elements/Label.js'; import Label from '../elements/Label.js'; import { NumberInput2Controller } from '../elements/NumberInput2Controller.js'; @@ -17,6 +18,14 @@ import { useActiveNucleusTab } from '../hooks/useActiveNucleusTab.js'; import { usePanelPreferences } from '../hooks/usePanelPreferences.js'; import { formatNumber } from '../utility/formatNumber.js'; + +const FooterContainer = styled.div` + display: flex; + justify-content: flex-end; + gap: 5px; +`; + + type Shape = NonNullable; type Kind = @@ -40,9 +49,9 @@ function getValues(peak: Peak1D, kind: Kind): Shape { const shapeData = (shape?.kind || '').toLocaleLowerCase() !== kind ? { - ...getKindDefaultValues(kind), - ...(shape?.fwhm && { fwhm: shape?.fwhm }), - } + ...getKindDefaultValues(kind), + ...(shape?.fwhm && { fwhm: shape?.fwhm }), + } : shape; return shapeData as Shape; @@ -104,17 +113,23 @@ function InnerEditPeakShapeModal(props: Required) { resolver: yupResolver(validation(kind)) as any, }); - function changePeakShapeHandler(values: any) { - dispatch({ - type: 'CHANGE_PEAK_SHAPE', - payload: { - id: peak.id, - shape: { - ...values, + function changePeakShapeHandler(applyToAll = false) { + + void handleSubmit((values) => { + + dispatch({ + type: 'CHANGE_PEAK_SHAPE', + payload: { + id: !applyToAll ? peak.id : undefined, + shape: { + ...values, + }, }, - }, - }); - onCloseDialog(); + }); + onCloseDialog(); + + })(); + } function handleChangeKind({ value }: { value: Kind }) { @@ -162,13 +177,18 @@ function InnerEditPeakShapeModal(props: Required) { )} - - handleSubmit(changePeakShapeHandler)()} - doneLabel="Save" - onCancel={() => onCloseDialog?.()} - /> + + + + + + ); diff --git a/src/component/panels/PeaksPanel/PeaksPanel.tsx b/src/component/panels/PeaksPanel/PeaksPanel.tsx index 1d38db62e4..704a607300 100644 --- a/src/component/panels/PeaksPanel/PeaksPanel.tsx +++ b/src/component/panels/PeaksPanel/PeaksPanel.tsx @@ -11,7 +11,6 @@ import { usePreferences } from '../../context/PreferencesContext.js'; import { useToaster } from '../../context/ToasterContext.js'; import { useAlert } from '../../elements/Alert.js'; import { useActiveSpectrumPeaksViewState } from '../../hooks/useActiveSpectrumPeaksViewState.js'; -import useCheckExperimentalFeature from '../../hooks/useCheckExperimentalFeature.js'; import { useFormatNumberByNucleus } from '../../hooks/useFormatNumberByNucleus.js'; import useSpectrum from '../../hooks/useSpectrum.js'; import { booleanToString } from '../../utility/booleanToString.js'; @@ -47,7 +46,6 @@ function PeaksPanelInner(props: PeaksPanelInnerProps) { const dispatch = useDispatch(); const alert = useAlert(); const toaster = useToaster(); - const isExperimental = useCheckExperimentalFeature(); const settingRef = useRef(null); @@ -129,6 +127,25 @@ function PeaksPanelInner(props: PeaksPanelInnerProps) { peaksViewState; const leftButtons: ToolbarItemProps[] = [ + { + disabled, + icon: , + tooltip: `${booleanToString(!showPeaksShapes)} peaks shapes`, + onClick: () => toggleViewProperty('showPeaksShapes'), + active: showPeaksShapes, + }, + { + disabled, + icon: , + tooltip: `${booleanToString(!showPeaksSum)} peaks sum`, + onClick: () => toggleViewProperty('showPeaksSum'), + active: showPeaksSum, + }, + { + icon: , + tooltip: 'Optimize peaks', + onClick: optimizePeaksHandler, + }, { disabled, icon: , @@ -143,32 +160,10 @@ function PeaksPanelInner(props: PeaksPanelInnerProps) { displayingMode === 'spread' ? 'Top of the peak' : 'Top of the spectrum', onClick: toggleDisplayingMode, active: displayingMode === 'spread', - }, + } + ]; - if (isExperimental) { - leftButtons.unshift( - { - disabled, - icon: , - tooltip: `${booleanToString(!showPeaksShapes)} peaks shapes`, - onClick: () => toggleViewProperty('showPeaksShapes'), - active: showPeaksShapes, - }, - { - disabled, - icon: , - tooltip: `${booleanToString(!showPeaksSum)} peaks sum`, - onClick: () => toggleViewProperty('showPeaksSum'), - active: showPeaksSum, - }, - { - icon: , - tooltip: 'Optimize peaks', - onClick: optimizePeaksHandler, - }, - ); - } return ( {!isFlipped && ( diff --git a/src/component/reducer/actions/PeaksActions.ts b/src/component/reducer/actions/PeaksActions.ts index a56d097b42..70ecb49e38 100644 --- a/src/component/reducer/actions/PeaksActions.ts +++ b/src/component/reducer/actions/PeaksActions.ts @@ -46,7 +46,7 @@ type AutoPeaksPickingAction = ActionType< type ChangePeaksShapeAction = ActionType< 'CHANGE_PEAK_SHAPE', { - id: string; + id?: string; shape: Peak1D['shape']; } >; @@ -92,9 +92,9 @@ function handleAddPeak(draft: Draft, action: AddPeakAction) { y: candidatePeak.y, width: 1, shape: { - kind: 'generalizedLorentzian', + kind: 'pseudoVoigt', fwhm: 1, - gamma: 0.5, + mu: 0.5, }, }; spectrum.peaks.values.push(...mapPeaks([peak], spectrum)); @@ -121,9 +121,9 @@ function handleAddPeaks(draft: Draft, action: AddPeaksAction) { y: peak.y, width: 1, shape: { - kind: 'generalizedLorentzian', + kind: 'pseudoVoigt', fwhm: 1, - gamma: 0.5, + mu: 0.5, }, }; spectrum.peaks.values.push(newPeak); @@ -204,6 +204,14 @@ function handleChangePeakShape( const spectrum = getSpectrum(draft); if (!isSpectrum1D(spectrum)) return; + if (!id) { + spectrum.peaks.values = spectrum.peaks.values.map((peak) => ({ + ...peak, + shape, + })); + return; + } + const peakIndex = spectrum.peaks.values.findIndex((peak) => peak.id === id); if (peakIndex !== -1) { spectrum.peaks.values[peakIndex].shape = shape; diff --git a/src/data/data1d/Spectrum1D/peaks/autoPeakPicking.ts b/src/data/data1d/Spectrum1D/peaks/autoPeakPicking.ts index c0bfaa548f..28f7ce4e30 100644 --- a/src/data/data1d/Spectrum1D/peaks/autoPeakPicking.ts +++ b/src/data/data1d/Spectrum1D/peaks/autoPeakPicking.ts @@ -41,7 +41,7 @@ export function autoPeakPicking( frequency, direction, sensitivity: 100, - shape: { kind: 'lorentzian' }, + shape: { kind: 'pseudoVoigt', mu: 0.5, fwhm: 1 }, noiseLevel: noise * noiseFactor, minMaxRatio, // Threshold to determine if a given peak should be considered as a noise realTopDetection: true, From 4a7e50fa7454ce5f425298d326b156f02d5e68ac Mon Sep 17 00:00:00 2001 From: hamed musallam Date: Thu, 30 Apr 2026 15:01:51 +0200 Subject: [PATCH 2/6] feat: default peak shape preferences --- src/component/1d/BrushTracker1D.tsx | 17 ++-- .../header/AutoPeakPickingOptionPanel.tsx | 9 +- src/component/modal/EditPeakShapeModal.tsx | 32 ++++---- .../panels/PeaksPanel/PeaksPanel.tsx | 3 +- .../panels/PeaksPanel/PeaksPreferences.tsx | 82 +++++++++++++++++-- src/component/reducer/actions/PeaksActions.ts | 61 ++++++++------ .../panelsPreferencesDefaultValues.ts | 8 +- .../Spectrum1D/peaks/autoPeakPicking.ts | 6 +- 8 files changed, 160 insertions(+), 58 deletions(-) diff --git a/src/component/1d/BrushTracker1D.tsx b/src/component/1d/BrushTracker1D.tsx index a65410d7be..b3c100ab19 100644 --- a/src/component/1d/BrushTracker1D.tsx +++ b/src/component/1d/BrushTracker1D.tsx @@ -94,6 +94,8 @@ export function BrushTracker1D({ children }: Required) { 'matrixGeneration', activeTab, ); + const { defaultPeakShape } = usePanelPreferences('peaks', activeTab); + const { logger } = useLogger(); const scaleState = useScaleChecked(); const convertToPPM = usePixelToPPMConverter(); @@ -205,12 +207,14 @@ export function BrushTracker1D({ children }: Required) { }); break; } - case options.peakPicking.id: + case options.peakPicking.id: { + const { startX, endX } = brushData; dispatch({ type: 'ADD_PEAKS', - payload: brushData, + payload: { startX, endX, defaultPeakShape }, }); break; + } case options.databaseRangesSelection.id: propagateEvent(); break; @@ -304,6 +308,7 @@ export function BrushTracker1D({ children }: Required) { logger, dispatchPreferences, activeTab, + defaultPeakShape, openAnalysisModal, width, height, @@ -420,13 +425,14 @@ export function BrushTracker1D({ children }: Required) { switch (keyModifiers) { case primaryKeyIdentifier: { switch (selectedTool) { - case 'peakPicking': + case 'peakPicking': { + const { x } = event; dispatch({ type: 'ADD_PEAK', - payload: event, + payload: { x, defaultPeakShape }, }); break; - + } case 'integral': dispatch({ type: 'CUT_INTEGRAL', @@ -494,6 +500,7 @@ export function BrushTracker1D({ children }: Required) { }, [ activeTab, + defaultPeakShape, dispatch, dispatchPreferences, getModifiersKey, diff --git a/src/component/header/AutoPeakPickingOptionPanel.tsx b/src/component/header/AutoPeakPickingOptionPanel.tsx index a54ddad08a..cb076daadd 100644 --- a/src/component/header/AutoPeakPickingOptionPanel.tsx +++ b/src/component/header/AutoPeakPickingOptionPanel.tsx @@ -8,10 +8,12 @@ import { useToaster } from '../context/ToasterContext.js'; import Label from '../elements/Label.js'; import { NumberInput2Controller } from '../elements/NumberInput2Controller.js'; import { Select2Controller } from '../elements/Select2Controller.js'; +import { useActiveNucleusTab } from '../hooks/useActiveNucleusTab.ts'; import { MIN_AREA_POINTS, useCheckPointsNumberInWindowArea, } from '../hooks/useCheckPointsNumberInWindowArea.js'; +import { usePanelPreferences } from '../hooks/usePanelPreferences.ts'; import { headerLabelStyle } from './Header.js'; import { HeaderWrapper } from './HeaderWrapper.js'; @@ -60,6 +62,8 @@ export function AutoPeakPickingOptionPanel() { const dispatch = useDispatch(); const pointsNumber = useCheckPointsNumberInWindowArea(); const toaster = useToaster(); + const nucleus = useActiveNucleusTab(); + const { defaultPeakShape } = usePanelPreferences('peaks', nucleus); const { handleSubmit, formState: { isValid }, @@ -74,7 +78,10 @@ export function AutoPeakPickingOptionPanel() { if (pointsNumber > MIN_AREA_POINTS) { dispatch({ type: 'AUTO_PEAK_PICKING', - payload: values, + payload: { + options: values, + defaultPeakShape, + }, }); } else { toaster.show({ diff --git a/src/component/modal/EditPeakShapeModal.tsx b/src/component/modal/EditPeakShapeModal.tsx index f059995d9d..788cc0e105 100644 --- a/src/component/modal/EditPeakShapeModal.tsx +++ b/src/component/modal/EditPeakShapeModal.tsx @@ -18,14 +18,12 @@ import { useActiveNucleusTab } from '../hooks/useActiveNucleusTab.js'; import { usePanelPreferences } from '../hooks/usePanelPreferences.js'; import { formatNumber } from '../utility/formatNumber.js'; - const FooterContainer = styled.div` display: flex; justify-content: flex-end; gap: 5px; `; - type Shape = NonNullable; type Kind = @@ -49,9 +47,9 @@ function getValues(peak: Peak1D, kind: Kind): Shape { const shapeData = (shape?.kind || '').toLocaleLowerCase() !== kind ? { - ...getKindDefaultValues(kind), - ...(shape?.fwhm && { fwhm: shape?.fwhm }), - } + ...getKindDefaultValues(kind), + ...(shape?.fwhm && { fwhm: shape?.fwhm }), + } : shape; return shapeData as Shape; @@ -64,7 +62,7 @@ function validation(kind: Kind) { }); } -const KINDS: Array<{ label: string; value: Kind }> = [ +export const PEAKS_SHAPES: Array<{ label: string; value: Kind }> = [ { value: 'gaussian', label: 'Gaussian', @@ -114,9 +112,7 @@ function InnerEditPeakShapeModal(props: Required) { }); function changePeakShapeHandler(applyToAll = false) { - void handleSubmit((values) => { - dispatch({ type: 'CHANGE_PEAK_SHAPE', payload: { @@ -127,9 +123,7 @@ function InnerEditPeakShapeModal(props: Required) { }, }); onCloseDialog(); - })(); - } function handleChangeKind({ value }: { value: Kind }) { @@ -150,7 +144,7 @@ function InnerEditPeakShapeModal(props: Required) { <>