chore: Introducing RotatedBounds

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
Mark Tolmacs
2026-03-30 14:17:53 +00:00
parent 7c766bdfc4
commit f3dba020b0
9 changed files with 176 additions and 110 deletions
+36 -1
View File
@@ -1,3 +1,5 @@
import type { Radians } from "@excalidraw/math";
/** /**
* x and y position of top left corner, x and y position of bottom right corner * x and y position of top left corner, x and y position of bottom right corner
*/ */
@@ -6,7 +8,31 @@ export type Bounds = readonly [
minY: number, minY: number,
maxX: number, maxX: number,
maxY: number, maxY: number,
]; ] & { _brand: "excalidraw__bounds" };
export type RotatedBounds = readonly [
minX: number,
minY: number,
maxX: number,
maxY: number,
angle: Radians,
] & {
_brand_rotated: "excalidraw__rotated_bounds";
};
export const bounds = <T extends Radians | undefined = undefined>(
minX: number,
minY: number,
maxX: number,
maxY: number,
angle: T = undefined as T,
) => {
return (
angle
? ([minX, minY, maxX, maxY, angle] as unknown)
: ([minX, minY, maxX, maxY] as unknown)
) as T extends Radians ? RotatedBounds : Bounds;
};
export const isBounds = (box: unknown): box is Bounds => export const isBounds = (box: unknown): box is Bounds =>
Array.isArray(box) && Array.isArray(box) &&
@@ -15,3 +41,12 @@ export const isBounds = (box: unknown): box is Bounds =>
typeof box[1] === "number" && typeof box[1] === "number" &&
typeof box[2] === "number" && typeof box[2] === "number" &&
typeof box[3] === "number"; typeof box[3] === "number";
export const isRotatedBounds = (box: unknown): box is RotatedBounds =>
Array.isArray(box) &&
box.length === 5 &&
typeof box[0] === "number" &&
typeof box[1] === "number" &&
typeof box[2] === "number" &&
typeof box[3] === "number" &&
typeof box[4] === "number";
+71 -62
View File
@@ -2,9 +2,11 @@ import rough from "roughjs/bin/rough";
import { import {
arrayToMap, arrayToMap,
bounds,
type Bounds, type Bounds,
invariant, invariant,
rescalePoints, rescalePoints,
type RotatedBounds,
sizeOf, sizeOf,
} from "@excalidraw/common"; } from "@excalidraw/common";
@@ -27,7 +29,6 @@ import type {
GlobalPoint, GlobalPoint,
LineSegment, LineSegment,
LocalPoint, LocalPoint,
NonRotated,
Radians, Radians,
} from "@excalidraw/math"; } from "@excalidraw/math";
@@ -91,7 +92,7 @@ export class ElementBounds {
private static boundsCache = new WeakMap< private static boundsCache = new WeakMap<
ExcalidrawElement, ExcalidrawElement,
{ {
bounds: Bounds; bounds: RotatedBounds;
version: ExcalidrawElement["version"]; version: ExcalidrawElement["version"];
} }
>(); >();
@@ -103,11 +104,11 @@ export class ElementBounds {
} }
>(); >();
static getBounds( static getBounds<T extends boolean = false>(
element: ExcalidrawElement, element: ExcalidrawElement,
elementsMap: ElementsMap, elementsMap: ElementsMap,
nonRotated: boolean = false, nonRotated: T = false as T,
) { ): T extends true ? Bounds : RotatedBounds {
const cachedBounds = const cachedBounds =
nonRotated && element.angle !== 0 nonRotated && element.angle !== 0
? ElementBounds.nonRotatedBoundsCache.get(element) ? ElementBounds.nonRotatedBoundsCache.get(element)
@@ -120,40 +121,41 @@ export class ElementBounds {
// which is causing problems down the line. Fix TBA. // which is causing problems down the line. Fix TBA.
!isBoundToContainer(element) !isBoundToContainer(element)
) { ) {
return cachedBounds.bounds; return cachedBounds.bounds as T extends true ? Bounds : RotatedBounds;
} }
if (nonRotated && element.angle !== 0) { if (nonRotated && element.angle !== 0) {
const nonRotatedBounds = ElementBounds.calculateBounds( const [minX, minY, maxX, maxY] = ElementBounds.calculateBounds(
{ {
...element, ...element,
angle: 0 as Radians, angle: 0 as Radians,
}, },
elementsMap, elementsMap,
); );
const nonRotatedBounds = bounds(minX, minY, maxX, maxY);
ElementBounds.nonRotatedBoundsCache.set(element, { ElementBounds.nonRotatedBoundsCache.set(element, {
version: element.version, version: element.version,
bounds: nonRotatedBounds, bounds: nonRotatedBounds,
}); });
return nonRotatedBounds; return nonRotatedBounds as T extends true ? Bounds : RotatedBounds;
} }
const bounds = ElementBounds.calculateBounds(element, elementsMap); const _bounds = ElementBounds.calculateBounds(element, elementsMap);
ElementBounds.boundsCache.set(element, { ElementBounds.boundsCache.set(element, {
version: element.version, version: element.version,
bounds, bounds: _bounds,
}); });
return bounds; return _bounds as T extends true ? Bounds : RotatedBounds;
} }
private static calculateBounds( private static calculateBounds(
element: ExcalidrawElement, element: ExcalidrawElement,
elementsMap: ElementsMap, elementsMap: ElementsMap,
): Bounds { ): RotatedBounds {
let bounds: Bounds; let _bounds: RotatedBounds;
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords( const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
element, element,
@@ -170,14 +172,15 @@ export class ElementBounds {
), ),
); );
return [ return bounds(
minX + element.x, minX + element.x,
minY + element.y, minY + element.y,
maxX + element.x, maxX + element.x,
maxY + element.y, maxY + element.y,
]; element.angle,
);
} else if (isLinearElement(element)) { } else if (isLinearElement(element)) {
bounds = getLinearElementRotatedBounds(element, cx, cy, elementsMap); _bounds = getLinearElementRotatedBounds(element, cx, cy, elementsMap);
} else if (element.type === "diamond") { } else if (element.type === "diamond") {
const [x11, y11] = pointRotateRads( const [x11, y11] = pointRotateRads(
pointFrom(cx, y1), pointFrom(cx, y1),
@@ -203,7 +206,7 @@ export class ElementBounds {
const minY = Math.min(y11, y12, y22, y21); const minY = Math.min(y11, y12, y22, y21);
const maxX = Math.max(x11, x12, x22, x21); const maxX = Math.max(x11, x12, x22, x21);
const maxY = Math.max(y11, y12, y22, y21); const maxY = Math.max(y11, y12, y22, y21);
bounds = [minX, minY, maxX, maxY]; _bounds = bounds(minX, minY, maxX, maxY, element.angle);
} else if (element.type === "ellipse") { } else if (element.type === "ellipse") {
const w = (x2 - x1) / 2; const w = (x2 - x1) / 2;
const h = (y2 - y1) / 2; const h = (y2 - y1) / 2;
@@ -211,7 +214,7 @@ export class ElementBounds {
const sin = Math.sin(element.angle); const sin = Math.sin(element.angle);
const ww = Math.hypot(w * cos, h * sin); const ww = Math.hypot(w * cos, h * sin);
const hh = Math.hypot(h * cos, w * sin); const hh = Math.hypot(h * cos, w * sin);
bounds = [cx - ww, cy - hh, cx + ww, cy + hh]; _bounds = bounds(cx - ww, cy - hh, cx + ww, cy + hh, element.angle);
} else { } else {
const [x11, y11] = pointRotateRads( const [x11, y11] = pointRotateRads(
pointFrom(x1, y1), pointFrom(x1, y1),
@@ -237,10 +240,10 @@ export class ElementBounds {
const minY = Math.min(y11, y12, y22, y21); const minY = Math.min(y11, y12, y22, y21);
const maxX = Math.max(x11, x12, x22, x21); const maxX = Math.max(x11, x12, x22, x21);
const maxY = Math.max(y11, y12, y22, y21); const maxY = Math.max(y11, y12, y22, y21);
bounds = [minX, minY, maxX, maxY]; _bounds = bounds(minX, minY, maxX, maxY, element.angle);
} }
return bounds; return _bounds;
} }
} }
@@ -625,7 +628,7 @@ export const getCubicBezierCurveBound = (
minY = Math.min(minY, ...ys); minY = Math.min(minY, ...ys);
maxY = Math.max(maxY, ...ys); maxY = Math.max(maxY, ...ys);
} }
return [minX, minY, maxX, maxY]; return bounds(minX, minY, maxX, maxY);
}; };
export const getMinMaxXYFromCurvePathOps = ( export const getMinMaxXYFromCurvePathOps = (
@@ -678,7 +681,7 @@ export const getMinMaxXYFromCurvePathOps = (
}, },
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }, { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
); );
return [minX, minY, maxX, maxY]; return bounds(minX, minY, maxX, maxY);
}; };
export const getBoundsFromPoints = ( export const getBoundsFromPoints = (
@@ -696,7 +699,7 @@ export const getBoundsFromPoints = (
maxY = Math.max(maxY, y); maxY = Math.max(maxY, y);
} }
return [minX, minY, maxX, maxY]; return bounds(minX, minY, maxX, maxY);
}; };
const getFreeDrawElementAbsoluteCoords = ( const getFreeDrawElementAbsoluteCoords = (
@@ -939,7 +942,7 @@ const getLinearElementRotatedBounds = (
cx: number, cx: number,
cy: number, cy: number,
elementsMap: ElementsMap, elementsMap: ElementsMap,
): Bounds => { ): RotatedBounds => {
const boundTextElement = getBoundTextElement(element, elementsMap); const boundTextElement = getBoundTextElement(element, elementsMap);
if (element.points.length < 2) { if (element.points.length < 2) {
@@ -950,20 +953,21 @@ const getLinearElementRotatedBounds = (
element.angle, element.angle,
); );
let coords: Bounds = [x, y, x, y]; let coords: RotatedBounds = bounds(x, y, x, y, element.angle);
if (boundTextElement) { if (boundTextElement) {
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText( const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
element, element,
elementsMap, elementsMap,
[x, y, x, y], bounds(x, y, x, y, element.angle),
boundTextElement, boundTextElement,
); );
coords = [ coords = bounds(
coordsWithBoundText[0], coordsWithBoundText[0],
coordsWithBoundText[1], coordsWithBoundText[1],
coordsWithBoundText[2], coordsWithBoundText[2],
coordsWithBoundText[3], coordsWithBoundText[3],
]; element.angle,
);
} }
return coords; return coords;
} }
@@ -979,7 +983,13 @@ const getLinearElementRotatedBounds = (
element.angle, element.angle,
); );
const res = getMinMaxXYFromCurvePathOps(ops, transformXY); const res = getMinMaxXYFromCurvePathOps(ops, transformXY);
let coords: Bounds = [res[0], res[1], res[2], res[3]]; let coords: RotatedBounds = bounds(
res[0],
res[1],
res[2],
res[3],
element.angle,
);
if (boundTextElement) { if (boundTextElement) {
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText( const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
element, element,
@@ -987,12 +997,13 @@ const getLinearElementRotatedBounds = (
coords, coords,
boundTextElement, boundTextElement,
); );
coords = [ coords = bounds(
coordsWithBoundText[0], coordsWithBoundText[0],
coordsWithBoundText[1], coordsWithBoundText[1],
coordsWithBoundText[2], coordsWithBoundText[2],
coordsWithBoundText[3], coordsWithBoundText[3],
]; element.angle,
);
} }
return coords; return coords;
}; };
@@ -1001,12 +1012,8 @@ export const getElementBounds = <T extends boolean = false>(
element: ExcalidrawElement, element: ExcalidrawElement,
elementsMap: ElementsMap, elementsMap: ElementsMap,
nonRotated: T = false as T, nonRotated: T = false as T,
): T extends true ? NonRotated<Bounds> : Bounds => { ) => {
return ElementBounds.getBounds( return ElementBounds.getBounds<T>(element, elementsMap, nonRotated);
element,
elementsMap,
nonRotated,
) as T extends true ? NonRotated<Bounds> : Bounds;
}; };
export const getCommonBounds = ( export const getCommonBounds = (
@@ -1014,7 +1021,7 @@ export const getCommonBounds = (
elementsMap?: ElementsMap, elementsMap?: ElementsMap,
): Bounds => { ): Bounds => {
if (!sizeOf(elements)) { if (!sizeOf(elements)) {
return [0, 0, 0, 0]; return bounds(0, 0, 0, 0);
} }
let minX = Infinity; let minX = Infinity;
@@ -1032,7 +1039,7 @@ export const getCommonBounds = (
maxY = Math.max(maxY, y2); maxY = Math.max(maxY, y2);
}); });
return [minX, minY, maxX, maxY]; return bounds(minX, minY, maxX, maxY);
}; };
export const getDraggedElementsBounds = ( export const getDraggedElementsBounds = (
@@ -1055,12 +1062,12 @@ export const getResizedElementAbsoluteCoords = (
normalizePoints: boolean, normalizePoints: boolean,
): Bounds => { ): Bounds => {
if (!(isLinearElement(element) || isFreeDrawElement(element))) { if (!(isLinearElement(element) || isFreeDrawElement(element))) {
return [ return bounds(
element.x, element.x,
element.y, element.y,
element.x + nextWidth, element.x + nextWidth,
element.y + nextHeight, element.y + nextHeight,
]; );
} }
const points = rescalePoints( const points = rescalePoints(
@@ -1070,11 +1077,11 @@ export const getResizedElementAbsoluteCoords = (
normalizePoints, normalizePoints,
); );
let bounds: Bounds; let _bounds: Bounds;
if (isFreeDrawElement(element)) { if (isFreeDrawElement(element)) {
// Free Draw // Free Draw
bounds = getBoundsFromPoints(points); _bounds = getBoundsFromPoints(points);
} else { } else {
// Line // Line
const gen = rough.generator(); const gen = rough.generator();
@@ -1086,16 +1093,16 @@ export const getResizedElementAbsoluteCoords = (
: gen.curve(points as [number, number][], generateRoughOptions(element)); : gen.curve(points as [number, number][], generateRoughOptions(element));
const ops = getCurvePathOps(curve); const ops = getCurvePathOps(curve);
bounds = getMinMaxXYFromCurvePathOps(ops); _bounds = getMinMaxXYFromCurvePathOps(ops);
} }
const [minX, minY, maxX, maxY] = bounds; const [minX, minY, maxX, maxY] = _bounds;
return [ return bounds(
minX + element.x, minX + element.x,
minY + element.y, minY + element.y,
maxX + element.x, maxX + element.x,
maxY + element.y, maxY + element.y,
]; );
}; };
export const getElementPointsCoords = ( export const getElementPointsCoords = (
@@ -1113,20 +1120,20 @@ export const getElementPointsCoords = (
: gen.curve(points as [number, number][], generateRoughOptions(element)); : gen.curve(points as [number, number][], generateRoughOptions(element));
const ops = getCurvePathOps(curve); const ops = getCurvePathOps(curve);
const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops); const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops);
return [ return bounds(
minX + element.x, minX + element.x,
minY + element.y, minY + element.y,
maxX + element.x, maxX + element.x,
maxY + element.y, maxY + element.y,
]; );
}; };
export const getClosestElementBounds = ( export const getClosestElementBounds = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
from: { x: number; y: number }, from: { x: number; y: number },
): Bounds => { ): RotatedBounds => {
if (!elements.length) { if (!elements.length) {
return [0, 0, 0, 0]; return bounds(0, 0, 0, 0, 0 as Radians);
} }
let minDistance = Infinity; let minDistance = Infinity;
@@ -1195,7 +1202,9 @@ export const getVisibleSceneBounds = ({
]; ];
}; };
export const getCenterForBounds = (bounds: Bounds): GlobalPoint => export const getCenterForBounds = (
bounds: Bounds | RotatedBounds,
): GlobalPoint =>
pointFrom( pointFrom(
bounds[0] + (bounds[2] - bounds[0]) / 2, bounds[0] + (bounds[2] - bounds[0]) / 2,
bounds[1] + (bounds[3] - bounds[1]) / 2, bounds[1] + (bounds[3] - bounds[1]) / 2,
@@ -1240,24 +1249,24 @@ export const aabbForElement = (
element.angle, element.angle,
); );
const bounds = [ const _bounds = bounds(
Math.min(topLeftX, topRightX, bottomRightX, bottomLeftX), Math.min(topLeftX, topRightX, bottomRightX, bottomLeftX),
Math.min(topLeftY, topRightY, bottomRightY, bottomLeftY), Math.min(topLeftY, topRightY, bottomRightY, bottomLeftY),
Math.max(topLeftX, topRightX, bottomRightX, bottomLeftX), Math.max(topLeftX, topRightX, bottomRightX, bottomLeftX),
Math.max(topLeftY, topRightY, bottomRightY, bottomLeftY), Math.max(topLeftY, topRightY, bottomRightY, bottomLeftY),
] as Bounds; );
if (offset) { if (offset) {
const [topOffset, rightOffset, downOffset, leftOffset] = offset; const [topOffset, rightOffset, downOffset, leftOffset] = offset;
return [ return bounds(
bounds[0] - leftOffset, _bounds[0] - leftOffset,
bounds[1] - topOffset, _bounds[1] - topOffset,
bounds[2] + rightOffset, _bounds[2] + rightOffset,
bounds[3] + downOffset, _bounds[3] + downOffset,
] as Bounds; );
} }
return bounds; return _bounds;
}; };
export const pointInsideBounds = <P extends GlobalPoint | LocalPoint>( export const pointInsideBounds = <P extends GlobalPoint | LocalPoint>(
@@ -1266,7 +1275,7 @@ export const pointInsideBounds = <P extends GlobalPoint | LocalPoint>(
): boolean => ): boolean =>
p[0] > bounds[0] && p[0] < bounds[2] && p[1] > bounds[1] && p[1] < bounds[3]; p[0] > bounds[0] && p[0] < bounds[2] && p[1] > bounds[1] && p[1] < bounds[3];
export const doBoundsIntersect = ( export const doNonRotatedBoundsIntersect = (
bounds1: Bounds | null, bounds1: Bounds | null,
bounds2: Bounds | null, bounds2: Bounds | null,
): boolean => { ): boolean => {
+27 -18
View File
@@ -1,4 +1,9 @@
import { invariant, isTransparent, type Bounds } from "@excalidraw/common"; import {
bounds,
invariant,
isTransparent,
type Bounds,
} from "@excalidraw/common";
import { import {
curveIntersectLineSegment, curveIntersectLineSegment,
isPointWithinBounds, isPointWithinBounds,
@@ -22,7 +27,6 @@ import type {
Curve, Curve,
GlobalPoint, GlobalPoint,
LineSegment, LineSegment,
NonRotated,
Radians, Radians,
} from "@excalidraw/math"; } from "@excalidraw/math";
@@ -30,7 +34,7 @@ import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
import { isPathALoop } from "./utils"; import { isPathALoop } from "./utils";
import { import {
doBoundsIntersect, doNonRotatedBoundsIntersect,
elementCenterPoint, elementCenterPoint,
getCenterForBounds, getCenterForBounds,
getCubicBezierCurveBound, getCubicBezierCurveBound,
@@ -194,7 +198,7 @@ export function getBoundsCorners(
bounds: Bounds, bounds: Bounds,
): readonly [GlobalPoint, GlobalPoint, GlobalPoint, GlobalPoint]; ): readonly [GlobalPoint, GlobalPoint, GlobalPoint, GlobalPoint];
export function getBoundsCorners( export function getBoundsCorners(
bounds: NonRotated<Bounds>, bounds: Bounds,
angle: Radians, angle: Radians,
): readonly [GlobalPoint, GlobalPoint, GlobalPoint, GlobalPoint]; ): readonly [GlobalPoint, GlobalPoint, GlobalPoint, GlobalPoint];
export function getBoundsCorners( export function getBoundsCorners(
@@ -234,7 +238,7 @@ export const getBoundsEdges = (
const isPointInRotatedBounds = ( const isPointInRotatedBounds = (
point: GlobalPoint, point: GlobalPoint,
bounds: NonRotated<Bounds>, bounds: Bounds,
angle: Radians, angle: Radians,
tolerance = 0, tolerance = 0,
) => { ) => {
@@ -344,9 +348,13 @@ const bindingBorderTest = (
// PERF: Run a cheap test to see if the binding element // PERF: Run a cheap test to see if the binding element
// is even close to the element // is even close to the element
const t = Math.max(1, tolerance); const t = Math.max(1, tolerance);
const bounds = [x - t, y - t, x + t, y + t] as Bounds; const elementBounds = getElementBounds(element, elementsMap, true);
const elementBounds = getElementBounds(element, elementsMap); if (
if (!doBoundsIntersect(bounds, elementBounds)) { !doNonRotatedBoundsIntersect(
bounds(x - t, y - t, x + t, y + t),
elementBounds,
)
) {
return false; return false;
} }
@@ -357,6 +365,7 @@ const bindingBorderTest = (
const enclosingFrameBounds = getElementBounds( const enclosingFrameBounds = getElementBounds(
enclosingFrame, enclosingFrame,
elementsMap, elementsMap,
true,
); );
if (!pointInsideBounds(p, enclosingFrameBounds)) { if (!pointInsideBounds(p, enclosingFrameBounds)) {
return false; return false;
@@ -506,15 +515,15 @@ export const intersectElementWithLineSegment = (
): GlobalPoint[] => { ): GlobalPoint[] => {
// First check if the line intersects the element's axis-aligned bounding box // First check if the line intersects the element's axis-aligned bounding box
// as it is much faster than checking intersection against the element's shape // as it is much faster than checking intersection against the element's shape
const intersectorBounds = [ const intersectorBounds = bounds(
Math.min(line[0][0] - offset, line[1][0] - offset), Math.min(line[0][0] - offset, line[1][0] - offset),
Math.min(line[0][1] - offset, line[1][1] - offset), Math.min(line[0][1] - offset, line[1][1] - offset),
Math.max(line[0][0] + offset, line[1][0] + offset), Math.max(line[0][0] + offset, line[1][0] + offset),
Math.max(line[0][1] + offset, line[1][1] + offset), Math.max(line[0][1] + offset, line[1][1] + offset),
] as Bounds; );
const elementBounds = getElementBounds(element, elementsMap); const elementBounds = getElementBounds(element, elementsMap, true);
if (!doBoundsIntersect(intersectorBounds, elementBounds)) { if (!doNonRotatedBoundsIntersect(intersectorBounds, elementBounds)) {
return []; return [];
} }
@@ -573,14 +582,14 @@ const curveIntersections = (
for (const c of curves) { for (const c of curves) {
// Optimize by doing a cheap bounding box check first // Optimize by doing a cheap bounding box check first
const b1 = getCubicBezierCurveBound(c[0], c[1], c[2], c[3]); const b1 = getCubicBezierCurveBound(c[0], c[1], c[2], c[3]);
const b2 = [ const b2 = bounds(
Math.min(segment[0][0], segment[1][0]), Math.min(segment[0][0], segment[1][0]),
Math.min(segment[0][1], segment[1][1]), Math.min(segment[0][1], segment[1][1]),
Math.max(segment[0][0], segment[1][0]), Math.max(segment[0][0], segment[1][0]),
Math.max(segment[0][1], segment[1][1]), Math.max(segment[0][1], segment[1][1]),
] as Bounds; );
if (!doBoundsIntersect(b1, b2)) { if (!doNonRotatedBoundsIntersect(b1, b2)) {
continue; continue;
} }
@@ -650,14 +659,14 @@ const intersectLinearOrFreeDrawWithLineSegment = (
for (const c of curves) { for (const c of curves) {
// Optimize by doing a cheap bounding box check first // Optimize by doing a cheap bounding box check first
const b1 = getCubicBezierCurveBound(c[0], c[1], c[2], c[3]); const b1 = getCubicBezierCurveBound(c[0], c[1], c[2], c[3]);
const b2 = [ const b2 = bounds(
Math.min(segment[0][0], segment[1][0]), Math.min(segment[0][0], segment[1][0]),
Math.min(segment[0][1], segment[1][1]), Math.min(segment[0][1], segment[1][1]),
Math.max(segment[0][0], segment[1][0]), Math.max(segment[0][0], segment[1][0]),
Math.max(segment[0][1], segment[1][1]), Math.max(segment[0][1], segment[1][1]),
] as Bounds; );
if (!doBoundsIntersect(b1, b2)) { if (!doNonRotatedBoundsIntersect(b1, b2)) {
continue; continue;
} }
+4 -3
View File
@@ -21,6 +21,7 @@ import {
invariant, invariant,
isShallowEqual, isShallowEqual,
getFeatureFlag, getFeatureFlag,
bounds,
} from "@excalidraw/common"; } from "@excalidraw/common";
import { import {
@@ -42,7 +43,7 @@ import type {
NullableGridSize, NullableGridSize,
Zoom, Zoom,
} from "@excalidraw/excalidraw/types"; } from "@excalidraw/excalidraw/types";
import type { Bounds } from "@excalidraw/common"; import type { RotatedBounds } from "@excalidraw/common";
import { import {
calculateFixedPointForNonElbowArrowBinding, calculateFixedPointForNonElbowArrowBinding,
@@ -1879,7 +1880,7 @@ export class LinearElementEditor {
static getMinMaxXYWithBoundText = ( static getMinMaxXYWithBoundText = (
element: ExcalidrawLinearElement, element: ExcalidrawLinearElement,
elementsMap: ElementsMap, elementsMap: ElementsMap,
elementBounds: Bounds, elementBounds: RotatedBounds,
boundTextElement: ExcalidrawTextElementWithContainer, boundTextElement: ExcalidrawTextElementWithContainer,
): [number, number, number, number, number, number] => { ): [number, number, number, number, number, number] => {
let [x1, y1, x2, y2] = elementBounds; let [x1, y1, x2, y2] = elementBounds;
@@ -2004,7 +2005,7 @@ export class LinearElementEditor {
return LinearElementEditor.getMinMaxXYWithBoundText( return LinearElementEditor.getMinMaxXYWithBoundText(
element, element,
elementsMap, elementsMap,
[x1, y1, x2, y2], bounds(x1, y1, x2, y2, element.angle),
boundTextElement, boundTextElement,
); );
} }
+2 -2
View File
@@ -246,7 +246,7 @@ import {
bindOrUnbindBindingElement, bindOrUnbindBindingElement,
mutateElement, mutateElement,
getElementBounds, getElementBounds,
doBoundsIntersect, doNonRotatedBoundsIntersect,
isPointInElement, isPointInElement,
maxBindingDistance_simple, maxBindingDistance_simple,
convertToExcalidrawElements, convertToExcalidrawElements,
@@ -1154,7 +1154,7 @@ class App extends React.Component<AppProps, AppState> {
startBounds && startBounds &&
endBounds && endBounds &&
startElement.id !== endElement.id && startElement.id !== endElement.id &&
doBoundsIntersect(startBounds, endBounds) doNonRotatedBoundsIntersect(startBounds, endBounds)
); );
} }
+2 -2
View File
@@ -2,7 +2,7 @@ import { arrayToMap, easeOut, THEME } from "@excalidraw/common";
import { import {
computeBoundTextPosition, computeBoundTextPosition,
doBoundsIntersect, doNonRotatedBoundsIntersect,
getBoundTextElement, getBoundTextElement,
getElementBounds, getElementBounds,
getElementLineSegments, getElementLineSegments,
@@ -219,7 +219,7 @@ const eraserTest = (
origElementBounds[3] + threshold, origElementBounds[3] + threshold,
]; ];
if (!doBoundsIntersect(segmentBounds, elementBounds)) { if (!doNonRotatedBoundsIntersect(segmentBounds, elementBounds)) {
return false; return false;
} }
+2 -2
View File
@@ -10,7 +10,7 @@ import { type Bounds } from "@excalidraw/common";
import { import {
computeBoundTextPosition, computeBoundTextPosition,
doBoundsIntersect, doNonRotatedBoundsIntersect,
getBoundTextElement, getBoundTextElement,
getElementBounds, getElementBounds,
intersectElementWithLineSegment, intersectElementWithLineSegment,
@@ -66,7 +66,7 @@ export const getLassoSelectedElementIds = (input: {
const elementBounds = getElementBounds(element, elementsMap); const elementBounds = getElementBounds(element, elementsMap);
if ( if (
doBoundsIntersect(lassoBounds, elementBounds) && doNonRotatedBoundsIntersect(lassoBounds, elementBounds) &&
!intersectedElements.has(element.id) && !intersectedElements.has(element.id) &&
!enclosedElements.has(element.id) !enclosedElements.has(element.id)
) { ) {
+11 -6
View File
@@ -1,3 +1,14 @@
//
// Generic markers
//
/**
* Can be used for any type of point-likes to mark them as rotated to enlist
* the type checker to weed out subtle bugs due to rotated and non-rotated
* versions of the same data point.
*/
export type Rotated<T> = T & { _brand_rotated: "excalimath_rotated" };
// //
// Measurements // Measurements
// //
@@ -23,12 +34,6 @@ export type Degrees = number & { _brand: "excalimath_degree" };
*/ */
export type InclusiveRange = [number, number] & { _brand: "excalimath_degree" }; export type InclusiveRange = [number, number] & { _brand: "excalimath_degree" };
/**
* Can be used for any type of point-likes that are non-rotated, such as
* non-AABB Bounds, or non-rotated Point.
*/
export type NonRotated<T> = T & { _brand_nonrotated: "excalimath_nonrotated" };
// //
// Point // Point
// //
+21 -14
View File
@@ -1,4 +1,9 @@
import { arrayToMap, type Bounds } from "@excalidraw/common"; import {
arrayToMap,
bounds,
type RotatedBounds,
type Bounds,
} from "@excalidraw/common";
import { getElementBounds } from "@excalidraw/element"; import { getElementBounds } from "@excalidraw/element";
import { import {
isArrowElement, isArrowElement,
@@ -90,7 +95,7 @@ const getMinMaxPoints = (points: Points) => {
return ret; return ret;
}; };
const getRotatedBBox = (element: Element): Bounds => { const getRotatedBBox = (element: Element): RotatedBounds => {
const points = getElementRelativePoints(element); const points = getElementRelativePoints(element);
const { cx, cy } = getMinMaxPoints(points); const { cx, cy } = getMinMaxPoints(points);
@@ -101,12 +106,13 @@ const getRotatedBBox = (element: Element): Bounds => {
); );
const { minX, minY, maxX, maxY } = getMinMaxPoints(rotatedPoints); const { minX, minY, maxX, maxY } = getMinMaxPoints(rotatedPoints);
return [ return bounds(
minX + element.x, minX + element.x,
minY + element.y, minY + element.y,
maxX + element.x, maxX + element.x,
maxY + element.y, maxY + element.y,
]; element.angle,
);
}; };
export const isElementInsideBBox = ( export const isElementInsideBBox = (
@@ -160,12 +166,12 @@ export const elementPartiallyOverlapsWithOrContainsBBox = (
export const elementsOverlappingBBox = ({ export const elementsOverlappingBBox = ({
elements, elements,
bounds, bounds: _bounds,
type, type,
errorMargin = 0, errorMargin = 0,
}: { }: {
elements: Elements; elements: Elements;
bounds: Bounds | ExcalidrawElement; bounds: RotatedBounds | ExcalidrawElement;
/** safety offset. Defaults to 0. */ /** safety offset. Defaults to 0. */
errorMargin?: number; errorMargin?: number;
/** /**
@@ -175,15 +181,16 @@ export const elementsOverlappingBBox = ({
**/ **/
type: "overlap" | "contain" | "inside"; type: "overlap" | "contain" | "inside";
}) => { }) => {
if (isExcalidrawElement(bounds)) { if (isExcalidrawElement(_bounds)) {
bounds = getElementBounds(bounds, arrayToMap(elements)); _bounds = getElementBounds(_bounds, arrayToMap(elements));
} }
const adjustedBBox: Bounds = [ const adjustedBBox = bounds(
bounds[0] - errorMargin, _bounds[0] - errorMargin,
bounds[1] - errorMargin, _bounds[1] - errorMargin,
bounds[2] + errorMargin, _bounds[2] + errorMargin,
bounds[3] + errorMargin, _bounds[3] + errorMargin,
]; _bounds[4],
);
const includedElementSet = new Set<string>(); const includedElementSet = new Set<string>();