react-cheminfo 0.70.0 → 0.71.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/README.md +57 -2
  2. package/lib/about/core/providers.d.ts +6 -0
  3. package/lib/about/core/providers.d.ts.map +1 -1
  4. package/lib/about/core/providers.js +7 -0
  5. package/lib/about/core/providers.js.map +1 -1
  6. package/lib/about/ui/AboutProvidedBy.js +34 -0
  7. package/lib/about/ui/AboutProvidedBy.js.map +1 -1
  8. package/lib/download/core/figurePng.d.ts +14 -0
  9. package/lib/download/core/figurePng.d.ts.map +1 -1
  10. package/lib/download/core/figurePng.js +21 -6
  11. package/lib/download/core/figurePng.js.map +1 -1
  12. package/lib/download/core/svgToPng.d.ts.map +1 -1
  13. package/lib/download/core/svgToPng.js +4 -2
  14. package/lib/download/core/svgToPng.js.map +1 -1
  15. package/lib/format/core/relativeTime.d.ts +8 -3
  16. package/lib/format/core/relativeTime.d.ts.map +1 -1
  17. package/lib/format/core/relativeTime.js +19 -7
  18. package/lib/format/core/relativeTime.js.map +1 -1
  19. package/lib/host/core/connectHost.d.ts +25 -34
  20. package/lib/host/core/connectHost.d.ts.map +1 -1
  21. package/lib/host/core/connectHost.js +43 -89
  22. package/lib/host/core/connectHost.js.map +1 -1
  23. package/lib/host/core/hostOutputs.d.ts +58 -0
  24. package/lib/host/core/hostOutputs.d.ts.map +1 -0
  25. package/lib/host/core/hostOutputs.js +151 -0
  26. package/lib/host/core/hostOutputs.js.map +1 -0
  27. package/lib/host/core/index.d.ts +9 -3
  28. package/lib/host/core/index.d.ts.map +1 -1
  29. package/lib/host/core/index.js +4 -1
  30. package/lib/host/core/index.js.map +1 -1
  31. package/lib/host/core/irKinds.d.ts +124 -0
  32. package/lib/host/core/irKinds.d.ts.map +1 -0
  33. package/lib/host/core/irKinds.js +46 -0
  34. package/lib/host/core/irKinds.js.map +1 -0
  35. package/lib/host/core/kinds.d.ts +13 -5
  36. package/lib/host/core/kinds.d.ts.map +1 -1
  37. package/lib/host/core/kinds.js +11 -5
  38. package/lib/host/core/kinds.js.map +1 -1
  39. package/lib/host/core/protocol.d.ts +50 -7
  40. package/lib/host/core/protocol.d.ts.map +1 -1
  41. package/lib/host/core/protocol.js +10 -6
  42. package/lib/host/core/protocol.js.map +1 -1
  43. package/lib/host/core/selectInputs.d.ts +37 -0
  44. package/lib/host/core/selectInputs.d.ts.map +1 -0
  45. package/lib/host/core/selectInputs.js +110 -0
  46. package/lib/host/core/selectInputs.js.map +1 -0
  47. package/lib/host/testing/sciPeaks.d.ts +31 -0
  48. package/lib/host/testing/sciPeaks.d.ts.map +1 -1
  49. package/lib/host/testing/sciPeaks.js +73 -0
  50. package/lib/host/testing/sciPeaks.js.map +1 -1
  51. package/lib/host/testing/standInHost.d.ts +9 -1
  52. package/lib/host/testing/standInHost.d.ts.map +1 -1
  53. package/lib/host/testing/standInHost.js +19 -1
  54. package/lib/host/testing/standInHost.js.map +1 -1
  55. package/lib/host/ui/useHostConnection.d.ts +13 -5
  56. package/lib/host/ui/useHostConnection.d.ts.map +1 -1
  57. package/lib/host/ui/useHostConnection.js +4 -3
  58. package/lib/host/ui/useHostConnection.js.map +1 -1
  59. package/lib/host-testing.d.ts +2 -2
  60. package/lib/host-testing.d.ts.map +1 -1
  61. package/lib/host-testing.js +2 -2
  62. package/lib/host-testing.js.map +1 -1
  63. package/lib/i18n/core/chromeCatalog.d.ts +1 -1
  64. package/lib/i18n/core/chromeCatalog.d.ts.map +1 -1
  65. package/lib/ir/core/handPicking.d.ts +58 -0
  66. package/lib/ir/core/handPicking.d.ts.map +1 -0
  67. package/lib/ir/core/handPicking.js +130 -0
  68. package/lib/ir/core/handPicking.js.map +1 -0
  69. package/lib/ir/core/index.d.ts +3 -0
  70. package/lib/ir/core/index.d.ts.map +1 -1
  71. package/lib/ir/core/index.js +3 -0
  72. package/lib/ir/core/index.js.map +1 -1
  73. package/lib/ir/core/irBandState.d.ts +57 -0
  74. package/lib/ir/core/irBandState.d.ts.map +1 -0
  75. package/lib/ir/core/irBandState.js +119 -0
  76. package/lib/ir/core/irBandState.js.map +1 -0
  77. package/lib/ir/core/irCommands.d.ts +11 -0
  78. package/lib/ir/core/irCommands.d.ts.map +1 -1
  79. package/lib/ir/core/irCommands.js +6 -0
  80. package/lib/ir/core/irCommands.js.map +1 -1
  81. package/lib/ir/core/irPeaks.d.ts +28 -0
  82. package/lib/ir/core/irPeaks.d.ts.map +1 -0
  83. package/lib/ir/core/irPeaks.js +66 -0
  84. package/lib/ir/core/irPeaks.js.map +1 -0
  85. package/lib/ir/core/irReducer.d.ts +8 -0
  86. package/lib/ir/core/irReducer.d.ts.map +1 -1
  87. package/lib/ir/core/irReducer.js +15 -32
  88. package/lib/ir/core/irReducer.js.map +1 -1
  89. package/lib/ir/core/irSpectrum.d.ts +15 -0
  90. package/lib/ir/core/irSpectrum.d.ts.map +1 -1
  91. package/lib/ir/core/irSpectrum.js.map +1 -1
  92. package/lib/ir/core/irState.d.ts +9 -6
  93. package/lib/ir/core/irState.d.ts.map +1 -1
  94. package/lib/ir/core/irState.js +10 -6
  95. package/lib/ir/core/irState.js.map +1 -1
  96. package/lib/ir/core/pickBands.d.ts +14 -0
  97. package/lib/ir/core/pickBands.d.ts.map +1 -1
  98. package/lib/ir/core/pickBands.js +39 -33
  99. package/lib/ir/core/pickBands.js.map +1 -1
  100. package/lib/ir/core/ramanBands.d.ts +9 -1
  101. package/lib/ir/core/ramanBands.d.ts.map +1 -1
  102. package/lib/ir/core/ramanBands.js +2 -2
  103. package/lib/ir/core/ramanBands.js.map +1 -1
  104. package/lib/ir/core/spectrumBands.d.ts +46 -0
  105. package/lib/ir/core/spectrumBands.d.ts.map +1 -0
  106. package/lib/ir/core/spectrumBands.js +98 -0
  107. package/lib/ir/core/spectrumBands.js.map +1 -0
  108. package/lib/ir/ui/BandRow.d.ts +5 -0
  109. package/lib/ir/ui/BandRow.d.ts.map +1 -1
  110. package/lib/ir/ui/BandRow.js +4 -3
  111. package/lib/ir/ui/BandRow.js.map +1 -1
  112. package/lib/ir/ui/BandsPanel.d.ts +5 -0
  113. package/lib/ir/ui/BandsPanel.d.ts.map +1 -1
  114. package/lib/ir/ui/BandsPanel.js +36 -16
  115. package/lib/ir/ui/BandsPanel.js.map +1 -1
  116. package/lib/ir/ui/BandsToolbar.d.ts +9 -0
  117. package/lib/ir/ui/BandsToolbar.d.ts.map +1 -0
  118. package/lib/ir/ui/BandsToolbar.js +61 -0
  119. package/lib/ir/ui/BandsToolbar.js.map +1 -0
  120. package/lib/ir/ui/IrCanvas.d.ts +2 -1
  121. package/lib/ir/ui/IrCanvas.d.ts.map +1 -1
  122. package/lib/ir/ui/IrCanvas.js +19 -5
  123. package/lib/ir/ui/IrCanvas.js.map +1 -1
  124. package/lib/ir/ui/IrChart.d.ts.map +1 -1
  125. package/lib/ir/ui/IrChart.js +8 -1
  126. package/lib/ir/ui/IrChart.js.map +1 -1
  127. package/lib/ir/ui/IrDocumentationDialog.js +1 -1
  128. package/lib/ir/ui/IrDocumentationDialog.js.map +1 -1
  129. package/lib/ir/ui/IrEditor.d.ts +4 -2
  130. package/lib/ir/ui/IrEditor.d.ts.map +1 -1
  131. package/lib/ir/ui/IrEditor.js.map +1 -1
  132. package/lib/ir/ui/IrShell.d.ts.map +1 -1
  133. package/lib/ir/ui/IrShell.js +6 -0
  134. package/lib/ir/ui/IrShell.js.map +1 -1
  135. package/lib/ir/ui/IrStatusBar.d.ts.map +1 -1
  136. package/lib/ir/ui/IrStatusBar.js +1 -1
  137. package/lib/ir/ui/IrStatusBar.js.map +1 -1
  138. package/lib/ir/ui/IrToolbar.d.ts +4 -3
  139. package/lib/ir/ui/IrToolbar.d.ts.map +1 -1
  140. package/lib/ir/ui/IrToolbar.js +5 -4
  141. package/lib/ir/ui/IrToolbar.js.map +1 -1
  142. package/lib/ir/ui/VibrationalEditor.d.ts +2 -1
  143. package/lib/ir/ui/VibrationalEditor.d.ts.map +1 -1
  144. package/lib/ir/ui/VibrationalEditor.js.map +1 -1
  145. package/lib/ir/ui/irActions.d.ts +5 -0
  146. package/lib/ir/ui/irActions.d.ts.map +1 -1
  147. package/lib/ir/ui/irActions.js +6 -0
  148. package/lib/ir/ui/irActions.js.map +1 -1
  149. package/lib/ir/ui/irChartProps.d.ts +4 -1
  150. package/lib/ir/ui/irChartProps.d.ts.map +1 -1
  151. package/lib/ir/ui/useIrEditor.d.ts +5 -11
  152. package/lib/ir/ui/useIrEditor.d.ts.map +1 -1
  153. package/lib/ir/ui/useIrEditor.js +17 -18
  154. package/lib/ir/ui/useIrEditor.js.map +1 -1
  155. package/lib/locales/de.json +63 -0
  156. package/lib/locales/en.json +63 -0
  157. package/lib/locales/es.json +63 -0
  158. package/lib/locales/fr.json +63 -0
  159. package/lib/locales/it.json +63 -0
  160. package/lib/slides/ui/slideSanitizeSchema.d.ts +3 -2
  161. package/lib/slides/ui/slideSanitizeSchema.d.ts.map +1 -1
  162. package/lib/slides/ui/slideSanitizeSchema.js +82 -2
  163. package/lib/slides/ui/slideSanitizeSchema.js.map +1 -1
  164. package/package.json +1 -1
  165. package/src/about/core/providers.ts +7 -0
  166. package/src/about/ui/AboutProvidedBy.tsx +49 -0
  167. package/src/download/core/figurePng.ts +21 -8
  168. package/src/download/core/svgToPng.ts +3 -4
  169. package/src/format/core/relativeTime.ts +29 -7
  170. package/src/host/core/connectHost.ts +77 -120
  171. package/src/host/core/hostOutputs.ts +197 -0
  172. package/src/host/core/index.ts +13 -7
  173. package/src/host/core/irKinds.ts +92 -0
  174. package/src/host/core/kinds.ts +16 -6
  175. package/src/host/core/protocol.ts +50 -8
  176. package/src/host/core/selectInputs.ts +145 -0
  177. package/src/host/testing/sciPeaks.ts +140 -0
  178. package/src/host/testing/standInHost.ts +30 -5
  179. package/src/host/ui/useHostConnection.ts +17 -8
  180. package/src/host-testing.ts +4 -0
  181. package/src/ir/core/handPicking.ts +154 -0
  182. package/src/ir/core/index.ts +14 -0
  183. package/src/ir/core/irBandState.ts +155 -0
  184. package/src/ir/core/irCommands.ts +7 -0
  185. package/src/ir/core/irPeaks.ts +79 -0
  186. package/src/ir/core/irReducer.ts +27 -34
  187. package/src/ir/core/irSpectrum.ts +16 -0
  188. package/src/ir/core/irState.ts +15 -8
  189. package/src/ir/core/pickBands.ts +46 -39
  190. package/src/ir/core/ramanBands.ts +2 -2
  191. package/src/ir/core/spectrumBands.ts +113 -0
  192. package/src/ir/ui/BandRow.tsx +20 -2
  193. package/src/ir/ui/BandsPanel.tsx +46 -88
  194. package/src/ir/ui/BandsToolbar.tsx +152 -0
  195. package/src/ir/ui/IrCanvas.tsx +22 -11
  196. package/src/ir/ui/IrChart.tsx +10 -1
  197. package/src/ir/ui/IrDocumentationDialog.tsx +7 -0
  198. package/src/ir/ui/IrEditor.tsx +4 -2
  199. package/src/ir/ui/IrShell.tsx +5 -0
  200. package/src/ir/ui/IrStatusBar.tsx +4 -1
  201. package/src/ir/ui/IrToolbar.tsx +14 -5
  202. package/src/ir/ui/VibrationalEditor.tsx +2 -1
  203. package/src/ir/ui/irActions.ts +11 -0
  204. package/src/ir/ui/irChartProps.ts +4 -1
  205. package/src/ir/ui/useIrEditor.ts +21 -21
  206. package/src/locales/de.json +63 -0
  207. package/src/locales/en.json +63 -0
  208. package/src/locales/es.json +63 -0
  209. package/src/locales/fr.json +63 -0
  210. package/src/locales/it.json +63 -0
  211. package/src/slides/ui/slideSanitizeSchema.ts +86 -2
  212. package/styles/chrome.css +7 -0
  213. package/styles/slides.css +47 -0
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useMemo } from 'react';
2
2
 
3
+ import { editBandsAt } from '../core/handPicking.ts';
3
4
  import type { IrBand } from '../core/irBand.ts';
4
5
  import type { IrPlotClick } from '../core/irEditorApi.ts';
5
6
  import type { IrDomain } from '../core/irSpectrum.ts';
@@ -35,7 +36,8 @@ export interface IrCanvasProps {
35
36
  chartId?: string;
36
37
  /**
37
38
  * Told of a click on the plot — a press let go without travelling, never the
38
- * end of a drag — with the spectrum selected at that moment.
39
+ * end of a drag — with the spectrum selected at that moment. Not told while
40
+ * bands are picked by hand, which is what a click is then.
39
41
  * @default undefined
40
42
  */
41
43
  onPlotClick?: (click: IrPlotClick) => void;
@@ -64,7 +66,8 @@ export function IrCanvas(props: IrCanvasProps) {
64
66
  chartId,
65
67
  onPlotClick,
66
68
  } = props;
67
- const { state, visibleSpectra, assigned } = useIrEditorState();
69
+ const { state, visibleSpectra, selectedSpectrum, assigned } =
70
+ useIrEditorState();
68
71
  const actions = useIrActions();
69
72
  const { domain, tool, selectedId } = state.view;
70
73
  const { technique, mode, showAssignments, labelLimit } = state.settings;
@@ -77,15 +80,23 @@ export function IrCanvas(props: IrCanvasProps) {
77
80
  );
78
81
 
79
82
  // Which spectrum was selected is the viewer's to say, not the chart's: the
80
- // chart only knows where the press landed.
81
- const onClickAt = useMemo(
82
- () =>
83
- onPlotClick === undefined
84
- ? undefined
85
- : (wavenumber: number) =>
86
- onPlotClick({ wavenumber, spectrumId: selectedId }),
87
- [onPlotClick, selectedId],
88
- );
83
+ // chart only knows where the press landed. While picking by hand a click is
84
+ // a band, and is the viewer's own rather than the host's.
85
+ const onClickAt = useMemo(() => {
86
+ if (tool === 'pick') {
87
+ if (selectedSpectrum === null) return undefined;
88
+ return (wavenumber: number, reach: number) => {
89
+ const bands = editBandsAt(selectedSpectrum, wavenumber, reach);
90
+ if (bands !== selectedSpectrum.bands) {
91
+ actions.setBands(selectedSpectrum.id, bands);
92
+ }
93
+ };
94
+ }
95
+ return onPlotClick === undefined
96
+ ? undefined
97
+ : (wavenumber: number) =>
98
+ onPlotClick({ wavenumber, spectrumId: selectedId });
99
+ }, [tool, selectedSpectrum, actions, onPlotClick, selectedId]);
89
100
 
90
101
  return (
91
102
  <div style={rootStyle}>
@@ -11,6 +11,7 @@ import { useChartSize } from '../../chart/ui/useChartSize.ts';
11
11
  import type { AssignedBand } from '../core/assignBands.ts';
12
12
  import type { IrBand } from '../core/irBand.ts';
13
13
  import { irModeFor } from '../core/irTechnique.ts';
14
+ import { BAND_HIT_RADIUS } from '../core/nearestBand.ts';
14
15
 
15
16
  import { BandMarks } from './BandMarks.tsx';
16
17
  import { IrPointerTracker } from './IrPointerTracker.tsx';
@@ -83,11 +84,15 @@ export function IrChart(props: IrChartProps): ReactElement {
83
84
  const frameWidth = plot.right + MARGIN.right;
84
85
  const frameHeight = plot.bottom + MARGIN.bottom;
85
86
 
87
+ // The scale a click is measured against comes out of the zoom the click is
88
+ // handed to, so the handler reads the latest one when the click happens.
89
+ const xFactor = useRef(0);
86
90
  const onClick = useMemo(
87
91
  () =>
88
92
  onPlotClick === undefined
89
93
  ? undefined
90
- : (point: ChartPoint) => onPlotClick(point.x),
94
+ : (point: ChartPoint) =>
95
+ onPlotClick(point.x, BAND_HIT_RADIUS / Math.abs(xFactor.current)),
91
96
  [onPlotClick],
92
97
  );
93
98
 
@@ -111,6 +116,10 @@ export function IrChart(props: IrChartProps): ReactElement {
111
116
  // was dragged over.
112
117
  const { domain: shown, xScale, yScale, selection, handlers } = zoom;
113
118
 
119
+ useLayoutEffect(() => {
120
+ xFactor.current = xScale.factor;
121
+ }, [xScale]);
122
+
114
123
  const reported = useRef(shown);
115
124
  // Before the browser paints, so a host that stores the window and hands it
116
125
  // back does so in the frame the gesture landed in rather than the one after.
@@ -111,6 +111,13 @@ function InfraredGuide() {
111
111
  a row marks the band on the chart, and hovering the chart lights the
112
112
  row.
113
113
  </p>
114
+ <p style={paragraphStyle}>
115
+ The bands are found automatically until you pick them by hand: with the
116
+ hand tool a click on the chart adds the band under it, at its top, and a
117
+ click on a mark takes it away, as does the cross at the end of its row.
118
+ From then on the threshold leaves the list alone, until it is found
119
+ automatically again.
120
+ </p>
114
121
  <p style={paragraphStyle}>
115
122
  A band is reported at the sampled point it peaked on, so on a 4 cm⁻¹
116
123
  grid a carbonyl centred at 1710 reads 1708 or 1712. Do not quote one as
@@ -18,7 +18,8 @@ export interface IrEditorProps {
18
18
  data?: IrSpectrum[];
19
19
  /**
20
20
  * Called whenever the spectra open change — one loaded, one closed, the chart
21
- * cleared — with the spectra now open.
21
+ * cleared, the bands of one picked — with the spectra now open, each carrying
22
+ * its bands.
22
23
  *
23
24
  * The document only. A zoom, a selection, a rename and a colour change what is
24
25
  * being looked at rather than what is loaded, and a host told about those would
@@ -50,7 +51,8 @@ export interface IrEditorProps {
50
51
  panels?: readonly IrHostPanel[];
51
52
  /**
52
53
  * The plot was clicked at this wavenumber (a press that did not travel, i.e.
53
- * not a zoom drag), with the spectrum selected at that moment.
54
+ * not a zoom drag), with the spectrum selected at that moment. Not called
55
+ * while bands are picked by hand: a click is a band then.
54
56
  * @default undefined
55
57
  */
56
58
  onPlotClick?: (click: IrPlotClick) => void;
@@ -92,6 +92,11 @@ export function IrShell(props: IrShellProps) {
92
92
  fullScreen: toggleFullScreen,
93
93
  readTool: () => actions.setTool('read'),
94
94
  boxTool: () => actions.setTool(tool === 'box' ? 'read' : 'box'),
95
+ // Picking by hand is done on the marks, so they are shown for it.
96
+ pickTool: () => {
97
+ actions.setTool(tool === 'pick' ? 'read' : 'pick');
98
+ if (!pickBands) actions.setSettings({ pickBands: true });
99
+ },
95
100
  zoomIn,
96
101
  zoomOut,
97
102
  resetZoom: () => actions.resetDomain(),
@@ -63,11 +63,14 @@ export function IrStatusBar() {
63
63
  {pickBands && assigned.length > 0 ? (
64
64
  <span style={itemStyle}>
65
65
  {countLabel(assigned.length, 'band', 'bands')}
66
+ {selectedSpectrum?.bandsByHand ? ' picked by hand' : null}
66
67
  {assignments ? `, ${countAssigned(assigned)} assigned` : null}
67
68
  </span>
68
69
  ) : null}
69
70
 
70
- {boxZoomOnly(mode) || tool === 'box' ? (
71
+ {tool === 'pick' ? (
72
+ <span style={mutedStyle}>click to add or remove a band</span>
73
+ ) : boxZoomOnly(mode) || tool === 'box' ? (
71
74
  <span style={mutedStyle}>square zoom: drag a rectangle</span>
72
75
  ) : null}
73
76
 
@@ -22,9 +22,10 @@ export interface IrToolbarProps {
22
22
  * The tools, down the left of the chart.
23
23
  *
24
24
  * Only what is done to the spectra: what is asked of the viewer itself — the
25
- * mark, help, full screen — is in the header. The two **tools** — read and
26
- * square zoom — come first and show which of the two a drag is currently
27
- * running, then the zoom commands that act at once, then what is being read: the
25
+ * mark, help, full screen — is in the header. The **tools** — read and square
26
+ * zoom, which say what a drag is running, and picking by hand, which says what
27
+ * a click is — come first, then the zoom commands that act at once, then what
28
+ * is being read: the
28
29
  * value axis, the picking, the assignments. Closing every spectrum is last and
29
30
  * alone, being the only one that throws something away.
30
31
  *
@@ -64,10 +65,12 @@ export function IrToolbar(props: IrToolbarProps) {
64
65
  disabled={isEmpty}
65
66
  />
66
67
  {/* The read tool drags along one axis, so it is drawn as that; the box
67
- tool sweeps a rectangle, which is what a marquee icon means. */}
68
+ tool sweeps a rectangle, which is what a marquee icon means. Picking
69
+ by hand is about a click, and a drag still reads while it is chosen,
70
+ so the two light together. */}
68
71
  <Toolbar.Item
69
72
  icon="arrows-horizontal"
70
- active={!settled && tool === 'read'}
73
+ active={!settled && tool !== 'box'}
71
74
  {...command('readTool')}
72
75
  disabled={isEmpty || settled}
73
76
  />
@@ -77,6 +80,12 @@ export function IrToolbar(props: IrToolbarProps) {
77
80
  {...command('boxTool')}
78
81
  disabled={isEmpty || settled}
79
82
  />
83
+ <Toolbar.Item
84
+ icon="add-location"
85
+ active={tool === 'pick'}
86
+ {...command('pickTool')}
87
+ disabled={selectedSpectrum === null}
88
+ />
80
89
 
81
90
  <Toolbar.Item icon="zoom-in" {...command('zoomIn')} disabled={isEmpty} />
82
91
  <Toolbar.Item
@@ -48,7 +48,8 @@ export interface VibrationalEditorProps {
48
48
  panels?: readonly IrHostPanel[];
49
49
  /**
50
50
  * The plot was clicked at this wavenumber (a press that did not travel, i.e.
51
- * not a zoom drag), with the spectrum selected at that moment.
51
+ * not a zoom drag), with the spectrum selected at that moment. Not called
52
+ * while bands are picked by hand.
52
53
  * @default undefined
53
54
  */
54
55
  onPlotClick?: (click: IrPlotClick) => void;
@@ -8,6 +8,7 @@
8
8
 
9
9
  import type { Dispatch } from 'react';
10
10
 
11
+ import type { IrBand } from '../core/irBand.ts';
11
12
  import type { IrAction } from '../core/irReducer.ts';
12
13
  import type { IrPanelId } from '../core/irSidePanels.ts';
13
14
  import type { IrDomain, IrSpectrum } from '../core/irSpectrum.ts';
@@ -27,6 +28,10 @@ export interface IrEditorActions {
27
28
  setSpectrumColor: (id: string, color: string) => void;
28
29
  /** Draw one, or stop drawing it. */
29
30
  setSpectrumVisible: (id: string, visible: boolean) => void;
31
+ /** Give one these bands, as picked by hand. */
32
+ setBands: (id: string, bands: IrBand[]) => void;
33
+ /** Have one's bands found automatically again, with the settings in force. */
34
+ resetBands: (id: string) => void;
30
35
  /** Point the panels at one, or at none. */
31
36
  selectSpectrum: (id: string | null) => void;
32
37
  /** Show this window. */
@@ -72,6 +77,12 @@ export function createIrActions(dispatch: Dispatch<IrAction>): IrEditorActions {
72
77
  setSpectrumVisible(id, visible) {
73
78
  dispatch({ type: 'setSpectrumVisible', id, visible });
74
79
  },
80
+ setBands(id, bands) {
81
+ dispatch({ type: 'setBands', id, bands });
82
+ },
83
+ resetBands(id) {
84
+ dispatch({ type: 'resetBands', id });
85
+ },
75
86
  selectSpectrum(id) {
76
87
  dispatch({ type: 'selectSpectrum', id });
77
88
  },
@@ -98,9 +98,12 @@ export interface IrChartProps {
98
98
  * Told the wavenumber a click on the plot landed at: a press let go over the
99
99
  * plot without travelling. The end of a drag is never one, nor is a press let
100
100
  * go over the axis labels, which inverts to a value the chart is not drawing.
101
+ *
102
+ * `reach` is how many cm⁻¹ a band's hit radius spans at the zoom the chart is
103
+ * at, so a band the click was aimed at can be told from one it was not.
101
104
  * @default undefined
102
105
  */
103
- onPlotClick?: (wavenumber: number) => void;
106
+ onPlotClick?: (wavenumber: number, reach: number) => void;
104
107
  /**
105
108
  * What a drag across the plot asks for: the wavenumber axis alone, or exactly
106
109
  * the rectangle it sweeps out. A viewer offering a square-zoom tool switches
@@ -1,16 +1,10 @@
1
1
  /**
2
2
  * One viewer's state, and everything derived from it.
3
3
  *
4
- * The bands are derived here rather than held, and that is the one structural
5
- * decision in this file. Picking is a pure function of a spectrum and the
6
- * picking settings, so a band held in state would be an answer that has to be
7
- * invalidated by hand every time either changes — and the version that is wrong
8
- * is the one drawn on the chart. Derived, a stale band cannot exist.
9
- *
10
- * It is memoised on the selected spectrum rather than on all of them: picking
11
- * runs a peak detection over a few thousand points, and doing it for six loaded
12
- * spectra when the panels only ever show one would cost six times what it needs
13
- * to on every change of a threshold.
4
+ * The bands are the selected spectrum's own: the reducer picks them when it
5
+ * arrives and keeps them on it, so a band picked by hand is still there the
6
+ * next time it is selected, and goes to the host with it. What is derived here
7
+ * is what each band could be, which the correlation table answers per band.
14
8
  */
15
9
 
16
10
  import { useEffect, useMemo, useReducer, useRef } from 'react';
@@ -19,12 +13,12 @@ import type { AssignedBand } from '../core/assignBands.ts';
19
13
  import { assignBands } from '../core/assignBands.ts';
20
14
  import type { BandAssignment } from '../core/bandAssignments.ts';
21
15
  import { BAND_ASSIGNMENTS } from '../core/bandAssignments.ts';
16
+ import type { IrBand } from '../core/irBand.ts';
22
17
  import { irReducer } from '../core/irReducer.ts';
23
18
  import type { IrSpectrum } from '../core/irSpectrum.ts';
24
19
  import type { CreateIrStateOptions, IrState } from '../core/irState.ts';
25
20
  import { createIrState } from '../core/irState.ts';
26
21
  import { IR_TECHNIQUE_RULES } from '../core/irTechnique.ts';
27
- import { pickBands } from '../core/pickBands.ts';
28
22
 
29
23
  import type { IrEditorActions } from './irActions.ts';
30
24
  import { createIrActions } from './irActions.ts';
@@ -33,7 +27,7 @@ import { createIrActions } from './irActions.ts';
33
27
  export interface UseIrEditorOptions extends CreateIrStateOptions {
34
28
  /**
35
29
  * Called whenever the spectra open change — one loaded, one closed, the chart
36
- * cleared — with the spectra now open.
30
+ * cleared, the bands of one picked — with the spectra now open.
37
31
  * @default undefined
38
32
  */
39
33
  onChange?: (spectra: IrSpectrum[]) => void;
@@ -70,8 +64,7 @@ export function useIrEditor(options: UseIrEditorOptions = {}): IrEditorApi {
70
64
 
71
65
  const { spectra } = state.data;
72
66
  const { selectedId } = state.view;
73
- const { pickBands: picking, picking: pickingOptions } = state.settings;
74
- const { assignmentTolerance, technique } = state.settings;
67
+ const { pickBands: picking, assignmentTolerance, technique } = state.settings;
75
68
 
76
69
  const visibleSpectra = useMemo(() => {
77
70
  const visible: IrSpectrum[] = [];
@@ -92,9 +85,9 @@ export function useIrEditor(options: UseIrEditorOptions = {}): IrEditorApi {
92
85
  const bands = useMemo(
93
86
  () =>
94
87
  selectedSpectrum === null || !picking
95
- ? []
96
- : pickBands(selectedSpectrum, pickingOptions),
97
- [selectedSpectrum, picking, pickingOptions],
88
+ ? NO_BANDS
89
+ : (selectedSpectrum.bands ?? NO_BANDS),
90
+ [selectedSpectrum, picking],
98
91
  );
99
92
 
100
93
  // Assigning is a walk of thirty ranges per band, which is nothing beside the
@@ -130,8 +123,15 @@ export function useIrEditor(options: UseIrEditorOptions = {}): IrEditorApi {
130
123
  /** The table a technique the correlation table does not cover is matched against. */
131
124
  const NO_ASSIGNMENTS: readonly BandAssignment[] = [];
132
125
 
126
+ /** What is shown while nothing is selected or picking is off, kept stable. */
127
+ const NO_BANDS: readonly IrBand[] = [];
128
+
133
129
  /**
134
- * Whether two lists hold the same spectra, by identity and in order.
130
+ * Whether two lists hold the same spectra, with the same bands, in order.
131
+ *
132
+ * By identity, and the bands by reference: a rename or a colour builds a new
133
+ * spectrum around the same list, so neither is reported, while a band picked
134
+ * builds a new list.
135
135
  * @param current - What was reported last.
136
136
  * @param next - What is open now.
137
137
  * @returns Whether the report can be skipped.
@@ -143,9 +143,9 @@ function sameSpectra(
143
143
  if (current === next) return true;
144
144
  if (current.length !== next.length) return false;
145
145
  for (let index = 0; index < current.length; index++) {
146
- if ((current[index] as IrSpectrum).id !== (next[index] as IrSpectrum).id) {
147
- return false;
148
- }
146
+ const before = current[index] as IrSpectrum;
147
+ const after = next[index] as IrSpectrum;
148
+ if (before.id !== after.id || before.bands !== after.bands) return false;
149
149
  }
150
150
  return true;
151
151
  }
@@ -8,6 +8,69 @@
8
8
  "account.signIn": "Anmelden",
9
9
  "account.signOut": "Abmelden",
10
10
  "account.signedInAs": "Angemeldet als {name}",
11
+ "account.username": "Benutzername",
12
+ "account.password": "Passwort",
13
+ "account.role": "Rolle",
14
+ "account.role.read": "Lesen",
15
+ "account.role.write": "Lesen und Schreiben",
16
+ "account.role.admin": "Administrator",
17
+ "account.you": "Sie",
18
+ "account.envManaged": "in .env festgelegt",
19
+ "account.envManagedTitle": "ADMIN_USERNAME und ADMIN_PASSWORD in der .env des Servers legen dieses Konto fest, es wird dort geändert.",
20
+ "account.accounts": "Konten",
21
+ "account.accountsNote": "Lesen sieht die Daten, Lesen und Schreiben ändert sie auch, und Administrator verwaltet zudem diese Konten.",
22
+ "account.column.added": "Hinzugefügt",
23
+ "account.column.lastSignIn": "Letzte Anmeldung",
24
+ "account.column.passwordChanged": "Passwort geändert",
25
+ "account.column.actions": "Ändern",
26
+ "account.never": "nie",
27
+ "account.add": "Konto hinzufügen",
28
+ "account.addSubmit": "Das Konto hinzufügen",
29
+ "account.added": "{username} kann sich jetzt anmelden.",
30
+ "account.usernameHelp": "Buchstaben, Ziffern, Punkte, Bindestriche und Unterstriche.",
31
+ "account.setPassword": "Passwort setzen",
32
+ "account.setPasswordTitle": "Das Passwort von {username} setzen",
33
+ "account.setPasswordNote": "{username} wird überall abgemeldet und meldet sich ab jetzt mit diesem Passwort an.",
34
+ "account.setPasswordSubmit": "Das Passwort setzen",
35
+ "account.roleOf": "Rolle von {username}",
36
+ "account.changePassword": "Ihr Passwort ändern",
37
+ "account.currentPassword": "Aktuelles Passwort",
38
+ "account.newPassword": "Neues Passwort",
39
+ "account.repeatPassword": "Neues Passwort, noch einmal",
40
+ "account.passwordsDiffer": "Die beiden neuen Passwörter unterscheiden sich.",
41
+ "account.passwordChanged": "Ihr Passwort ist geändert. Andere Browser, die mit Ihrem Konto angemeldet waren, sind abgemeldet.",
42
+ "account.ownEnvManaged": "Die .env des Servers legt Ihr Konto fest: Ändern Sie ADMIN_PASSWORD dort und starten Sie den Server neu.",
43
+ "account.remove": "Entfernen",
44
+ "account.removeQuestion": "{username} entfernen? Das Konto kann sich dann nicht mehr anmelden.",
45
+ "account.needsWrite": "Diese Seite ändert die Daten, und {username} darf sie nur lesen.",
46
+ "account.needsAdmin": "Diese Seite ist für Administratoren, und {username} ist keiner.",
47
+ "account.switchAccount": "Mit einem anderen Konto anmelden",
48
+ "account.unreachable": "Der Server ist nicht erreichbar. Versuchen Sie es gleich noch einmal.",
49
+ "account.refusal.wrongCredentials": "Falscher Benutzername oder falsches Passwort.",
50
+ "account.refusal.tooManyAttempts": "Zu viele Anmeldeversuche von hier. Versuchen Sie es in 15 Minuten wieder.",
51
+ "account.refusal.signInElsewhere": "Die Anmeldung geschieht auf den Seiten dieser Website.",
52
+ "account.refusal.signOutElsewhere": "Die Abmeldung geschieht auf den Seiten dieser Website.",
53
+ "account.refusal.changeElsewhere": "Änderungen geschehen von den Seiten dieser Website aus.",
54
+ "account.refusal.signInRequired": "Dies ist für die Konten dieser Website: Melden Sie sich an.",
55
+ "account.refusal.sessionEnded": "Die Sitzung ist zu Ende: Melden Sie sich erneut an.",
56
+ "account.refusal.wrongToken": "Das ist nicht das ADMIN_TOKEN dieses Servers.",
57
+ "account.refusal.noAdminToken": "Auf diesem Server ist kein ADMIN_TOKEN gesetzt: Melden Sie sich stattdessen an.",
58
+ "account.refusal.roleTooLow": "Ihr Konto darf das nicht: Fragen Sie einen Administrator.",
59
+ "account.refusal.usernameLength": "Ein Benutzername hat {min} bis {max} Zeichen.",
60
+ "account.refusal.usernameCharacters": "Ein Benutzername enthält Buchstaben, Ziffern, Punkte, Bindestriche und Unterstriche und beginnt mit einem Buchstaben oder einer Ziffer.",
61
+ "account.refusal.usernameTaken": "Dieser Benutzername ist vergeben.",
62
+ "account.refusal.passwordEmpty": "Ein Passwort darf nicht leer sein.",
63
+ "account.refusal.passwordTooLong": "Ein Passwort hat höchstens {max} Zeichen.",
64
+ "account.refusal.noSuchAccount": "Dieses Konto gibt es nicht mehr.",
65
+ "account.refusal.nothingToChange": "Senden Sie ein neues Passwort, eine neue Rolle oder beides.",
66
+ "account.refusal.useChangePassword": "Ändern Sie Ihr eigenes Passwort mit POST /password, das nach dem aktuellen fragt.",
67
+ "account.refusal.ownRole": "Sie können Ihre eigene Rolle nicht ändern.",
68
+ "account.refusal.removeYourself": "Sie können das Konto, das Sie verwenden, nicht entfernen.",
69
+ "account.refusal.lastAdmin": "Der letzte Administrator kann weder entfernt noch herabgestuft werden.",
70
+ "account.refusal.envManaged": "Die .env des Servers legt dieses Konto fest, es wird dort geändert.",
71
+ "account.refusal.tokenCannotChangePassword": "Ein Passwort ändert das angemeldete Konto, nicht das ADMIN_TOKEN.",
72
+ "account.refusal.wrongCurrentPassword": "Das aktuelle Passwort ist nicht dieses.",
73
+ "account.refusal.accountRemoved": "Das angemeldete Konto wurde entfernt.",
11
74
  "ecosystem.tools": "Werkzeuge",
12
75
  "ecosystem.menuHeading": "Unsere Werkzeuge, alle im Browser",
13
76
  "ecosystem.otherTools": "Unsere weiteren Werkzeuge",
@@ -8,6 +8,69 @@
8
8
  "account.signIn": "Sign in",
9
9
  "account.signOut": "Sign out",
10
10
  "account.signedInAs": "Signed in as {name}",
11
+ "account.username": "Username",
12
+ "account.password": "Password",
13
+ "account.role": "Role",
14
+ "account.role.read": "Read",
15
+ "account.role.write": "Read and write",
16
+ "account.role.admin": "Administrator",
17
+ "account.you": "you",
18
+ "account.envManaged": "defined in .env",
19
+ "account.envManagedTitle": "ADMIN_USERNAME and ADMIN_PASSWORD in the server's .env define this account, so it is changed there.",
20
+ "account.accounts": "Accounts",
21
+ "account.accountsNote": "Read sees the data, Read and write also changes it, and Administrator also manages these accounts.",
22
+ "account.column.added": "Added",
23
+ "account.column.lastSignIn": "Last sign-in",
24
+ "account.column.passwordChanged": "Password changed",
25
+ "account.column.actions": "Change",
26
+ "account.never": "never",
27
+ "account.add": "Add an account",
28
+ "account.addSubmit": "Add the account",
29
+ "account.added": "{username} can sign in now.",
30
+ "account.usernameHelp": "Letters, digits, dots, dashes and underscores.",
31
+ "account.setPassword": "Set password",
32
+ "account.setPasswordTitle": "Set the password of {username}",
33
+ "account.setPasswordNote": "{username} is signed out everywhere, and signs in with this password from now on.",
34
+ "account.setPasswordSubmit": "Set the password",
35
+ "account.roleOf": "Role of {username}",
36
+ "account.changePassword": "Change your password",
37
+ "account.currentPassword": "Current password",
38
+ "account.newPassword": "New password",
39
+ "account.repeatPassword": "New password, again",
40
+ "account.passwordsDiffer": "The two new passwords differ.",
41
+ "account.passwordChanged": "Your password is changed. Other browsers signed in as you are signed out.",
42
+ "account.ownEnvManaged": "The server's .env defines your account: change ADMIN_PASSWORD there, then restart the server.",
43
+ "account.remove": "Remove",
44
+ "account.removeQuestion": "Remove {username}? It can no longer sign in.",
45
+ "account.needsWrite": "This page changes the data, and {username} can only read it.",
46
+ "account.needsAdmin": "This page is for administrators, and {username} is not one.",
47
+ "account.switchAccount": "Sign in as somebody else",
48
+ "account.unreachable": "The server cannot be reached. Try again in a moment.",
49
+ "account.refusal.wrongCredentials": "Wrong username or password.",
50
+ "account.refusal.tooManyAttempts": "Too many sign-ins from here. Try again in 15 minutes.",
51
+ "account.refusal.signInElsewhere": "Signing in happens on this site's own pages.",
52
+ "account.refusal.signOutElsewhere": "Signing out happens on this site's own pages.",
53
+ "account.refusal.changeElsewhere": "Changes are made from this site's own pages.",
54
+ "account.refusal.signInRequired": "This is for the accounts of this site: sign in.",
55
+ "account.refusal.sessionEnded": "The session has ended: sign in again.",
56
+ "account.refusal.wrongToken": "That is not the ADMIN_TOKEN of this server.",
57
+ "account.refusal.noAdminToken": "No ADMIN_TOKEN is set on this server: sign in instead.",
58
+ "account.refusal.roleTooLow": "Your account cannot do this: ask an administrator.",
59
+ "account.refusal.usernameLength": "A username has {min} to {max} characters.",
60
+ "account.refusal.usernameCharacters": "A username holds letters, digits, dots, dashes and underscores, and starts with a letter or a digit.",
61
+ "account.refusal.usernameTaken": "That username is taken.",
62
+ "account.refusal.passwordEmpty": "A password cannot be empty.",
63
+ "account.refusal.passwordTooLong": "A password has at most {max} characters.",
64
+ "account.refusal.noSuchAccount": "That account no longer exists.",
65
+ "account.refusal.nothingToChange": "Send a new password, a new role, or both.",
66
+ "account.refusal.useChangePassword": "Change your own password with POST /password, which asks for the current one.",
67
+ "account.refusal.ownRole": "You cannot change your own role.",
68
+ "account.refusal.removeYourself": "You cannot remove the account you are using.",
69
+ "account.refusal.lastAdmin": "The last administrator can be neither removed nor demoted.",
70
+ "account.refusal.envManaged": "The server's .env defines this account, so it is changed there.",
71
+ "account.refusal.tokenCannotChangePassword": "A password is changed by the account signed in, not with the ADMIN_TOKEN.",
72
+ "account.refusal.wrongCurrentPassword": "The current password is not that one.",
73
+ "account.refusal.accountRemoved": "The account signed in was removed.",
11
74
  "ecosystem.tools": "Tools",
12
75
  "ecosystem.menuHeading": "Our tools, all in the browser",
13
76
  "ecosystem.otherTools": "Our other tools",
@@ -8,6 +8,69 @@
8
8
  "account.signIn": "Iniciar sesión",
9
9
  "account.signOut": "Cerrar sesión",
10
10
  "account.signedInAs": "Sesión iniciada como {name}",
11
+ "account.username": "Nombre de usuario",
12
+ "account.password": "Contraseña",
13
+ "account.role": "Rol",
14
+ "account.role.read": "Lectura",
15
+ "account.role.write": "Lectura y escritura",
16
+ "account.role.admin": "Administrador",
17
+ "account.you": "usted",
18
+ "account.envManaged": "definida en .env",
19
+ "account.envManagedTitle": "ADMIN_USERNAME y ADMIN_PASSWORD, en el .env del servidor, definen esta cuenta, que se cambia allí.",
20
+ "account.accounts": "Cuentas",
21
+ "account.accountsNote": "Lectura ve los datos, Lectura y escritura también los cambia, y Administrador también gestiona estas cuentas.",
22
+ "account.column.added": "Añadida",
23
+ "account.column.lastSignIn": "Último inicio de sesión",
24
+ "account.column.passwordChanged": "Contraseña cambiada",
25
+ "account.column.actions": "Cambiar",
26
+ "account.never": "nunca",
27
+ "account.add": "Añadir una cuenta",
28
+ "account.addSubmit": "Añadir la cuenta",
29
+ "account.added": "{username} ya puede iniciar sesión.",
30
+ "account.usernameHelp": "Letras, cifras, puntos, guiones y guiones bajos.",
31
+ "account.setPassword": "Fijar contraseña",
32
+ "account.setPasswordTitle": "Fijar la contraseña de {username}",
33
+ "account.setPasswordNote": "{username} queda desconectado en todas partes, e inicia sesión con esta contraseña a partir de ahora.",
34
+ "account.setPasswordSubmit": "Fijar la contraseña",
35
+ "account.roleOf": "Rol de {username}",
36
+ "account.changePassword": "Cambiar su contraseña",
37
+ "account.currentPassword": "Contraseña actual",
38
+ "account.newPassword": "Nueva contraseña",
39
+ "account.repeatPassword": "Nueva contraseña, otra vez",
40
+ "account.passwordsDiffer": "Las dos contraseñas nuevas no coinciden.",
41
+ "account.passwordChanged": "Su contraseña ha cambiado. Los demás navegadores con su sesión quedan desconectados.",
42
+ "account.ownEnvManaged": "El .env del servidor define su cuenta: cambie ADMIN_PASSWORD allí y reinicie el servidor.",
43
+ "account.remove": "Eliminar",
44
+ "account.removeQuestion": "¿Eliminar {username}? Ya no podrá iniciar sesión.",
45
+ "account.needsWrite": "Esta página cambia los datos, y {username} solo puede leerlos.",
46
+ "account.needsAdmin": "Esta página es para administradores, y {username} no lo es.",
47
+ "account.switchAccount": "Iniciar sesión con otra cuenta",
48
+ "account.unreachable": "No se puede contactar con el servidor. Inténtelo de nuevo en un momento.",
49
+ "account.refusal.wrongCredentials": "Nombre de usuario o contraseña incorrectos.",
50
+ "account.refusal.tooManyAttempts": "Demasiados intentos de inicio de sesión desde aquí. Inténtelo de nuevo dentro de 15 minutos.",
51
+ "account.refusal.signInElsewhere": "El inicio de sesión se hace desde las páginas de este sitio.",
52
+ "account.refusal.signOutElsewhere": "El cierre de sesión se hace desde las páginas de este sitio.",
53
+ "account.refusal.changeElsewhere": "Los cambios se hacen desde las páginas de este sitio.",
54
+ "account.refusal.signInRequired": "Esto es para las cuentas de este sitio: inicie sesión.",
55
+ "account.refusal.sessionEnded": "La sesión ha terminado: vuelva a iniciar sesión.",
56
+ "account.refusal.wrongToken": "Ese no es el ADMIN_TOKEN de este servidor.",
57
+ "account.refusal.noAdminToken": "Este servidor no tiene ningún ADMIN_TOKEN: inicie sesión en su lugar.",
58
+ "account.refusal.roleTooLow": "Su cuenta no puede hacer esto: pídaselo a un administrador.",
59
+ "account.refusal.usernameLength": "Un nombre de usuario tiene de {min} a {max} caracteres.",
60
+ "account.refusal.usernameCharacters": "Un nombre de usuario contiene letras, cifras, puntos, guiones y guiones bajos, y empieza por una letra o una cifra.",
61
+ "account.refusal.usernameTaken": "Ese nombre de usuario ya existe.",
62
+ "account.refusal.passwordEmpty": "Una contraseña no puede estar vacía.",
63
+ "account.refusal.passwordTooLong": "Una contraseña tiene como máximo {max} caracteres.",
64
+ "account.refusal.noSuchAccount": "Esa cuenta ya no existe.",
65
+ "account.refusal.nothingToChange": "Envíe una contraseña nueva, un rol nuevo, o ambos.",
66
+ "account.refusal.useChangePassword": "Cambie su propia contraseña con POST /password, que pide la actual.",
67
+ "account.refusal.ownRole": "No puede cambiar su propio rol.",
68
+ "account.refusal.removeYourself": "No puede eliminar la cuenta que está usando.",
69
+ "account.refusal.lastAdmin": "El último administrador no puede eliminarse ni degradarse.",
70
+ "account.refusal.envManaged": "El .env del servidor define esta cuenta, que se cambia allí.",
71
+ "account.refusal.tokenCannotChangePassword": "Una contraseña la cambia la cuenta con la sesión iniciada, no el ADMIN_TOKEN.",
72
+ "account.refusal.wrongCurrentPassword": "La contraseña actual no es esa.",
73
+ "account.refusal.accountRemoved": "La cuenta con la sesión iniciada fue eliminada.",
11
74
  "ecosystem.tools": "Herramientas",
12
75
  "ecosystem.menuHeading": "Nuestras herramientas, todas en el navegador",
13
76
  "ecosystem.otherTools": "Nuestras otras herramientas",