import throttle from "lodash.throttle"; import { useEffect, useMemo, useRef } from "react"; import { EVENT } from "../constants"; import { getTransformHandles } from "../element"; import { mutateElement } from "../element/mutateElement"; import { resizeSingleElement } from "../element/resizeElements"; import { ExcalidrawElement } from "../element/types"; import { KEYS } from "../keys"; import { degreeToRadian, radianToDegree, rotatePoint } from "../math"; import Scene from "../scene/Scene"; import { AppState, Point } from "../types"; import { arrayToMap } from "../utils"; const shouldKeepAspectRatio = (element: ExcalidrawElement) => { return element.type === "image"; }; type AdjustableProperty = "width" | "height" | "angle" | "x" | "y"; interface DragInputProps { label: string | React.ReactNode; property: AdjustableProperty; element: ExcalidrawElement; zoom: AppState["zoom"]; } const DragInput = ({ label, property, element, zoom }: DragInputProps) => { const inputRef = useRef(null); const labelRef = useRef(null); const originalElementsMap = useMemo( () => arrayToMap(Scene.getScene(element)?.getNonDeletedElements() ?? []), [element], ); const handleChange = useMemo( () => ( initialValue: number, delta: number, source: "pointerMove" | "keyDown", pointerOffset?: number, ) => { if (inputRef.current) { const keepAspectRatio = shouldKeepAspectRatio(element); if ( (property === "width" || property === "height") && source === "pointerMove" && pointerOffset ) { const handles = getTransformHandles(element, zoom, "mouse"); let referencePoint: Point | undefined; let handleDirection: "e" | "s" | "se" | undefined; if (keepAspectRatio && handles.se) { referencePoint = [handles.se[0], handles.se[1]]; handleDirection = "se"; } else if (property === "width" && handles.e) { referencePoint = [handles.e[0], handles.e[1]]; handleDirection = "e"; } else if (property === "height" && handles.s) { referencePoint = [handles.s[0], handles.s[1]]; handleDirection = "s"; } if (referencePoint !== undefined && handleDirection !== undefined) { const pointerRotated = rotatePoint( [ referencePoint[0] + (property === "width" ? pointerOffset : 0), referencePoint[1] + (property === "height" ? pointerOffset : 0), ], referencePoint, element.angle, ); resizeSingleElement( originalElementsMap, keepAspectRatio, element, handleDirection, false, pointerRotated[0], pointerRotated[1], ); } } else if ( source === "keyDown" || (source === "pointerMove" && property !== "width" && property !== "height") ) { const incVal = Math.round( Math.sign(delta) * Math.pow(Math.abs(delta) / 10, 1.6), ); let newVal = initialValue + incVal; newVal = property === "angle" ? // so the degree converted from radian is an integer degreeToRadian( Math.round( radianToDegree( degreeToRadian( Math.sign(newVal % 360) === -1 ? (newVal % 360) + 360 : newVal % 360, ), ), ), ) : Math.round(newVal); mutateElement(element, { [property]: newVal, }); } } }, [element, property, zoom, originalElementsMap], ); const hangleChangeThrottled = useMemo(() => { return throttle(handleChange, 16); }, [handleChange]); useEffect(() => { const value = Math.round( property === "angle" ? radianToDegree(element[property]) * 100 : element[property] * 100, ) / 100; if (inputRef.current) { inputRef.current.value = String(value); } }, [element, element.version, element.versionNonce, property]); useEffect(() => { hangleChangeThrottled.cancel(); }); return ( ); }; export default DragInput;