summaryrefslogtreecommitdiffstats
path: root/packages/excalidraw/components/Stats/Position.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/excalidraw/components/Stats/Position.tsx')
-rw-r--r--packages/excalidraw/components/Stats/Position.tsx214
1 files changed, 214 insertions, 0 deletions
diff --git a/packages/excalidraw/components/Stats/Position.tsx b/packages/excalidraw/components/Stats/Position.tsx
new file mode 100644
index 0000000..038e58e
--- /dev/null
+++ b/packages/excalidraw/components/Stats/Position.tsx
@@ -0,0 +1,214 @@
+import type { ElementsMap, ExcalidrawElement } from "../../element/types";
+import StatsDragInput from "./DragInput";
+import type { DragInputCallbackType } from "./DragInput";
+import { getStepSizedValue, moveElement } from "./utils";
+import type Scene from "../../scene/Scene";
+import type { AppState } from "../../types";
+import { clamp, pointFrom, pointRotateRads, round } from "@excalidraw/math";
+import { isImageElement } from "../../element/typeChecks";
+import {
+ getFlipAdjustedCropPosition,
+ getUncroppedWidthAndHeight,
+} from "../../element/cropElement";
+import { mutateElement } from "../../element/mutateElement";
+
+interface PositionProps {
+ property: "x" | "y";
+ element: ExcalidrawElement;
+ elementsMap: ElementsMap;
+ scene: Scene;
+ appState: AppState;
+}
+
+const STEP_SIZE = 10;
+
+const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
+ accumulatedChange,
+ instantChange,
+ originalElements,
+ originalElementsMap,
+ shouldChangeByStepSize,
+ nextValue,
+ property,
+ scene,
+ originalAppState,
+}) => {
+ const elementsMap = scene.getNonDeletedElementsMap();
+ const elements = scene.getNonDeletedElements();
+ const origElement = originalElements[0];
+ const [cx, cy] = [
+ origElement.x + origElement.width / 2,
+ origElement.y + origElement.height / 2,
+ ];
+ const [topLeftX, topLeftY] = pointRotateRads(
+ pointFrom(origElement.x, origElement.y),
+ pointFrom(cx, cy),
+ origElement.angle,
+ );
+
+ if (originalAppState.croppingElementId === origElement.id) {
+ const element = elementsMap.get(origElement.id);
+
+ if (!element || !isImageElement(element) || !element.crop) {
+ return;
+ }
+
+ const crop = element.crop;
+ let nextCrop = crop;
+ const isFlippedByX = element.scale[0] === -1;
+ const isFlippedByY = element.scale[1] === -1;
+ const { width: uncroppedWidth, height: uncroppedHeight } =
+ getUncroppedWidthAndHeight(element);
+
+ if (nextValue !== undefined) {
+ if (property === "x") {
+ const nextValueInNatural =
+ nextValue * (crop.naturalWidth / uncroppedWidth);
+
+ if (isFlippedByX) {
+ nextCrop = {
+ ...crop,
+ x: clamp(
+ crop.naturalWidth - nextValueInNatural - crop.width,
+ 0,
+ crop.naturalWidth - crop.width,
+ ),
+ };
+ } else {
+ nextCrop = {
+ ...crop,
+ x: clamp(
+ nextValue * (crop.naturalWidth / uncroppedWidth),
+ 0,
+ crop.naturalWidth - crop.width,
+ ),
+ };
+ }
+ }
+
+ if (property === "y") {
+ nextCrop = {
+ ...crop,
+ y: clamp(
+ nextValue * (crop.naturalHeight / uncroppedHeight),
+ 0,
+ crop.naturalHeight - crop.height,
+ ),
+ };
+ }
+
+ mutateElement(element, {
+ crop: nextCrop,
+ });
+
+ return;
+ }
+
+ const changeInX =
+ (property === "x" ? instantChange : 0) * (isFlippedByX ? -1 : 1);
+ const changeInY =
+ (property === "y" ? instantChange : 0) * (isFlippedByY ? -1 : 1);
+
+ nextCrop = {
+ ...crop,
+ x: clamp(crop.x + changeInX, 0, crop.naturalWidth - crop.width),
+ y: clamp(crop.y + changeInY, 0, crop.naturalHeight - crop.height),
+ };
+
+ mutateElement(element, {
+ crop: nextCrop,
+ });
+
+ return;
+ }
+
+ if (nextValue !== undefined) {
+ const newTopLeftX = property === "x" ? nextValue : topLeftX;
+ const newTopLeftY = property === "y" ? nextValue : topLeftY;
+ moveElement(
+ newTopLeftX,
+ newTopLeftY,
+ origElement,
+ elementsMap,
+ elements,
+ scene,
+ originalElementsMap,
+ );
+ return;
+ }
+
+ const changeInTopX = property === "x" ? accumulatedChange : 0;
+ const changeInTopY = property === "y" ? accumulatedChange : 0;
+
+ const newTopLeftX =
+ property === "x"
+ ? Math.round(
+ shouldChangeByStepSize
+ ? getStepSizedValue(origElement.x + changeInTopX, STEP_SIZE)
+ : topLeftX + changeInTopX,
+ )
+ : topLeftX;
+
+ const newTopLeftY =
+ property === "y"
+ ? Math.round(
+ shouldChangeByStepSize
+ ? getStepSizedValue(origElement.y + changeInTopY, STEP_SIZE)
+ : topLeftY + changeInTopY,
+ )
+ : topLeftY;
+
+ moveElement(
+ newTopLeftX,
+ newTopLeftY,
+ origElement,
+ elementsMap,
+ elements,
+ scene,
+ originalElementsMap,
+ );
+};
+
+const Position = ({
+ property,
+ element,
+ elementsMap,
+ scene,
+ appState,
+}: PositionProps) => {
+ const [topLeftX, topLeftY] = pointRotateRads(
+ pointFrom(element.x, element.y),
+ pointFrom(element.x + element.width / 2, element.y + element.height / 2),
+ element.angle,
+ );
+ let value = round(property === "x" ? topLeftX : topLeftY, 2);
+
+ if (
+ appState.croppingElementId === element.id &&
+ isImageElement(element) &&
+ element.crop
+ ) {
+ const flipAdjustedPosition = getFlipAdjustedCropPosition(element);
+
+ if (flipAdjustedPosition) {
+ value = round(
+ property === "x" ? flipAdjustedPosition.x : flipAdjustedPosition.y,
+ 2,
+ );
+ }
+ }
+
+ return (
+ <StatsDragInput
+ label={property === "x" ? "X" : "Y"}
+ elements={[element]}
+ dragInputCallback={handlePositionChange}
+ scene={scene}
+ value={value}
+ property={property}
+ appState={appState}
+ />
+ );
+};
+
+export default Position;