diff --git a/src/components/DragInput.tsx b/src/components/DragInput.tsx new file mode 100644 index 0000000000..2941a3c263 --- /dev/null +++ b/src/components/DragInput.tsx @@ -0,0 +1,235 @@ +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; diff --git a/src/components/Stats.scss b/src/components/Stats.scss index 5cadcd14ae..40ee491c6e 100644 --- a/src/components/Stats.scss +++ b/src/components/Stats.scss @@ -2,7 +2,7 @@ .excalidraw { .Stats { - width: 202px; + width: 204px; position: absolute; top: 64px; right: 12px; diff --git a/src/components/Stats.tsx b/src/components/Stats.tsx index f29d3b3028..a3450c2e42 100644 --- a/src/components/Stats.tsx +++ b/src/components/Stats.tsx @@ -1,14 +1,7 @@ -import { nanoid } from "nanoid"; import React, { useEffect, useMemo, useState } from "react"; import { getCommonBounds } from "../element/bounds"; -import { mutateElement } from "../element/mutateElement"; -import { - ExcalidrawElement, - NonDeletedExcalidrawElement, -} from "../element/types"; +import { NonDeletedExcalidrawElement } from "../element/types"; import { t } from "../i18n"; -import { KEYS } from "../keys"; -import { degreeToRadian, radianToDegree } from "../math"; import { getTargetElements } from "../scene"; import Scene from "../scene/Scene"; import { AppState, ExcalidrawProps } from "../types"; @@ -16,6 +9,7 @@ import { CloseIcon } from "./icons"; import { Island } from "./Island"; import "./Stats.scss"; import { throttle } from "lodash"; +import DragInput from "./DragInput"; const STATS_TIMEOUT = 50; interface StatsProps { @@ -28,9 +22,6 @@ interface StatsProps { type ElementStatItem = { label: string; - value: number; - element: NonDeletedExcalidrawElement; - version: string; property: "x" | "y" | "width" | "height" | "angle"; }; @@ -70,70 +61,6 @@ export const Stats = (props: StatsProps) => { [throttledSetSceneDimension], ); - const [elementStats, setElementStats] = useState([]); - - const throttledSetElementStats = useMemo( - () => - throttle((element: NonDeletedExcalidrawElement | null) => { - const stats: ElementStatItem[] = element - ? [ - { - label: "X", - value: Math.round(element.x), - element, - property: "x", - version: nanoid(), - }, - { - label: "Y", - value: Math.round(element.y), - element, - property: "y", - version: nanoid(), - }, - { - label: "W", - value: Math.round(element.width), - element, - property: "width", - version: nanoid(), - }, - { - label: "H", - value: Math.round(element.height), - element, - property: "height", - version: nanoid(), - }, - { - label: "A", - value: Math.round(radianToDegree(element.angle) * 100) / 100, - element, - property: "angle", - version: nanoid(), - }, - ] - : []; - - setElementStats(stats); - }, STATS_TIMEOUT), - [], - ); - - useEffect(() => { - throttledSetElementStats(singleElement); - }, [ - singleElement, - singleElement?.version, - singleElement?.versionNonce, - throttledSetElementStats, - ]); - - useEffect( - () => () => throttledSetElementStats.cancel(), - [throttledSetElementStats], - ); - return (
@@ -185,78 +112,38 @@ export const Stats = (props: StatsProps) => { gap: "4px 8px", }} > - {elementStats.map((statsItem) => { - return ( - - ); - })} + {( + [ + { + label: "X", + property: "x", + }, + { + label: "Y", + property: "y", + }, + { + label: "W", + property: "width", + }, + { + label: "H", + property: "height", + }, + { + label: "A", + property: "angle", + }, + ] as ElementStatItem[] + ).map((statsItem) => ( + + ))}
diff --git a/src/css/styles.scss b/src/css/styles.scss index c663e55bec..ce6c02b323 100644 --- a/src/css/styles.scss +++ b/src/css/styles.scss @@ -7,6 +7,12 @@ --zIndex-layerUI: 3; } +body.dragResize, +body.dragResize a:hover, +body.dragResize * { + cursor: ew-resize; +} + .excalidraw { --ui-font: Assistant, system-ui, BlinkMacSystemFont, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;