Skip to content

Commit 7be751b

Browse files
feat: manually add signal in a range from 2D
refactor: remove integration fix: clip the 1d trace signals fix: add signal in f2 trace fix: range and signal position
1 parent 989d4f0 commit 7be751b

10 files changed

Lines changed: 409 additions & 29 deletions

File tree

src/component/2d/1d-tracer/Left1DChart.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import type { Spectrum1D } from '@zakodium/nmrium-core';
2-
import { memo } from 'react';
2+
import { memo, useRef } from 'react';
33

44
import { useChartData } from '../../context/ChartContext.js';
55
import useXYReduce from '../../hooks/useXYReduce.js';
66
import { PathBuilder } from '../../utility/PathBuilder.js';
77
import { use1DTraceYScale, useScale2DY } from '../utilities/scale.js';
88

9+
import { Ranges1D } from './Ranges1D.tsx';
10+
import { Signals1D } from './Signals1D.tsx';
11+
912
interface Left1DChartProps {
1013
horizontalMargin?: number;
1114
data: Spectrum1D;
@@ -44,17 +47,21 @@ function Left1DChart({
4447
data: spectrum,
4548
}: Left1DChartProps) {
4649
const { height, margin, displayerKey } = useChartData();
50+
const svgRef = useRef<SVGSVGElement>(null);
4751

4852
const width = margin.left;
4953

5054
const path = usePath(spectrum, { horizontalMargin, width });
55+
const ranges = spectrum.ranges.values;
5156

5257
const innerHeight = height - margin.bottom - margin.top;
5358

5459
if (!innerHeight || !width) return null;
5560

5661
return (
5762
<svg
63+
ref={svgRef}
64+
style={{ overflow: 'visible' }}
5865
viewBox={`0 0 ${width} ${innerHeight + margin.top}`}
5966
width={width}
6067
height={innerHeight + margin.top}
@@ -63,6 +70,9 @@ function Left1DChart({
6370
<clipPath id={`${displayerKey}clip-left`}>
6471
<rect width={width} height={innerHeight} x="0" y={margin.top} />
6572
</clipPath>
73+
<clipPath id={`${displayerKey}clip-left-ranges`}>
74+
<rect width={width + 20} height={innerHeight} x="0" y={margin.top} />
75+
</clipPath>
6676
</defs>
6777
<g clipPath={`url(#${displayerKey}clip-left)`}>
6878
<path
@@ -72,6 +82,19 @@ function Left1DChart({
7282
d={path}
7383
/>
7484
</g>
85+
<g clipPath={`url(#${displayerKey}clip-left-ranges)`}>
86+
<Ranges1D
87+
ranges={ranges}
88+
orientation="vertical"
89+
spectrumId={spectrum.id}
90+
/>
91+
<Signals1D
92+
ranges={ranges}
93+
svgRef={svgRef}
94+
orientation="vertical"
95+
spectrumId={spectrum.id}
96+
/>
97+
</g>
7598
</svg>
7699
);
77100
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import styled from '@emotion/styled';
2+
import type { Jcoupling, Range, Signal1D } from '@zakodium/nmr-types';
3+
4+
import { getOpacityBasedOnSignalKind } from '../../../data/utilities/RangeUtilities.ts';
5+
import { useChartData } from '../../context/ChartContext.tsx';
6+
import { useDispatch } from '../../context/DispatchContext.tsx';
7+
import { useScale2DX, useScale2DY } from '../utilities/scale.js';
8+
9+
interface Range1DTraceProps {
10+
position: number;
11+
size: number;
12+
orientation?: 'horizontal' | 'vertical';
13+
opacity?: number;
14+
onClick?: (e: React.MouseEvent<SVGGElement, MouseEvent>) => void;
15+
}
16+
17+
interface ElementLayout {
18+
groupTransform: string;
19+
pathD: string;
20+
}
21+
22+
const Path = styled.path`
23+
fill: none;
24+
stroke-width: 1px;
25+
shape-rendering: crispedges;
26+
stroke: black;
27+
28+
&:hover {
29+
stroke: red;
30+
}
31+
`;
32+
const length = 5;
33+
const innerMargin = 10;
34+
35+
function Range1DTrace(props: Range1DTraceProps) {
36+
const {
37+
position,
38+
size,
39+
orientation = 'horizontal',
40+
opacity = 1,
41+
onClick,
42+
} = props;
43+
const { margin } = useChartData();
44+
45+
const layout: ElementLayout =
46+
orientation === 'vertical'
47+
? {
48+
groupTransform: `translate(${margin.left - innerMargin} ${position})`,
49+
pathD: `M0 0 L${length} 0 L${length} ${size} L0 ${size}`,
50+
}
51+
: {
52+
groupTransform: `translate(${position} ${margin.top - innerMargin})`,
53+
pathD: `M0 0 L0 ${length} L${size} ${length} L${size} 0`,
54+
};
55+
56+
return (
57+
<g transform={layout.groupTransform} style={{ opacity }} onClick={onClick}>
58+
<rect
59+
x={orientation === 'vertical' ? -(margin.left - innerMargin) : 0}
60+
y={orientation === 'vertical' ? 0 : -(margin.top - innerMargin)}
61+
width={orientation === 'vertical' ? margin.left : size}
62+
height={orientation === 'vertical' ? size : margin.top}
63+
fill="transparent"
64+
/>
65+
<Path d={layout.pathD} />
66+
</g>
67+
);
68+
}
69+
70+
interface Ranges1DProps {
71+
ranges: Range[];
72+
orientation: 'horizontal' | 'vertical';
73+
spectrumId: string;
74+
}
75+
76+
export function Ranges1D(props: Ranges1DProps) {
77+
const { ranges, orientation, spectrumId } = props;
78+
const scaleX = useScale2DX();
79+
const scaleY = useScale2DY();
80+
const dispatch = useDispatch();
81+
const scale = orientation === 'horizontal' ? scaleX : scaleY;
82+
83+
return ranges.map((range) => {
84+
const opacity = getOpacityBasedOnSignalKind(range);
85+
86+
const { from, to, id } = range;
87+
const fromInPixel = scale(from);
88+
const toInPixel = scale(to);
89+
const start = Math.min(fromInPixel, toInPixel);
90+
const size = Math.abs(fromInPixel - toInPixel);
91+
92+
function handleAddSignal(e: React.MouseEvent<SVGGElement, MouseEvent>) {
93+
const boundingRect = e.currentTarget.getBoundingClientRect();
94+
const x = e.clientX - boundingRect.left + start;
95+
const y = e.clientY - boundingRect.top + start;
96+
97+
const valueInPixel = orientation === 'horizontal' ? x : y;
98+
const delta = scale.invert(valueInPixel);
99+
100+
const updatedRange = structuredClone(range);
101+
const signal: Signal1D = {
102+
id: crypto.randomUUID(),
103+
delta,
104+
js: [
105+
{
106+
multiplicity: 'm',
107+
} as Jcoupling,
108+
],
109+
kind: 'signal',
110+
multiplicity: 'm',
111+
};
112+
updatedRange.signals.push(signal);
113+
114+
dispatch({
115+
type: 'UPDATE_RANGE',
116+
payload: {
117+
range: updatedRange,
118+
spectrumId,
119+
},
120+
});
121+
}
122+
123+
return (
124+
<Range1DTrace
125+
orientation={orientation}
126+
key={id}
127+
position={start}
128+
size={size}
129+
onClick={handleAddSignal}
130+
opacity={opacity}
131+
/>
132+
);
133+
});
134+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import type { Range, Signal1D } from '@zakodium/nmr-types';
2+
import type { RefObject } from 'react';
3+
import { useMemo, useRef, useState } from 'react';
4+
5+
import { Anchor } from '../../AnchorSVG.tsx';
6+
import { useChartData } from '../../context/ChartContext.tsx';
7+
import { useDispatch } from '../../context/DispatchContext.tsx';
8+
import { useScale2DX, useScale2DY } from '../utilities/scale.js';
9+
10+
interface Signals1DProps {
11+
spectrumId: string;
12+
ranges: Range[];
13+
svgRef: RefObject<SVGSVGElement>;
14+
orientation: 'horizontal' | 'vertical';
15+
}
16+
17+
interface SignalWithRange extends Signal1D {
18+
from: number;
19+
to: number;
20+
rangeID: string;
21+
}
22+
23+
export function Signals1D(props: Signals1DProps) {
24+
const { ranges, svgRef, orientation, spectrumId } = props;
25+
const {
26+
margin: { top, left },
27+
} = useChartData();
28+
const scaleX = useScale2DX();
29+
const scaleY = useScale2DY();
30+
const size = orientation === 'horizontal' ? top : left;
31+
const scale = orientation === 'horizontal' ? scaleX : scaleY;
32+
const draggingAnchorRef = useRef<{ index: number; delta: number } | null>(
33+
null,
34+
);
35+
// Only used during active drag for local updates
36+
const [draggingAnchor, setDraggingAnchor] = useState<{
37+
index: number;
38+
delta: number;
39+
} | null>(null);
40+
const dispatch = useDispatch();
41+
42+
function handleDragMove(
43+
index: number,
44+
newPosition: { x: number; y: number },
45+
range: { from: number; to: number },
46+
) {
47+
// console.log(newX)
48+
49+
const delta = orientation === 'horizontal' ? newPosition.x : newPosition.y;
50+
const from = orientation === 'horizontal' ? range.from : range.to;
51+
const to = orientation === 'horizontal' ? range.to : range.from;
52+
const clampedX = Math.max(scale(to), Math.min(scale(from), delta));
53+
const updated = { index, delta: scale.invert(clampedX) };
54+
draggingAnchorRef.current = updated;
55+
setDraggingAnchor(updated);
56+
}
57+
function handleDragEnd(signal: SignalWithRange) {
58+
const finalDelta = draggingAnchorRef.current?.delta;
59+
60+
if (!finalDelta) return;
61+
62+
draggingAnchorRef.current = null;
63+
setDraggingAnchor(null);
64+
dispatch({
65+
type: 'CHANGE_SIGNAL_DELTA',
66+
payload: {
67+
value: finalDelta,
68+
rangeId: signal.rangeID,
69+
signalId: signal.id,
70+
spectrumId,
71+
},
72+
});
73+
}
74+
75+
function handleDelete(signal: SignalWithRange) {
76+
dispatch({
77+
type: 'DELETE_1D_SIGNAL',
78+
payload: {
79+
rangeId: signal.rangeID,
80+
signalId: signal.id,
81+
spectrumId,
82+
},
83+
});
84+
}
85+
86+
const signals = useMemo(() => {
87+
const result: SignalWithRange[] = [];
88+
let flatIndex = 0;
89+
90+
for (const range of ranges ?? []) {
91+
for (const signal of range.signals ?? []) {
92+
const isDragging = draggingAnchor?.index === flatIndex;
93+
94+
result.push({
95+
...signal,
96+
delta: isDragging ? draggingAnchor.delta : signal.delta,
97+
from: range.from,
98+
to: range.to,
99+
rangeID: range.id,
100+
});
101+
102+
flatIndex++;
103+
}
104+
}
105+
106+
return result;
107+
}, [ranges, draggingAnchor?.index, draggingAnchor?.delta]);
108+
return (
109+
<g>
110+
{signals.map((signal, index) => {
111+
const { rangeID, id, delta, from, to } = signal;
112+
const x = orientation === 'horizontal' ? scale(delta) : size - 5;
113+
const y = orientation === 'horizontal' ? size - 5 : scale(delta);
114+
115+
return (
116+
<Anchor
117+
key={`${id}-${rangeID}`}
118+
position={{ x, y }}
119+
svgRef={svgRef}
120+
shape="circle"
121+
onDragMove={(newPosition) =>
122+
handleDragMove(index, newPosition, { from, to })
123+
}
124+
onDragEnd={() => handleDragEnd(signal)}
125+
onDelete={() => handleDelete(signal)}
126+
cursorOrientation={orientation}
127+
anchorStyle={{
128+
guideStyle: 'dashed',
129+
size: 8,
130+
hoverSize: 15,
131+
dragSize: 15,
132+
hoverStroke: 'red',
133+
dragStroke: 'darkgreen',
134+
guideColor: 'red',
135+
guideDragColor: 'darkgreen',
136+
fill: 'red',
137+
stroke: 'transparent',
138+
}}
139+
svgHeight={100}
140+
/>
141+
);
142+
})}
143+
</g>
144+
);
145+
}

0 commit comments

Comments
 (0)