import * as React from 'react'; import { useId, useRef } from 'react'; import { useInteractions } from '../hooks/useInteractions'; import { Brush, Direction, Extent } from '../interfaces'; import { clamp } from '../util'; const BORDER_WIDTH = 6; const EDGE_COLOR = 'transparent'; const MIN_BRUSH_SIZE = 8; const BORDER_CORRECTION = 1; interface BrushProps { value: Brush; direction?: Direction; onChange?: (brush: Brush) => void; onChangeEnd?: (brush: Brush) => void; parent: React.RefObject; extent?: Extent; clearOnMouse?: boolean; } /** * Brush with draggable borders */ export function SVGBrush({ parent, value, direction = 'xy', onChange, onChangeEnd, extent, clearOnMouse = true }: BrushProps) { const id = useId(); const callbacksRef = useRef({ onChange, onChangeEnd }); callbacksRef.current = { onChange, onChangeEnd }; const { setRef } = useInteractions({ onClick: () => { if (clearOnMouse) { callbacksRef.current.onChange?.(null); callbacksRef.current.onChangeEnd?.(null); } }, onMouseUp: () => { callbacksRef.current.onChangeEnd?.(value); }, onDrag: (event) => { const bounds = parent.current.getBoundingClientRect(); const internalExtent = extent || { x1: 0, y1: 0, x2: bounds.width, y2: bounds.height, }; const relativePosition = { x: event.clientX - bounds.left, y: event.clientY - bounds.top, }; const newBrush = { ...value }; switch (event.target.id) { case id: { const w = value.x2 - value.x1; const h = value.y2 - value.y1; newBrush.x1 = clamp(relativePosition.x - event.anchor.x, internalExtent.x1, internalExtent.x2 - w); newBrush.y1 = clamp(relativePosition.y - event.anchor.y, internalExtent.y1, internalExtent.y2 - h); newBrush.x2 = newBrush.x1 + value.x2 - value.x1; newBrush.y2 = newBrush.y1 + value.y2 - value.y1; break; } case `${id}-west`: newBrush.x1 = clamp(relativePosition.x, internalExtent.x1, value.x2 - MIN_BRUSH_SIZE); break; case `${id}-ost`: newBrush.x2 = clamp(relativePosition.x, value.x1 + MIN_BRUSH_SIZE, internalExtent.x2); break; case `${id}-north`: newBrush.y1 = clamp(relativePosition.y, internalExtent.y1, value.y2 - MIN_BRUSH_SIZE); break; case `${id}-south`: newBrush.y2 = clamp(relativePosition.y, value.y1 + MIN_BRUSH_SIZE, internalExtent.y2); break; case `${id}-northwest`: newBrush.x1 = clamp(relativePosition.x, internalExtent.x1, value.x2 - MIN_BRUSH_SIZE); newBrush.y1 = clamp(relativePosition.y, internalExtent.y1, value.y2 - MIN_BRUSH_SIZE); break; case `${id}-northeast`: newBrush.x2 = clamp(relativePosition.x, value.x1 + MIN_BRUSH_SIZE, internalExtent.x2); newBrush.y1 = clamp(relativePosition.y, internalExtent.y1, value.y2 - MIN_BRUSH_SIZE); break; case `${id}-southwest`: newBrush.x1 = clamp(relativePosition.x, internalExtent.x1, value.x2 - MIN_BRUSH_SIZE); newBrush.y2 = clamp(relativePosition.y, value.y1 + MIN_BRUSH_SIZE, internalExtent.y2); break; case `${id}-southeast`: newBrush.x2 = clamp(relativePosition.x, value.x1 + MIN_BRUSH_SIZE, internalExtent.x2); newBrush.y2 = clamp(relativePosition.y, value.y1 + MIN_BRUSH_SIZE, internalExtent.y2); break; default: break; } if (newBrush.x1 !== value.x1 || newBrush.x2 !== value.x2 || newBrush.y1 !== value.y1 || newBrush.y2 !== value.y2) { callbacksRef.current.onChange?.(newBrush); } }, }); return ( {direction !== 'x' ? ( <> ) : null} {direction !== 'y' ? ( <> ) : null} {direction === 'xy' ? ( <> ) : null} ); }