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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,6 @@ import type {
ExcalidrawElement, ExcalidrawElement,
ExcalidrawLinearElement, ExcalidrawLinearElement,
ExcalidrawFreeDrawElement, ExcalidrawFreeDrawElement,
NonDeleted,
ExcalidrawTextElementWithContainer, ExcalidrawTextElementWithContainer,
ElementsMap, ElementsMap,
} from "./types"; } from "./types";
@ -643,33 +642,6 @@ export const getClosestElementBounds = (
return getElementBounds(closestElement, elementsMap); 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 * returns scene coords of user's editor viewport (visible canvas area) bounds
*/ */

View file

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

View file

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

View file

@ -1,6 +1,6 @@
import type { LineSegment } from "../math"; import type { LineSegment } from "../math";
import { isLineSegment, lineSegment, point, type GlobalPoint } 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"; import { isBounds } from "./element/typeChecks";
// The global data holder to collect the debug operations // 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 = ( export const debugDrawBounds = (
box: Bounds | Bounds[], box: Bounds | Bounds[],
opts?: { opts?: {

View file

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