Skip to content
2 changes: 2 additions & 0 deletions src/component/1d/plugin_chart_brush_tracker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@
if (!operatorUI) return null;

const isLiveEditable = operatorUI?.isLiveEditable ?? false;
const isLivePreview = isLiveEditable && (liveEdit.value?.checked ?? false);

return (
<CoreOperatorChartBrushTracker
core={core}
operatorUI={operatorUI}
spectrum={spectrum}
operation={liveOperation}
isLivePreview={isLivePreview}

Check failure on line 44 in src/component/1d/plugin_chart_brush_tracker.tsx

View workflow job for this annotation

GitHub Actions / nodejs / test-package

Type '{ core: NMRiumCore; operatorUI: Readonly<ProcessingOperatorUI<ProcessingOperatorId>>; spectrum: Spectrum; ... 9 more ...; onChange: (liveOperation: SpectrumProcessingOperation<...> | ... 2 more ... | SpectrumProcessingOperation<...>) => void; }' is not assignable to type 'IntrinsicAttributes & CoreOperatorChartBrushTrackerProps'.

Check failure on line 44 in src/component/1d/plugin_chart_brush_tracker.tsx

View workflow job for this annotation

GitHub Actions / nodejs / lint-check-types

Type '{ core: NMRiumCore; operatorUI: Readonly<ProcessingOperatorUI<ProcessingOperatorId>>; spectrum: Spectrum; ... 9 more ...; onChange: (liveOperation: SpectrumProcessingOperation<...> | ... 2 more ... | SpectrumProcessingOperation<...>) => void; }' is not assignable to type 'IntrinsicAttributes & CoreOperatorChartBrushTrackerProps'.
height={height}
width={width}
margin={margin}
Expand Down
21 changes: 20 additions & 1 deletion src/component/1d/plugin_svg_chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
import { useCore } from '../context/CoreContext.tsx';
import type { ScaleLinearNumberFunction } from '../context/ScaleContext.tsx';
import { useScaleChecked } from '../context/ScaleContext.tsx';
import { useProcessingsMutations } from '../context/processings_mutations_context.tsx';
import { useIndicatorLineColor } from '../hooks/useIndicatorLineColor.ts';
import useSpectrum from '../hooks/useSpectrum.ts';
import { useVerticalAlign } from '../hooks/useVerticalAlign.ts';
import { useLiveEdit } from '../panels/filtersPanel/processings/use_live_edit.ts';
import { useLiveOperation } from '../panels/filtersPanel/processings/use_live_operation.ts';
import { CoreOperatorChartSVG } from '../utility/core_slots/core_operator_chart_svg.tsx';

Expand All @@ -17,7 +19,9 @@
const core = useCore();
const spectrum = useSpectrum();
const { height, width, margin } = useChartData();
const [liveOperation] = useLiveOperation();
const [liveOperation, setLiveOperation] = useLiveOperation();
const liveEdit = useLiveEdit(liveOperation?.uid);
const processingsMutations = useProcessingsMutations();
const { scaleX: scaleXBuilder } = useScaleChecked();
const indicatorLineColor = useIndicatorLineColor();

Expand All @@ -31,12 +35,27 @@
const operatorUI = core.slotOperator(liveOperation.operatorId);
if (!operatorUI) return null;

const isLiveEditable = operatorUI?.isLiveEditable ?? false;
const isLivePreview = isLiveEditable && (liveEdit.value?.checked ?? false);

return (
<CoreOperatorChartSVG
core={core}
operatorUI={operatorUI}
spectrum={spectrum}
operation={liveOperation}
isLivePreview={isLivePreview}

Check failure on line 47 in src/component/1d/plugin_svg_chart.tsx

View workflow job for this annotation

GitHub Actions / nodejs / test-package

Type '{ core: NMRiumCore; operatorUI: Readonly<ProcessingOperatorUI<ProcessingOperatorId>>; spectrum: Spectrum; ... 8 more ...; indicatorLineColor: string; }' is not assignable to type 'IntrinsicAttributes & CoreOperatorChartSVGProps'.

Check failure on line 47 in src/component/1d/plugin_svg_chart.tsx

View workflow job for this annotation

GitHub Actions / nodejs / lint-check-types

Type '{ core: NMRiumCore; operatorUI: Readonly<ProcessingOperatorUI<ProcessingOperatorId>>; spectrum: Spectrum; ... 8 more ...; indicatorLineColor: string; }' is not assignable to type 'IntrinsicAttributes & CoreOperatorChartSVGProps'.
onChange={(liveOperation) => {

Check failure on line 48 in src/component/1d/plugin_svg_chart.tsx

View workflow job for this annotation

GitHub Actions / nodejs / test-package

Parameter 'liveOperation' implicitly has an 'any' type.

Check failure on line 48 in src/component/1d/plugin_svg_chart.tsx

View workflow job for this annotation

GitHub Actions / nodejs / lint-check-types

Parameter 'liveOperation' implicitly has an 'any' type.
liveOperation = setLiveOperation(liveOperation);

if (!isLiveEditable) return;
if (!liveEdit.value?.checked) return;

void processingsMutations.applyLiveChange(
liveOperation,
liveEdit.value?.shouldProcessNext ?? false,
);
}}
height={height}
width={width}
margin={margin}
Expand Down
160 changes: 135 additions & 25 deletions src/component/EventsTrackers/BrushTracker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import type {
MouseButton,
Position,
} from './brush_tracker.types.ts';
import type { DragGestureController } from './brush_tracker_drag_gesture.ts';
import { createDragGesture } from './brush_tracker_drag_gesture.ts';
import type { BrushTrackerEventEmitter } from './brush_tracker_events_listeners.ts';
import {
BrushTrackerEventEmitterContext,
Expand Down Expand Up @@ -83,6 +85,15 @@ export function useBrushTracker() {
function isSelfControlledTarget(target: EventTarget) {
return (target as Element).closest('[data-self-control="true"]') !== null;
}

/**
* Minimal pointer travel in px between pointerdown and pointerup for the
* gesture to be considered a drag. Pointer jitter below this threshold
* (hand tremor, trackpad imprecision) still counts as a click.
* Keep it in sync with the consumer drag threshold
* (`ProcessingOperatorUI.ChartBrushTracker` dragThreshold).
*/
const dragDetectionThreshold = 4;
function getMouseXY(event: MouseEvent, currentTarget?: Element) {
const boundingRect = (
currentTarget ?? event.currentTarget
Expand Down Expand Up @@ -112,8 +123,16 @@ export function BrushTracker(options: BrushTrackerProps) {
const lastRef = useRef<Position>({ x: 0, y: 0 });
const isSelfControlledRef = useRef(false);

const { onClick, onDoubleClick, onBrush, onBrushEnd, onZoom, emitter } =
useWrappedEvents(options);
const {
onClick,
onDoubleClick,
onBrush,
onBrushEnd,
onZoom,
onDrag,
onDragEnd,
emitter,
} = useWrappedEvents(options);

const handleClickWithDebounce = useCallback(
(event: MouseEvent, currentTarget: Element) => {
Expand Down Expand Up @@ -179,6 +198,8 @@ export function BrushTracker(options: BrushTrackerProps) {

isDraggingRef.current = false; // Reset dragging flag

let dragGesture: DragGestureController | null = null;

//check that the right or left mouse button pressed
if ([0, 2].includes(event.button)) {
if (noPropagation) {
Expand All @@ -193,6 +214,14 @@ export function BrushTracker(options: BrushTrackerProps) {
};
lastRef.current = { x: 0, y: 0 };

dragGesture = createDragGesture({
mouseButton: MouseButtons[event.button],
startX: startPositionRef.current.x,
startY: startPositionRef.current.y,
rectX: boundingRect.x,
rectY: boundingRect.y,
});

if (!event.ctrlKey) {
dispatch({
type: 'DOWN',
Expand All @@ -211,16 +240,31 @@ export function BrushTracker(options: BrushTrackerProps) {
}

function moveCallback(event: globalThis.PointerEvent) {
isDraggingRef.current = true; // set flag to true to skip click event if the user dragged the mouse
const {
clientX,
clientY,
screenX,
screenY,
shiftKey,
altKey,
ctrlKey,
} = event;
const { clientX, clientY } = event;

// Only a real pointer travel marks the gesture as a drag: pointer
// jitter must not suppress the click event dispatched on pointerup.
// Both positions must be compared in the same coordinate system:
// `clientX` is viewport-relative while `startPositionRef` is
// relative to the chart container, so the container offset must
// be removed before comparing, otherwise the layout offset itself
// counts as pointer travel. The same threshold also gates the
// `MOVE` dispatch, so pointer jitter neither shows the brush
// banner nor updates the brush range while clicking (e.g. adding
// anchors).
const boundingRect = boundingRectRef.current;
const isPointerTravel =
boundingRect &&
Math.hypot(
clientX - boundingRect.x - startPositionRef.current.x,
clientY - boundingRect.y - startPositionRef.current.y,
) > dragDetectionThreshold;

if (isPointerTravel) {
isDraggingRef.current = true;
}

const { screenX, screenY, shiftKey, altKey, ctrlKey } = event;

if (ctrlKey) {
if (boundingRectRef.current) {
Expand Down Expand Up @@ -248,19 +292,31 @@ export function BrushTracker(options: BrushTrackerProps) {
lastRef.current = { x, y };
}
} else {
dispatch({
type: 'MOVE',
payload: {
screenX,
screenY,
clientX,
clientY,
},
});
const dragEvent = dragGesture?.move(event);
if (dragEvent) {
onDrag(dragEvent);
}

if (!dragEvent?.isPropagationStopped() && isPointerTravel) {
dispatch({
type: 'MOVE',
payload: {
screenX,
screenY,
clientX,
clientY,
},
});
}
}
}

function upCallback() {
function upCallback(pointerUpEvent: globalThis.PointerEvent) {
const dragEndEvent = dragGesture?.end(pointerUpEvent);
if (dragEndEvent) {
onDragEnd(dragEndEvent);
}

if (isDraggingRef.current) {
dispatch({
type: 'UP',
Expand All @@ -283,7 +339,14 @@ export function BrushTracker(options: BrushTrackerProps) {

return false;
},
[clickTriggerMode, handleClickWithDebounce, noPropagation, onZoom],
[
clickTriggerMode,
handleClickWithDebounce,
noPropagation,
onDrag,
onDragEnd,
onZoom,
],
);

const handleMouseWheel = useCallback(
Expand Down Expand Up @@ -590,7 +653,13 @@ export function detectBrushing(

type UseWrappedEventsProps = Pick<
BrushTrackerProps,
'onBrush' | 'onBrushEnd' | 'onClick' | 'onDoubleClick' | 'onZoom'
| 'onBrush'
| 'onBrushEnd'
| 'onClick'
| 'onDoubleClick'
| 'onZoom'
| 'onDrag'
| 'onDragEnd'
>;

interface UseWrappedEvents extends Required<UseWrappedEventsProps> {
Expand All @@ -610,6 +679,8 @@ function useWrappedEvents(props: UseWrappedEventsProps): UseWrappedEvents {
onZoom: onZoomProp = noop,
onDoubleClick: onDoubleClickProp = noop,
onClick: onClickProp = noop,
onDrag: onDragProp = noop,
onDragEnd: onDragEndProp = noop,
} = props;
const emitter = useInitBrushTrackerEventEmitter();

Expand Down Expand Up @@ -680,5 +751,44 @@ function useWrappedEvents(props: UseWrappedEventsProps): UseWrappedEvents {
},
);

return { onClick, onDoubleClick, onBrush, onBrushEnd, onZoom, emitter };
const onDrag = useEventCallback<Parameters<typeof onDragProp>, void>(
(event) => {
try {
emitter.emit('drag', event);
} catch (error) {
reportError(error);
}

if (event.isPropagationStopped()) return;
if (event.isDefaultPrevented()) return;

onDragProp(event);
},
);

const onDragEnd = useEventCallback<Parameters<typeof onDragEndProp>, void>(
(event) => {
try {
emitter.emit('drag-end', event);
} catch (error) {
reportError(error);
}

if (event.isPropagationStopped()) return;
if (event.isDefaultPrevented()) return;

onDragEndProp(event);
},
);

return {
onClick,
onDoubleClick,
onBrush,
onBrushEnd,
onZoom,
onDrag,
onDragEnd,
emitter,
};
}
Loading
Loading