Unify bounds calculation

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
Mark Tolmacs 2024-09-24 18:25:31 +02:00
parent c0915c6b98
commit 0c47bd0004
No known key found for this signature in database
9 changed files with 80 additions and 94 deletions

View file

@ -1,6 +1,6 @@
import { register } from "./register";
import { getSelectedElements } from "../scene";
import { getNonDeletedElements } from "../element";
import { getCommonBounds, getNonDeletedElements } from "../element";
import type {
ExcalidrawArrowElement,
ExcalidrawElbowArrowElement,
@ -12,7 +12,6 @@ import { resizeMultipleElements } from "../element/resizeElements";
import type { AppClassProperties, AppState } from "../types";
import { arrayToMap } from "../utils";
import { CODES, KEYS } from "../keys";
import { getCommonBoundingBox } from "../element/bounds";
import {
bindOrUnbindLinearElements,
isBindingEnabled,
@ -132,8 +131,9 @@ const flipElements = (
});
}
const { minX, minY, maxX, maxY, midX, midY } =
getCommonBoundingBox(selectedElements);
const [minX, minY, maxX, maxY] = getCommonBounds(selectedElements);
const midX = (minX + maxX) / 2;
const midY = (minY + maxY) / 2;
resizeMultipleElements(
elementsMap,
@ -175,8 +175,11 @@ const flipElements = (
{ elbowArrows: [], otherElements: [] },
);
const { midX: newMidX, midY: newMidY } =
getCommonBoundingBox(selectedElements);
const [newMinX, newMinY, newMaxX, newMaxY] =
getCommonBounds(selectedElements);
const newMidX = (newMinX + newMaxX) / 2;
const newMidY = (newMinY + newMaxY) / 2;
const [diffX, diffY] = [midX - newMidX, midY - newMidY];
otherElements.forEach((element) =>
mutateElement(element, {

View file

@ -1,7 +1,6 @@
import type { ElementsMap, ExcalidrawElement } from "./element/types";
import { newElementWith } from "./element/mutateElement";
import type { BoundingBox } from "./element/bounds";
import { getCommonBoundingBox } from "./element/bounds";
import { getCommonBounds, type Bounds } from "./element/bounds";
import { getMaximumGroups } from "./groups";
export interface Alignment {
@ -18,14 +17,10 @@ export const alignElements = (
selectedElements,
elementsMap,
);
const selectionBoundingBox = getCommonBoundingBox(selectedElements);
const selectionBounds = getCommonBounds(selectedElements);
return groups.flatMap((group) => {
const translation = calculateTranslation(
group,
selectionBoundingBox,
alignment,
);
const translation = calculateTranslation(group, selectionBounds, alignment);
return group.map((element) =>
newElementWith(element, {
x: element.x + translation.x,
@ -37,10 +32,30 @@ export const alignElements = (
const calculateTranslation = (
group: ExcalidrawElement[],
selectionBoundingBox: BoundingBox,
selectionBounds: Bounds,
{ axis, position }: Alignment,
): { x: number; y: number } => {
const groupBoundingBox = getCommonBoundingBox(group);
const selectionBoundingBox = {
minX: selectionBounds[0],
minY: selectionBounds[1],
maxX: selectionBounds[2],
maxY: selectionBounds[3],
midX: (selectionBounds[0] + selectionBounds[2]) / 2,
midY: (selectionBounds[1] + selectionBounds[3]) / 2,
width: selectionBounds[2] - selectionBounds[0],
height: selectionBounds[3] - selectionBounds[1],
};
const groupBounds = getCommonBounds(group);
const groupBoundingBox = {
minX: groupBounds[0],
minY: groupBounds[1],
maxX: groupBounds[2],
maxY: groupBounds[3],
midX: (groupBounds[0] + groupBounds[2]) / 2,
midY: (groupBounds[1] + groupBounds[3]) / 2,
width: groupBounds[2] - groupBounds[0],
height: groupBounds[3] - groupBounds[1],
};
const [min, max]: ["minX" | "minY", "maxX" | "maxY"] =
axis === "x" ? ["minX", "maxX"] : ["minY", "maxY"];

View file

@ -11,7 +11,6 @@ import type App from "../components/App";
import { atom } from "jotai";
import { jotaiStore } from "../jotai";
import type { ExcalidrawElement } from "../element/types";
import { getCommonBoundingBox } from "../element/bounds";
import { AbortError } from "../errors";
import { t } from "../i18n";
import { useEffect, useRef } from "react";
@ -34,7 +33,7 @@ import {
import type { MaybePromise } from "../utility-types";
import { Emitter } from "../emitter";
import { Queue } from "../queue";
import { hashElementsVersion, hashString } from "../element";
import { getCommonBounds, hashElementsVersion, hashString } from "../element";
type LibraryUpdate = {
/** deleted library items since last onLibraryChange event */
@ -387,7 +386,8 @@ export const distributeLibraryItemsOnSquareGrid = (
const maxHeight = libraryItems
.slice(row * ITEMS_PER_ROW, row * ITEMS_PER_ROW + ITEMS_PER_ROW)
.reduce((acc, item) => {
const { height } = getCommonBoundingBox(item.elements);
const bounds = getCommonBounds(item.elements);
const height = bounds[3] - bounds[1];
return Math.max(acc, height);
}, 0);
return maxHeight;
@ -402,7 +402,9 @@ export const distributeLibraryItemsOnSquareGrid = (
currCol = 0;
}
if (currCol === targetCol) {
const { width } = getCommonBoundingBox(item.elements);
const bounds = getCommonBounds(item.elements);
const width = bounds[2] - bounds[0];
maxWidth = Math.max(maxWidth, width);
}
index++;
@ -434,7 +436,10 @@ export const distributeLibraryItemsOnSquareGrid = (
}
maxWidthCurrCol = getMaxWidthPerCol(col);
const { minX, minY, width, height } = getCommonBoundingBox(item.elements);
const bounds = getCommonBounds(item.elements);
const [minX, minY, maxX, maxY] = bounds;
const width = maxX - minX;
const height = maxY - minY;
const offsetCenterX = (maxWidthCurrCol - width) / 2;
const offsetCenterY = (maxHeightCurrRow - height) / 2;
resElements.push(

View file

@ -1,7 +1,7 @@
import { newElementWith } from "./element/mutateElement";
import { getMaximumGroups } from "./groups";
import { getCommonBoundingBox } from "./element/bounds";
import type { ElementsMap, ExcalidrawElement } from "./element/types";
import { getCommonBounds } from "./element/bounds";
export interface Distribution {
space: "between";
@ -18,9 +18,35 @@ export const distributeElements = (
? (["minX", "midX", "maxX", "width"] as const)
: (["minY", "midY", "maxY", "height"] as const);
const bounds = getCommonBoundingBox(selectedElements);
const bb = getCommonBounds(selectedElements);
const bounds = {
minX: bb[0],
minY: bb[1],
maxX: bb[2],
maxY: bb[3],
midX: (bb[0] + bb[2]) / 2,
midY: (bb[1] + bb[3]) / 2,
width: bb[2] - bb[0],
height: bb[3] - bb[1],
};
const groups = getMaximumGroups(selectedElements, elementsMap)
.map((group) => [group, getCommonBoundingBox(group)] as const)
.map((group) => {
const bounds = getCommonBounds(group);
return [
group,
{
minX: bounds[0],
minY: bounds[1],
maxX: bounds[2],
maxY: bounds[3],
midX: (bounds[0] + bounds[2]) / 2,
midY: (bounds[1] + bounds[3]) / 2,
width: bounds[2] - bounds[0],
height: bounds[3] - bounds[1],
},
] as const;
})
.sort((a, b) => a[1][mid] - b[1][mid]);
let span = 0;

View file

@ -2,7 +2,6 @@ import type {
ExcalidrawElement,
ExcalidrawLinearElement,
ExcalidrawFreeDrawElement,
NonDeleted,
ExcalidrawTextElementWithContainer,
ElementsMap,
} from "./types";
@ -643,33 +642,6 @@ export const getClosestElementBounds = (
return getElementBounds(closestElement, elementsMap);
};
export interface BoundingBox {
minX: number;
minY: number;
maxX: number;
maxY: number;
midX: number;
midY: number;
width: number;
height: number;
}
export const getCommonBoundingBox = (
elements: ExcalidrawElement[] | readonly NonDeleted<ExcalidrawElement>[],
): BoundingBox => {
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
return {
minX,
minY,
maxX,
maxY,
width: maxX - minX,
height: maxY - minY,
midX: (minX + maxX) / 2,
midY: (minY + maxY) / 2,
};
};
/**
* returns scene coords of user's editor viewport (visible canvas area) bounds
*/

View file

@ -17,7 +17,6 @@ import {
getElementAbsoluteCoords,
getCommonBounds,
getResizedElementAbsoluteCoords,
getCommonBoundingBox,
} from "./bounds";
import {
isArrowElement,
@ -808,9 +807,11 @@ export const resizeMultipleElements = (
return [...acc, { ...text, ...xy }];
}, [] as ExcalidrawTextElementWithContainer[]);
const { minX, minY, maxX, maxY, midX, midY } = getCommonBoundingBox(
const [minX, minY, maxX, maxY] = getCommonBounds(
targetElements.map(({ orig }) => orig).concat(boundTextElements),
);
const midX = (minX + maxX) / 2;
const midY = (minY + maxY) / 2;
const width = maxX - minX;
const height = maxY - minY;

View file

@ -3,7 +3,7 @@ import { vi } from "vitest";
import { fireEvent, render, waitFor } from "./test-utils";
import { act, queryByTestId } from "@testing-library/react";
import { Excalidraw } from "../index";
import { Excalidraw, getCommonBounds } from "../index";
import { API } from "./helpers/api";
import { MIME_TYPES } from "../constants";
import type { LibraryItem, LibraryItems } from "../types";
@ -11,7 +11,6 @@ import { UI } from "./helpers/ui";
import { serializeLibraryAsJSON } from "../data/json";
import { distributeLibraryItemsOnSquareGrid } from "../data/library";
import type { ExcalidrawGenericElement } from "../element/types";
import { getCommonBoundingBox } from "../element/bounds";
import { parseLibraryJSON } from "../data/blob";
const { h } = window;
@ -294,6 +293,7 @@ describe("distributeLibraryItemsOnSquareGrid()", () => {
expect(distributed.length).toEqual(
libraryItems.map((x) => x.elements).flat().length,
);
const el4_el5_bounds = getCommonBounds([el4, el5]);
expect(distributed).toEqual(
expect.arrayContaining([
expect.objectContaining({
@ -306,7 +306,7 @@ describe("distributeLibraryItemsOnSquareGrid()", () => {
x:
el1.width +
PADDING +
(getCommonBoundingBox([el4, el5]).width - el2.width) / 2,
(el4_el5_bounds[2] - el4_el5_bounds[0] - el2.width) / 2,
y: Math.abs(el1.height - el2.height) / 2,
}),
expect.objectContaining({

View file

@ -1,6 +1,6 @@
import type { LineSegment } from "../math";
import { isLineSegment, lineSegment, point, type GlobalPoint } from "../math";
import type { BoundingBox, Bounds } from "./element/bounds";
import type { Bounds } from "./element/bounds";
import { isBounds } from "./element/typeChecks";
// The global data holder to collect the debug operations
@ -72,41 +72,6 @@ export const debugDrawPoint = (
);
};
export const debugDrawBoundingBox = (
box: BoundingBox | BoundingBox[],
opts?: {
color?: string;
permanent?: boolean;
},
) => {
(Array.isArray(box) ? box : [box]).forEach((bbox) =>
debugDrawLine(
[
lineSegment(
point<GlobalPoint>(bbox.minX, bbox.minY),
point<GlobalPoint>(bbox.maxX, bbox.minY),
),
lineSegment(
point<GlobalPoint>(bbox.maxX, bbox.minY),
point<GlobalPoint>(bbox.maxX, bbox.maxY),
),
lineSegment(
point<GlobalPoint>(bbox.maxX, bbox.maxY),
point<GlobalPoint>(bbox.minX, bbox.maxY),
),
lineSegment(
point<GlobalPoint>(bbox.minX, bbox.maxY),
point<GlobalPoint>(bbox.minX, bbox.minY),
),
],
{
color: opts?.color ?? "cyan",
permanent: opts?.permanent,
},
),
);
};
export const debugDrawBounds = (
box: Bounds | Bounds[],
opts?: {

View file

@ -1,3 +1,2 @@
export * from "./export";
export * from "./withinBounds";
export { getCommonBounds } from "../excalidraw/element/bounds";