chore: Element center point util (#9298)
All checks were successful
Tests / test (push) Successful in 5m4s

This commit is contained in:
jhanma17dev 2025-04-09 10:04:51 -05:00 committed by GitHub
parent 6fc85022ae
commit dff69e9191
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 49 additions and 78 deletions

View file

@ -1,9 +1,10 @@
import { average } from "@excalidraw/math"; import { average, pointFrom, type GlobalPoint } from "@excalidraw/math";
import type { import type {
ExcalidrawBindableElement, ExcalidrawBindableElement,
FontFamilyValues, FontFamilyValues,
FontString, FontString,
ExcalidrawElement,
} from "@excalidraw/element/types"; } from "@excalidraw/element/types";
import type { import type {
@ -1201,3 +1202,17 @@ export const escapeDoubleQuotes = (str: string) => {
export const castArray = <T>(value: T | T[]): T[] => export const castArray = <T>(value: T | T[]): T[] =>
Array.isArray(value) ? value : [value]; Array.isArray(value) ? value : [value];
export const elementCenterPoint = (
element: ExcalidrawElement,
xOffset: number = 0,
yOffset: number = 0,
) => {
const { x, y, width, height } = element;
const centerXPoint = x + width / 2 + xOffset;
const centerYPoint = y + height / 2 + yOffset;
return pointFrom<GlobalPoint>(centerXPoint, centerYPoint);
};

View file

@ -6,6 +6,7 @@ import {
invariant, invariant,
isDevEnv, isDevEnv,
isTestEnv, isTestEnv,
elementCenterPoint,
} from "@excalidraw/common"; } from "@excalidraw/common";
import { import {
@ -904,13 +905,7 @@ export const getHeadingForElbowArrowSnap = (
if (!distance) { if (!distance) {
return vectorToHeading( return vectorToHeading(
vectorFromPoint( vectorFromPoint(p, elementCenterPoint(bindableElement)),
p,
pointFrom<GlobalPoint>(
bindableElement.x + bindableElement.width / 2,
bindableElement.y + bindableElement.height / 2,
),
),
); );
} }
@ -1040,10 +1035,7 @@ export const avoidRectangularCorner = (
element: ExcalidrawBindableElement, element: ExcalidrawBindableElement,
p: GlobalPoint, p: GlobalPoint,
): GlobalPoint => { ): GlobalPoint => {
const center = pointFrom<GlobalPoint>( const center = elementCenterPoint(element);
element.x + element.width / 2,
element.y + element.height / 2,
);
const nonRotatedPoint = pointRotateRads(p, center, -element.angle as Radians); const nonRotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) { if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) {
@ -1140,10 +1132,9 @@ export const snapToMid = (
tolerance: number = 0.05, tolerance: number = 0.05,
): GlobalPoint => { ): GlobalPoint => {
const { x, y, width, height, angle } = element; const { x, y, width, height, angle } = element;
const center = pointFrom<GlobalPoint>(
x + width / 2 - 0.1, const center = elementCenterPoint(element, -0.1, -0.1);
y + height / 2 - 0.1,
);
const nonRotated = pointRotateRads(p, center, -angle as Radians); const nonRotated = pointRotateRads(p, center, -angle as Radians);
// snap-to-center point is adaptive to element size, but we don't want to go // snap-to-center point is adaptive to element size, but we don't want to go
@ -1228,10 +1219,7 @@ const updateBoundPoint = (
startOrEnd === "startBinding" ? "start" : "end", startOrEnd === "startBinding" ? "start" : "end",
elementsMap, elementsMap,
).fixedPoint; ).fixedPoint;
const globalMidPoint = pointFrom<GlobalPoint>( const globalMidPoint = elementCenterPoint(bindableElement);
bindableElement.x + bindableElement.width / 2,
bindableElement.y + bindableElement.height / 2,
);
const global = pointFrom<GlobalPoint>( const global = pointFrom<GlobalPoint>(
bindableElement.x + fixedPoint[0] * bindableElement.width, bindableElement.x + fixedPoint[0] * bindableElement.width,
bindableElement.y + fixedPoint[1] * bindableElement.height, bindableElement.y + fixedPoint[1] * bindableElement.height,
@ -1275,10 +1263,7 @@ const updateBoundPoint = (
elementsMap, elementsMap,
); );
const center = pointFrom<GlobalPoint>( const center = elementCenterPoint(bindableElement);
bindableElement.x + bindableElement.width / 2,
bindableElement.y + bindableElement.height / 2,
);
const interceptorLength = const interceptorLength =
pointDistance(adjacentPoint, edgePointAbsolute) + pointDistance(adjacentPoint, edgePointAbsolute) +
pointDistance(adjacentPoint, center) + pointDistance(adjacentPoint, center) +
@ -1771,10 +1756,7 @@ const determineFocusDistance = (
// Another point on the line, in absolute coordinates (closer to element) // Another point on the line, in absolute coordinates (closer to element)
b: GlobalPoint, b: GlobalPoint,
): number => { ): number => {
const center = pointFrom<GlobalPoint>( const center = elementCenterPoint(element);
element.x + element.width / 2,
element.y + element.height / 2,
);
if (pointsEqual(a, b)) { if (pointsEqual(a, b)) {
return 0; return 0;
@ -1904,10 +1886,7 @@ const determineFocusPoint = (
focus: number, focus: number,
adjacentPoint: GlobalPoint, adjacentPoint: GlobalPoint,
): GlobalPoint => { ): GlobalPoint => {
const center = pointFrom<GlobalPoint>( const center = elementCenterPoint(element);
element.x + element.width / 2,
element.y + element.height / 2,
);
if (focus === 0) { if (focus === 0) {
return center; return center;
@ -2338,10 +2317,7 @@ export const getGlobalFixedPointForBindableElement = (
element.x + element.width * fixedX, element.x + element.width * fixedX,
element.y + element.height * fixedY, element.y + element.height * fixedY,
), ),
pointFrom<GlobalPoint>( elementCenterPoint(element),
element.x + element.width / 2,
element.y + element.height / 2,
),
element.angle, element.angle,
); );
}; };

View file

@ -1,4 +1,4 @@
import { isTransparent } from "@excalidraw/common"; import { isTransparent, elementCenterPoint } from "@excalidraw/common";
import { import {
curveIntersectLineSegment, curveIntersectLineSegment,
isPointWithinBounds, isPointWithinBounds,
@ -16,7 +16,7 @@ import {
} from "@excalidraw/math/ellipse"; } from "@excalidraw/math/ellipse";
import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision"; import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision";
import { getPolygonShape } from "@excalidraw/utils/shape"; import { type GeometricShape, getPolygonShape } from "@excalidraw/utils/shape";
import type { import type {
GlobalPoint, GlobalPoint,
@ -26,8 +26,6 @@ import type {
Radians, Radians,
} from "@excalidraw/math"; } from "@excalidraw/math";
import type { GeometricShape } from "@excalidraw/utils/shape";
import type { FrameNameBounds } from "@excalidraw/excalidraw/types"; import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
import { getBoundTextShape, isPathALoop } from "./shapes"; import { getBoundTextShape, isPathALoop } from "./shapes";
@ -191,10 +189,7 @@ const intersectRectanguloidWithLineSegment = (
l: LineSegment<GlobalPoint>, l: LineSegment<GlobalPoint>,
offset: number = 0, offset: number = 0,
): GlobalPoint[] => { ): GlobalPoint[] => {
const center = pointFrom<GlobalPoint>( const center = elementCenterPoint(element);
element.x + element.width / 2,
element.y + element.height / 2,
);
// To emulate a rotated rectangle we rotate the point in the inverse angle // To emulate a rotated rectangle we rotate the point in the inverse angle
// instead. It's all the same distance-wise. // instead. It's all the same distance-wise.
const rotatedA = pointRotateRads<GlobalPoint>( const rotatedA = pointRotateRads<GlobalPoint>(
@ -253,10 +248,7 @@ const intersectDiamondWithLineSegment = (
l: LineSegment<GlobalPoint>, l: LineSegment<GlobalPoint>,
offset: number = 0, offset: number = 0,
): GlobalPoint[] => { ): GlobalPoint[] => {
const center = pointFrom<GlobalPoint>( const center = elementCenterPoint(element);
element.x + element.width / 2,
element.y + element.height / 2,
);
// Rotate the point to the inverse direction to simulate the rotated diamond // Rotate the point to the inverse direction to simulate the rotated diamond
// points. It's all the same distance-wise. // points. It's all the same distance-wise.
@ -304,10 +296,7 @@ const intersectEllipseWithLineSegment = (
l: LineSegment<GlobalPoint>, l: LineSegment<GlobalPoint>,
offset: number = 0, offset: number = 0,
): GlobalPoint[] => { ): GlobalPoint[] => {
const center = pointFrom<GlobalPoint>( const center = elementCenterPoint(element);
element.x + element.width / 2,
element.y + element.height / 2,
);
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians); const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians); const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);

View file

@ -14,6 +14,8 @@ import {
} from "@excalidraw/math"; } from "@excalidraw/math";
import { type Point } from "points-on-curve"; import { type Point } from "points-on-curve";
import { elementCenterPoint } from "@excalidraw/common";
import { import {
getElementAbsoluteCoords, getElementAbsoluteCoords,
getResizedElementAbsoluteCoords, getResizedElementAbsoluteCoords,
@ -61,7 +63,7 @@ export const cropElement = (
const rotatedPointer = pointRotateRads( const rotatedPointer = pointRotateRads(
pointFrom(pointerX, pointerY), pointFrom(pointerX, pointerY),
pointFrom(element.x + element.width / 2, element.y + element.height / 2), elementCenterPoint(element),
-element.angle as Radians, -element.angle as Radians,
); );

View file

@ -1,12 +1,13 @@
import { import {
curvePointDistance, curvePointDistance,
distanceToLineSegment, distanceToLineSegment,
pointFrom,
pointRotateRads, pointRotateRads,
} from "@excalidraw/math"; } from "@excalidraw/math";
import { ellipse, ellipseDistanceFromPoint } from "@excalidraw/math/ellipse"; import { ellipse, ellipseDistanceFromPoint } from "@excalidraw/math/ellipse";
import { elementCenterPoint } from "@excalidraw/common";
import type { GlobalPoint, Radians } from "@excalidraw/math"; import type { GlobalPoint, Radians } from "@excalidraw/math";
import { import {
@ -53,10 +54,7 @@ const distanceToRectanguloidElement = (
element: ExcalidrawRectanguloidElement, element: ExcalidrawRectanguloidElement,
p: GlobalPoint, p: GlobalPoint,
) => { ) => {
const center = pointFrom<GlobalPoint>( const center = elementCenterPoint(element);
element.x + element.width / 2,
element.y + element.height / 2,
);
// To emulate a rotated rectangle we rotate the point in the inverse angle // To emulate a rotated rectangle we rotate the point in the inverse angle
// instead. It's all the same distance-wise. // instead. It's all the same distance-wise.
const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians); const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
@ -84,10 +82,7 @@ const distanceToDiamondElement = (
element: ExcalidrawDiamondElement, element: ExcalidrawDiamondElement,
p: GlobalPoint, p: GlobalPoint,
): number => { ): number => {
const center = pointFrom<GlobalPoint>( const center = elementCenterPoint(element);
element.x + element.width / 2,
element.y + element.height / 2,
);
// Rotate the point to the inverse direction to simulate the rotated diamond // Rotate the point to the inverse direction to simulate the rotated diamond
// points. It's all the same distance-wise. // points. It's all the same distance-wise.
@ -115,10 +110,7 @@ const distanceToEllipseElement = (
element: ExcalidrawEllipseElement, element: ExcalidrawEllipseElement,
p: GlobalPoint, p: GlobalPoint,
): number => { ): number => {
const center = pointFrom( const center = elementCenterPoint(element);
element.x + element.width / 2,
element.y + element.height / 2,
);
return ellipseDistanceFromPoint( return ellipseDistanceFromPoint(
// Instead of rotating the ellipse, rotate the point to the inverse angle // Instead of rotating the ellipse, rotate the point to the inverse angle
pointRotateRads(p, center, -element.angle as Radians), pointRotateRads(p, center, -element.angle as Radians),

View file

@ -4,6 +4,7 @@ import {
LINE_CONFIRM_THRESHOLD, LINE_CONFIRM_THRESHOLD,
ROUNDNESS, ROUNDNESS,
invariant, invariant,
elementCenterPoint,
} from "@excalidraw/common"; } from "@excalidraw/common";
import { import {
isPoint, isPoint,
@ -297,7 +298,7 @@ export const aabbForElement = (
midY: element.y + element.height / 2, midY: element.y + element.height / 2,
}; };
const center = pointFrom(bbox.midX, bbox.midY); const center = elementCenterPoint(element);
const [topLeftX, topLeftY] = pointRotateRads( const [topLeftX, topLeftY] = pointRotateRads(
pointFrom(bbox.minX, bbox.minY), pointFrom(bbox.minX, bbox.minY),
center, center,

View file

@ -10,6 +10,8 @@ import {
type GlobalPoint, type GlobalPoint,
} from "@excalidraw/math"; } from "@excalidraw/math";
import { elementCenterPoint } from "@excalidraw/common";
import type { Curve, LineSegment } from "@excalidraw/math"; import type { Curve, LineSegment } from "@excalidraw/math";
import { getCornerRadius } from "./shapes"; import { getCornerRadius } from "./shapes";
@ -68,10 +70,7 @@ export function deconstructRectanguloidElement(
return [sides, []]; return [sides, []];
} }
const center = pointFrom<GlobalPoint>( const center = elementCenterPoint(element);
element.x + element.width / 2,
element.y + element.height / 2,
);
const r = rectangle( const r = rectangle(
pointFrom(element.x, element.y), pointFrom(element.x, element.y),
@ -254,10 +253,7 @@ export function deconstructDiamondElement(
return [[topRight, bottomRight, bottomLeft, topLeft], []]; return [[topRight, bottomRight, bottomLeft, topLeft], []];
} }
const center = pointFrom<GlobalPoint>( const center = elementCenterPoint(element);
element.x + element.width / 2,
element.y + element.height / 2,
);
const [top, right, bottom, left]: GlobalPoint[] = [ const [top, right, bottom, left]: GlobalPoint[] = [
pointFrom(element.x + topX, element.y + topY), pointFrom(element.x + topX, element.y + topY),

View file

@ -20,7 +20,7 @@ import {
isTextElement, isTextElement,
isFrameLikeElement, isFrameLikeElement,
} from "@excalidraw/element/typeChecks"; } from "@excalidraw/element/typeChecks";
import { KEYS, arrayToMap } from "@excalidraw/common"; import { KEYS, arrayToMap, elementCenterPoint } from "@excalidraw/common";
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math"; import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
@ -151,7 +151,7 @@ export class Keyboard {
const getElementPointForSelection = ( const getElementPointForSelection = (
element: ExcalidrawElement, element: ExcalidrawElement,
): GlobalPoint => { ): GlobalPoint => {
const { x, y, width, height, angle } = element; const { x, y, width, angle } = element;
const target = pointFrom<GlobalPoint>( const target = pointFrom<GlobalPoint>(
x + x +
(isLinearElement(element) || isFreeDrawElement(element) ? 0 : width / 2), (isLinearElement(element) || isFreeDrawElement(element) ? 0 : width / 2),
@ -166,7 +166,7 @@ const getElementPointForSelection = (
(bounds[1] + bounds[3]) / 2, (bounds[1] + bounds[3]) / 2,
); );
} else { } else {
center = pointFrom(x + width / 2, y + height / 2); center = elementCenterPoint(element);
} }
if (isTextElement(element)) { if (isTextElement(element)) {