Skip to content

Commit fdee542

Browse files
committed
fix: drag and resize handling for publication string
* manage `bounding` in pixels and props `bounding` in percent `useBoundingBox` apply `convertToPixel` to init / derive the state * simplify drag / resize handler * set data as-is * no more delta computation (`auto` to any size broke the first height resize) by using `eRef.clientHeight` and `eRef.clientWidth` Refs: #3966 (comment)
1 parent a875274 commit fdee542

2 files changed

Lines changed: 126 additions & 95 deletions

File tree

src/component/1d/FloatPublicationString.tsx

Lines changed: 113 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import styled from '@emotion/styled';
22
import type { BoundingBox, Spectrum, TextStyle } from '@zakodium/nmrium-core';
3-
import { useEffect, useMemo, useState } from 'react';
3+
import { useCallback, useEffect, useMemo, useState } from 'react';
44
import { BsArrowsMove } from 'react-icons/bs';
55
import { FaEdit, FaTimes } from 'react-icons/fa';
66
import { Rnd } from 'react-rnd';
@@ -43,6 +43,8 @@ function useWrapSVGText(params: UseWrapSVGTextParams) {
4343

4444
const debugCanvas = false;
4545
const labelSize = style.fontSize ?? 12;
46+
// ctx used only for debug canvas purpose.
47+
// eslint-disable-next-line @typescript-eslint/no-deprecated
4648
const { getTextWidth, ctx } = useTextMetrics({
4749
labelSize,
4850
labelStyle: style.fontStyle,
@@ -175,55 +177,115 @@ function PublicationText(props: PublicationTextProps) {
175177

176178
interface DraggablePublicationStringProps {
177179
value: string;
178-
bonding: BoundingBox;
180+
bounding: BoundingBox;
179181
nucleus: string;
180182
spectrum: Spectrum;
181183
}
182184

185+
function useBoundingBox(externalBoundingPercent: BoundingBox) {
186+
const { percentToPixel, pixelToPercent } = useSVGUnitConverter();
187+
188+
const convertToPixel = useCallback(
189+
(bounding: Partial<BoundingBox>) => {
190+
const { x, y, height, width } = bounding;
191+
const output: Partial<BoundingBox> = {};
192+
193+
if (typeof x === 'number') {
194+
output.x = percentToPixel(x, 'x');
195+
}
196+
if (typeof y === 'number') {
197+
output.y = percentToPixel(y, 'y');
198+
}
199+
if (typeof width === 'number') {
200+
output.width = width;
201+
}
202+
if (typeof height === 'number') {
203+
output.height = height;
204+
}
205+
206+
return output;
207+
},
208+
[percentToPixel],
209+
);
210+
211+
const convertToPercent = useCallback(
212+
(bounding: Partial<BoundingBox>) => {
213+
const { x, y, height, width } = bounding;
214+
const output: Partial<BoundingBox> = {};
215+
216+
if (typeof x === 'number') {
217+
output.x = pixelToPercent(x, 'x');
218+
}
219+
if (typeof y === 'number') {
220+
output.y = pixelToPercent(y, 'y');
221+
}
222+
if (typeof width === 'number') {
223+
output.width = width;
224+
}
225+
if (typeof height === 'number') {
226+
output.height = height;
227+
}
228+
229+
return output;
230+
},
231+
[pixelToPercent],
232+
);
233+
234+
const [bounding, setBounding] = useState<BoundingBox>(() => {
235+
return convertToPixel(externalBoundingPercent) as BoundingBox;
236+
});
237+
238+
useEffect(() => {
239+
setBounding(convertToPixel(externalBoundingPercent) as BoundingBox);
240+
}, [convertToPixel, externalBoundingPercent]);
241+
242+
return {
243+
bounding,
244+
setBounding,
245+
convertToPixel,
246+
convertToPercent,
247+
};
248+
}
249+
183250
function DraggablePublicationString(props: DraggablePublicationStringProps) {
184-
const { value, bonding: externalBounding, nucleus, spectrum } = props;
251+
const {
252+
value,
253+
bounding: externalBoundingInPercent,
254+
nucleus,
255+
spectrum,
256+
} = props;
185257
const spectrumKey = spectrum.id;
186258

187259
const dispatch = useDispatch();
188260
const { viewerRef } = useGlobal();
189-
const [bounding, setBounding] = useState<BoundingBox>(externalBounding);
190261
const [isMoveActive, setIsMoveActive] = useState(false);
191-
const { percentToPixel, pixelToPercent } = useSVGUnitConverter();
192262
const isExportProcessStart = useCheckExportStatus();
193263
const acsOptions = useACSSettings(nucleus);
194264
const [isDialogOpen, setIsDialogOpen] = useState(false);
195265

196-
useEffect(() => {
197-
setBounding({ ...externalBounding });
198-
}, [externalBounding]);
199-
200-
function handleResize(
201-
internalBounding: Pick<BoundingBox, 'height' | 'width'>,
202-
) {
203-
const { width = 0, height = 0 } = convertToPixel(externalBounding);
204-
internalBounding.width += width;
205-
internalBounding.height += height;
206-
setBounding((prevBounding) => ({
207-
...prevBounding,
208-
...convertToPercent(internalBounding),
209-
}));
266+
const { bounding, setBounding, convertToPercent } = useBoundingBox(
267+
externalBoundingInPercent,
268+
);
269+
270+
function handleResize(bounding: Pick<BoundingBox, 'height' | 'width'>) {
271+
setBounding((prevBounding) => {
272+
return {
273+
...prevBounding,
274+
width: bounding.width,
275+
height: bounding.height,
276+
};
277+
});
210278
}
211279

212-
function handleDrag(internalBounding: Pick<BoundingBox, 'x' | 'y'>) {
280+
function handleDrag(newPosition: Pick<BoundingBox, 'x' | 'y'>) {
213281
setBounding((prevBounding) => ({
214282
...prevBounding,
215-
...convertToPercent(internalBounding),
283+
...newPosition,
216284
}));
217285
}
286+
218287
function handleChangeInsetBounding(bounding: Partial<BoundingBox>) {
219-
if (
220-
typeof bounding?.width === 'number' &&
221-
typeof bounding?.height === 'number'
222-
) {
223-
const { width, height } = externalBounding;
224-
bounding.width += width;
225-
bounding.height += height;
226-
}
288+
setBounding((prev) => ({ ...prev, ...bounding }));
227289

228290
dispatch({
229291
type: 'CHANGE_RANGES_VIEW_FLOATING_BOX_BOUNDING',
@@ -235,45 +297,6 @@ function DraggablePublicationString(props: DraggablePublicationStringProps) {
235297
});
236298
}
237299

238-
function convertToPixel(bounding: Partial<BoundingBox>) {
239-
const { x, y, height, width } = bounding;
240-
const output: Partial<BoundingBox> = {};
241-
242-
if (x) {
243-
output.x = percentToPixel(x, 'x');
244-
}
245-
if (y) {
246-
output.y = percentToPixel(y, 'y');
247-
}
248-
if (width) {
249-
output.width = width;
250-
}
251-
if (height) {
252-
output.height = height;
253-
}
254-
255-
return output;
256-
}
257-
function convertToPercent(bounding: Partial<BoundingBox>) {
258-
const { x, y, height, width } = bounding;
259-
const output: Partial<BoundingBox> = {};
260-
261-
if (x) {
262-
output.x = pixelToPercent(x, 'x');
263-
}
264-
if (y) {
265-
output.y = pixelToPercent(y, 'y');
266-
}
267-
if (width) {
268-
output.width = width;
269-
}
270-
if (height) {
271-
output.height = height;
272-
}
273-
274-
return output;
275-
}
276-
277300
function handleRemove() {
278301
dispatch({
279302
type: 'TOGGLE_RANGES_VIEW_PROPERTY',
@@ -306,10 +329,7 @@ function DraggablePublicationString(props: DraggablePublicationStringProps) {
306329
];
307330
if (!viewerRef || !value) return null;
308331

309-
const { width, height, x: xInPercent, y: yInPercent } = bounding;
310-
311-
const x = percentToPixel(xInPercent, 'x');
312-
const y = percentToPixel(yInPercent, 'y');
332+
const { width, height, x = 0, y = 0 } = bounding;
313333

314334
if (isExportProcessStart) {
315335
return (
@@ -333,18 +353,26 @@ function DraggablePublicationString(props: DraggablePublicationStringProps) {
333353
dragHandleClassName="handle"
334354
enableUserSelectHack={false}
335355
bounds={`#${viewerRef.id}`}
336-
onResize={(e, dir, eRef, size, position) =>
337-
handleResize({ ...size, ...position })
338-
}
339-
onResizeStop={(e, dir, eRef, size, position) =>
340-
handleChangeInsetBounding({ ...size, ...position })
341-
}
356+
onResize={(e, dir, eRef, size, position) => {
357+
handleResize({
358+
...position,
359+
height: eRef.clientHeight,
360+
width: eRef.clientWidth,
361+
});
362+
}}
363+
onResizeStop={(e, dir, eRef, size, position) => {
364+
handleChangeInsetBounding({
365+
...position,
366+
height: eRef.clientHeight,
367+
width: eRef.clientWidth,
368+
});
369+
}}
342370
onDragStart={() => setIsMoveActive(true)}
343-
onDrag={(e, { x, y }) => {
344-
handleDrag({ x, y });
371+
onDrag={(e, data) => {
372+
handleDrag({ x: data.x, y: data.y });
345373
}}
346-
onDragStop={(e, { x, y }) => {
347-
handleChangeInsetBounding({ x, y });
374+
onDragStop={(e, data) => {
375+
handleChangeInsetBounding({ x: data.x, y: data.y });
348376
setIsMoveActive(false);
349377
}}
350378
resizeHandleWrapperStyle={{ backgroundColor: 'white' }}
@@ -353,7 +381,6 @@ function DraggablePublicationString(props: DraggablePublicationStringProps) {
353381
buttons={actionButtons}
354382
fill
355383
positioningStrategy="fixed"
356-
357384
direction="row"
358385
targetProps={{ style: { width: '100%', height: '100%' } }}
359386
space={2}
@@ -365,8 +392,8 @@ function DraggablePublicationString(props: DraggablePublicationStringProps) {
365392
width={width || 'auto'}
366393
height={height || 'auto'}
367394
xmlns="http://www.w3.org/2000/svg"
368-
369-
><PublicationText
395+
>
396+
<PublicationText
370397
text={value}
371398
width={width}
372399
textStyle={acsOptions.textStyle}
@@ -424,7 +451,7 @@ export function FloatPublicationString() {
424451
key={id}
425452
spectrum={spectrum}
426453
nucleus={nucleus}
427-
bonding={publicationStringBounding}
454+
bounding={publicationStringBounding}
428455
value={publicationString[id]}
429456
/>
430457
);

src/component/hooks/useSVGUnitConverter.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { useMemo } from 'react';
2+
13
import { useChartData } from '../context/ChartContext.js';
24

35
function truncate(value: any, numberOfDigits = 0) {
@@ -15,14 +17,16 @@ export function convertPercentToPixel(value: number, baseValue: number) {
1517
export function useSVGUnitConverter() {
1618
const { width, height } = useChartData();
1719

18-
function pixelToPercent(value: number, axis: 'x' | 'y') {
19-
const size = axis === 'x' ? width : height;
20-
return convertPixelToPercent(value, size);
21-
}
22-
function percentToPixel(value: number, axis: 'x' | 'y') {
23-
const size = axis === 'x' ? width : height;
24-
return convertPercentToPixel(value, size);
25-
}
20+
return useMemo(() => {
21+
function pixelToPercent(value: number, axis: 'x' | 'y') {
22+
const size = axis === 'x' ? width : height;
23+
return convertPixelToPercent(value, size);
24+
}
25+
function percentToPixel(value: number, axis: 'x' | 'y') {
26+
const size = axis === 'x' ? width : height;
27+
return convertPercentToPixel(value, size);
28+
}
2629

27-
return { pixelToPercent, percentToPixel };
30+
return { pixelToPercent, percentToPixel };
31+
}, [width, height]);
2832
}

0 commit comments

Comments
 (0)