aboutsummaryrefslogtreecommitdiffstats
path: root/packages/excalidraw/components/Stats/utils.ts
diff options
context:
space:
mode:
authorkj_sh6042026-03-15 16:19:35 -0400
committerkj_sh6042026-03-15 16:19:35 -0400
commit6ec259a0e71174651bae95d4628138bf6fd68742 (patch)
tree5e33c6a5ec091ecabfcb257fdc7b6a88ed8754ac /packages/excalidraw/components/Stats/utils.ts
parent16c8578b15c727f22921f8a80a56ee4d4e7f2272 (diff)
refactor: packages/
Diffstat (limited to 'packages/excalidraw/components/Stats/utils.ts')
-rw-r--r--packages/excalidraw/components/Stats/utils.ts219
1 files changed, 219 insertions, 0 deletions
diff --git a/packages/excalidraw/components/Stats/utils.ts b/packages/excalidraw/components/Stats/utils.ts
new file mode 100644
index 0000000..61555ba
--- /dev/null
+++ b/packages/excalidraw/components/Stats/utils.ts
@@ -0,0 +1,219 @@
+import type { Radians } from "@excalidraw/math";
+import { pointFrom, pointRotateRads } from "@excalidraw/math";
+import {
+ bindOrUnbindLinearElements,
+ updateBoundElements,
+} from "../../element/binding";
+import { mutateElement } from "../../element/mutateElement";
+import { getBoundTextElement } from "../../element/textElement";
+import {
+ isFrameLikeElement,
+ isLinearElement,
+ isTextElement,
+} from "../../element/typeChecks";
+import type {
+ ElementsMap,
+ ExcalidrawElement,
+ NonDeletedExcalidrawElement,
+ NonDeletedSceneElementsMap,
+} from "../../element/types";
+import {
+ getSelectedGroupIds,
+ getElementsInGroup,
+ isInGroup,
+} from "../../groups";
+import type Scene from "../../scene/Scene";
+import type { AppState } from "../../types";
+
+export type StatsInputProperty =
+ | "x"
+ | "y"
+ | "width"
+ | "height"
+ | "angle"
+ | "fontSize"
+ | "gridStep";
+
+export const SMALLEST_DELTA = 0.01;
+
+export const isPropertyEditable = (
+ element: ExcalidrawElement,
+ property: keyof ExcalidrawElement,
+) => {
+ if (property === "height" && isTextElement(element)) {
+ return false;
+ }
+ if (property === "width" && isTextElement(element)) {
+ return false;
+ }
+ if (property === "angle" && isFrameLikeElement(element)) {
+ return false;
+ }
+ return true;
+};
+
+export const getStepSizedValue = (value: number, stepSize: number) => {
+ const v = value + stepSize / 2;
+ return v - (v % stepSize);
+};
+
+export type AtomicUnit = Record<string, true>;
+export const getElementsInAtomicUnit = (
+ atomicUnit: AtomicUnit,
+ elementsMap: ElementsMap,
+ originalElementsMap?: ElementsMap,
+) => {
+ return Object.keys(atomicUnit)
+ .map((id) => ({
+ original: (originalElementsMap ?? elementsMap).get(id),
+ latest: elementsMap.get(id),
+ }))
+ .filter((el) => el.original !== undefined && el.latest !== undefined) as {
+ original: NonDeletedExcalidrawElement;
+ latest: NonDeletedExcalidrawElement;
+ }[];
+};
+
+export const newOrigin = (
+ x1: number,
+ y1: number,
+ w1: number,
+ h1: number,
+ w2: number,
+ h2: number,
+ angle: number,
+) => {
+ /**
+ * The formula below is the result of solving
+ * rotate(x1, y1, cx1, cy1, angle) = rotate(x2, y2, cx2, cy2, angle)
+ * where rotate is the function defined in math.ts
+ *
+ * This is so that the new origin (x2, y2),
+ * when rotated against the new center (cx2, cy2),
+ * coincides with (x1, y1) rotated against (cx1, cy1)
+ *
+ * The reason for doing this computation is so the element's top left corner
+ * on the canvas remains fixed after any changes in its dimension.
+ */
+
+ return {
+ x:
+ x1 +
+ (w1 - w2) / 2 +
+ ((w2 - w1) / 2) * Math.cos(angle) +
+ ((h1 - h2) / 2) * Math.sin(angle),
+ y:
+ y1 +
+ (h1 - h2) / 2 +
+ ((w2 - w1) / 2) * Math.sin(angle) +
+ ((h2 - h1) / 2) * Math.cos(angle),
+ };
+};
+
+export const moveElement = (
+ newTopLeftX: number,
+ newTopLeftY: number,
+ originalElement: ExcalidrawElement,
+ elementsMap: NonDeletedSceneElementsMap,
+ elements: readonly NonDeletedExcalidrawElement[],
+ scene: Scene,
+ originalElementsMap: ElementsMap,
+ shouldInformMutation = true,
+) => {
+ const latestElement = elementsMap.get(originalElement.id);
+ if (!latestElement) {
+ return;
+ }
+ const [cx, cy] = [
+ originalElement.x + originalElement.width / 2,
+ originalElement.y + originalElement.height / 2,
+ ];
+ const [topLeftX, topLeftY] = pointRotateRads(
+ pointFrom(originalElement.x, originalElement.y),
+ pointFrom(cx, cy),
+ originalElement.angle,
+ );
+
+ const changeInX = newTopLeftX - topLeftX;
+ const changeInY = newTopLeftY - topLeftY;
+
+ const [x, y] = pointRotateRads(
+ pointFrom(newTopLeftX, newTopLeftY),
+ pointFrom(cx + changeInX, cy + changeInY),
+ -originalElement.angle as Radians,
+ );
+
+ mutateElement(
+ latestElement,
+ {
+ x,
+ y,
+ },
+ shouldInformMutation,
+ );
+ updateBindings(latestElement, elementsMap, elements, scene);
+
+ const boundTextElement = getBoundTextElement(
+ originalElement,
+ originalElementsMap,
+ );
+ if (boundTextElement) {
+ const latestBoundTextElement = elementsMap.get(boundTextElement.id);
+ latestBoundTextElement &&
+ mutateElement(
+ latestBoundTextElement,
+ {
+ x: boundTextElement.x + changeInX,
+ y: boundTextElement.y + changeInY,
+ },
+ shouldInformMutation,
+ );
+ }
+};
+
+export const getAtomicUnits = (
+ targetElements: readonly ExcalidrawElement[],
+ appState: AppState,
+) => {
+ const selectedGroupIds = getSelectedGroupIds(appState);
+ const _atomicUnits = selectedGroupIds.map((gid) => {
+ return getElementsInGroup(targetElements, gid).reduce((acc, el) => {
+ acc[el.id] = true;
+ return acc;
+ }, {} as AtomicUnit);
+ });
+ targetElements
+ .filter((el) => !isInGroup(el))
+ .forEach((el) => {
+ _atomicUnits.push({
+ [el.id]: true,
+ });
+ });
+ return _atomicUnits;
+};
+
+export const updateBindings = (
+ latestElement: ExcalidrawElement,
+ elementsMap: NonDeletedSceneElementsMap,
+ elements: readonly NonDeletedExcalidrawElement[],
+ scene: Scene,
+ options?: {
+ simultaneouslyUpdated?: readonly ExcalidrawElement[];
+ newSize?: { width: number; height: number };
+ zoom?: AppState["zoom"];
+ },
+) => {
+ if (isLinearElement(latestElement)) {
+ bindOrUnbindLinearElements(
+ [latestElement],
+ elementsMap,
+ elements,
+ scene,
+ true,
+ [],
+ options?.zoom,
+ );
+ } else {
+ updateBoundElements(latestElement, elementsMap, options);
+ }
+};