,
+ endPoint: P,
+) => {
+ const shape = ShapeCache.generateElementShape(element, null);
+ if (!shape) {
+ return null;
+ }
+
+ const ops = getCurvePathOps(shape[0]);
+ let currentP = point(0, 0);
+ let index = 0;
+ let minDistance = Infinity;
+ let controlPoints: P[] | null = null;
+
+ while (index < ops.length) {
+ const { op, data } = ops[index];
+ if (op === "move") {
+ invariant(
+ isPoint(data),
+ "The returned ops is not compatible with a point",
+ );
+ currentP = pointFromPair(data);
+ }
+ if (op === "bcurveTo") {
+ const p0 = currentP;
+ const p1 = point
(data[0], data[1]);
+ const p2 = point
(data[2], data[3]);
+ const p3 = point
(data[4], data[5]);
+ const distance = pointDistance(p3, endPoint);
+ if (distance < minDistance) {
+ minDistance = distance;
+ controlPoints = [p0, p1, p2, p3];
+ }
+ currentP = p3;
+ }
+ index++;
+ }
+
+ return controlPoints;
+};
+
+export const getBezierXY =
(
+ p0: P,
+ p1: P,
+ p2: P,
+ p3: P,
+ t: number,
+): P => {
+ const equation = (t: number, idx: number) =>
+ Math.pow(1 - t, 3) * p3[idx] +
+ 3 * t * Math.pow(1 - t, 2) * p2[idx] +
+ 3 * Math.pow(t, 2) * (1 - t) * p1[idx] +
+ p0[idx] * Math.pow(t, 3);
+ const tx = equation(t, 0);
+ const ty = equation(t, 1);
+ return point(tx, ty);
+};
+
+const getPointsInBezierCurve =
(
+ element: NonDeleted,
+ endPoint: P,
+) => {
+ const controlPoints: P[] = getControlPointsForBezierCurve(element, endPoint)!;
+ if (!controlPoints) {
+ return [];
+ }
+ const pointsOnCurve: P[] = [];
+ let t = 1;
+ // Take 20 points on curve for better accuracy
+ while (t > 0) {
+ const p = getBezierXY(
+ controlPoints[0],
+ controlPoints[1],
+ controlPoints[2],
+ controlPoints[3],
+ t,
+ );
+ pointsOnCurve.push(point(p[0], p[1]));
+ t -= 0.05;
+ }
+ if (pointsOnCurve.length) {
+ if (pointsEqual(pointsOnCurve.at(-1)!, endPoint)) {
+ pointsOnCurve.push(point(endPoint[0], endPoint[1]));
+ }
+ }
+ return pointsOnCurve;
+};
+
+const getBezierCurveArcLengths = (
+ element: NonDeleted,
+ endPoint: P,
+) => {
+ const arcLengths: number[] = [];
+ arcLengths[0] = 0;
+ const points = getPointsInBezierCurve(element, endPoint);
+ let index = 0;
+ let distance = 0;
+ while (index < points.length - 1) {
+ const segmentDistance = pointDistance(points[index], points[index + 1]);
+ distance += segmentDistance;
+ arcLengths.push(distance);
+ index++;
+ }
+
+ return arcLengths;
+};
+
+export const getBezierCurveLength = (
+ element: NonDeleted,
+ endPoint: P,
+) => {
+ const arcLengths = getBezierCurveArcLengths(element, endPoint);
+ return arcLengths.at(-1) as number;
+};
+
+// This maps interval to actual interval t on the curve so that when t = 0.5, its actually the point at 50% of the length
+export const mapIntervalToBezierT = (
+ element: NonDeleted,
+ endPoint: P,
+ interval: number, // The interval between 0 to 1 for which you want to find the point on the curve,
+) => {
+ const arcLengths = getBezierCurveArcLengths(element, endPoint);
+ const pointsCount = arcLengths.length - 1;
+ const curveLength = arcLengths.at(-1) as number;
+ const targetLength = interval * curveLength;
+ let low = 0;
+ let high = pointsCount;
+ let index = 0;
+ // Doing a binary search to find the largest length that is less than the target length
+ while (low < high) {
+ index = Math.floor(low + (high - low) / 2);
+ if (arcLengths[index] < targetLength) {
+ low = index + 1;
+ } else {
+ high = index;
+ }
+ }
+ if (arcLengths[index] > targetLength) {
+ index--;
+ }
+ if (arcLengths[index] === targetLength) {
+ return index / pointsCount;
+ }
+
+ return (
+ 1 -
+ (index +
+ (targetLength - arcLengths[index]) /
+ (arcLengths[index + 1] - arcLengths[index])) /
+ pointsCount
+ );
+};
+
+/**
+ * Get the axis-aligned bounding box for a given element
+ */
+export const aabbForElement = (
+ element: Readonly,
+ offset?: [number, number, number, number],
+) => {
+ const bbox = {
+ minX: element.x,
+ minY: element.y,
+ maxX: element.x + element.width,
+ maxY: element.y + element.height,
+ midX: element.x + element.width / 2,
+ midY: element.y + element.height / 2,
+ };
+
+ const center = point(bbox.midX, bbox.midY);
+ const [topLeftX, topLeftY] = pointRotateRads(
+ point(bbox.minX, bbox.minY),
+ center,
+ element.angle,
+ );
+ const [topRightX, topRightY] = pointRotateRads(
+ point(bbox.maxX, bbox.minY),
+ center,
+ element.angle,
+ );
+ const [bottomRightX, bottomRightY] = pointRotateRads(
+ point(bbox.maxX, bbox.maxY),
+ center,
+ element.angle,
+ );
+ const [bottomLeftX, bottomLeftY] = pointRotateRads(
+ point(bbox.minX, bbox.maxY),
+ center,
+ element.angle,
+ );
+
+ const bounds = [
+ Math.min(topLeftX, topRightX, bottomRightX, bottomLeftX),
+ Math.min(topLeftY, topRightY, bottomRightY, bottomLeftY),
+ Math.max(topLeftX, topRightX, bottomRightX, bottomLeftX),
+ Math.max(topLeftY, topRightY, bottomRightY, bottomLeftY),
+ ] as Bounds;
+
+ if (offset) {
+ const [topOffset, rightOffset, downOffset, leftOffset] = offset;
+ return [
+ bounds[0] - leftOffset,
+ bounds[1] - topOffset,
+ bounds[2] + rightOffset,
+ bounds[3] + downOffset,
+ ] as Bounds;
+ }
+
+ return bounds;
+};
+
+export const pointInsideBounds = (
+ p: P,
+ bounds: Bounds,
+): boolean =>
+ p[0] > bounds[0] && p[0] < bounds[2] && p[1] > bounds[1] && p[1] < bounds[3];
+
+export const aabbsOverlapping = (a: Bounds, b: Bounds) =>
+ pointInsideBounds(point(a[0], a[1]), b) ||
+ pointInsideBounds(point(a[2], a[1]), b) ||
+ pointInsideBounds(point(a[2], a[3]), b) ||
+ pointInsideBounds(point(a[0], a[3]), b) ||
+ pointInsideBounds(point(b[0], b[1]), a) ||
+ pointInsideBounds(point(b[2], b[1]), a) ||
+ pointInsideBounds(point(b[2], b[3]), a) ||
+ pointInsideBounds(point(b[0], b[3]), a);
+
+export const getCornerRadius = (x: number, element: ExcalidrawElement) => {
+ if (
+ element.roundness?.type === ROUNDNESS.PROPORTIONAL_RADIUS ||
+ element.roundness?.type === ROUNDNESS.LEGACY
+ ) {
+ return x * DEFAULT_PROPORTIONAL_RADIUS;
+ }
+
+ if (element.roundness?.type === ROUNDNESS.ADAPTIVE_RADIUS) {
+ const fixedRadiusSize = element.roundness?.value ?? DEFAULT_ADAPTIVE_RADIUS;
+
+ const CUTOFF_SIZE = fixedRadiusSize / DEFAULT_PROPORTIONAL_RADIUS;
+
+ if (x <= CUTOFF_SIZE) {
+ return x * DEFAULT_PROPORTIONAL_RADIUS;
+ }
+
+ return fixedRadiusSize;
+ }
+
+ return 0;
+};
+
+// Checks if the first and last point are close enough
+// to be considered a loop
+export const isPathALoop = (
+ points: ExcalidrawLinearElement["points"],
+ /** supply if you want the loop detection to account for current zoom */
+ zoomValue: Zoom["value"] = 1 as NormalizedZoomValue,
+): boolean => {
+ if (points.length >= 3) {
+ const [first, last] = [points[0], points[points.length - 1]];
+ const distance = pointDistance(first, last);
+
+ // Adjusting LINE_CONFIRM_THRESHOLD to current zoom so that when zoomed in
+ // really close we make the threshold smaller, and vice versa.
+ return distance <= LINE_CONFIRM_THRESHOLD / zoomValue;
+ }
+ return false;
+};
diff --git a/packages/excalidraw/snapping.ts b/packages/excalidraw/snapping.ts
index ee19c648b..9da3d74c4 100644
--- a/packages/excalidraw/snapping.ts
+++ b/packages/excalidraw/snapping.ts
@@ -1,3 +1,12 @@
+import type { InclusiveRange } from "../math";
+import {
+ point,
+ pointRotateRads,
+ rangeInclusive,
+ rangeIntersection,
+ rangesOverlap,
+ type GlobalPoint,
+} from "../math";
import { TOOL_TYPE } from "./constants";
import type { Bounds } from "./element/bounds";
import {
@@ -14,7 +23,6 @@ import type {
} from "./element/types";
import { getMaximumGroups } from "./groups";
import { KEYS } from "./keys";
-import { rangeIntersection, rangesOverlap, rotatePoint } from "./math";
import {
getSelectedElements,
getVisibleAndNonSelectedElements,
@@ -23,7 +31,7 @@ import type {
AppClassProperties,
AppState,
KeyboardModifiersObject,
- Point,
+ NullableGridSize,
} from "./types";
const SNAP_DISTANCE = 8;
@@ -42,7 +50,7 @@ type Vector2D = {
y: number;
};
-type PointPair = [Point, Point];
+type PointPair = [GlobalPoint, GlobalPoint];
export type PointSnap = {
type: "point";
@@ -62,9 +70,9 @@ export type Gap = {
// ↑ end side
startBounds: Bounds;
endBounds: Bounds;
- startSide: [Point, Point];
- endSide: [Point, Point];
- overlap: [number, number];
+ startSide: [GlobalPoint, GlobalPoint];
+ endSide: [GlobalPoint, GlobalPoint];
+ overlap: InclusiveRange;
length: number;
};
@@ -88,7 +96,7 @@ export type Snaps = Snap[];
export type PointSnapLine = {
type: "points";
- points: Point[];
+ points: GlobalPoint[];
};
export type PointerSnapLine = {
@@ -108,14 +116,14 @@ export type SnapLine = PointSnapLine | GapSnapLine | PointerSnapLine;
// -----------------------------------------------------------------------------
export class SnapCache {
- private static referenceSnapPoints: Point[] | null = null;
+ private static referenceSnapPoints: GlobalPoint[] | null = null;
private static visibleGaps: {
verticalGaps: Gap[];
horizontalGaps: Gap[];
} | null = null;
- public static setReferenceSnapPoints = (snapPoints: Point[] | null) => {
+ public static setReferenceSnapPoints = (snapPoints: GlobalPoint[] | null) => {
SnapCache.referenceSnapPoints = snapPoints;
};
@@ -191,8 +199,8 @@ export const getElementsCorners = (
omitCenter: false,
boundingBoxCorners: false,
},
-): Point[] => {
- let result: Point[] = [];
+): GlobalPoint[] => {
+ let result: GlobalPoint[] = [];
if (elements.length === 1) {
const element = elements[0];
@@ -219,33 +227,53 @@ export const getElementsCorners = (
(element.type === "diamond" || element.type === "ellipse") &&
!boundingBoxCorners
) {
- const leftMid = rotatePoint(
- [x1, y1 + halfHeight],
- [cx, cy],
+ const leftMid = pointRotateRads(
+ point(x1, y1 + halfHeight),
+ point(cx, cy),
element.angle,
);
- const topMid = rotatePoint([x1 + halfWidth, y1], [cx, cy], element.angle);
- const rightMid = rotatePoint(
- [x2, y1 + halfHeight],
- [cx, cy],
+ const topMid = pointRotateRads(
+ point(x1 + halfWidth, y1),
+ point(cx, cy),
element.angle,
);
- const bottomMid = rotatePoint(
- [x1 + halfWidth, y2],
- [cx, cy],
+ const rightMid = pointRotateRads(
+ point(x2, y1 + halfHeight),
+ point(cx, cy),
element.angle,
);
- const center: Point = [cx, cy];
+ const bottomMid = pointRotateRads(
+ point(x1 + halfWidth, y2),
+ point(cx, cy),
+ element.angle,
+ );
+ const center = point(cx, cy);
result = omitCenter
? [leftMid, topMid, rightMid, bottomMid]
: [leftMid, topMid, rightMid, bottomMid, center];
} else {
- const topLeft = rotatePoint([x1, y1], [cx, cy], element.angle);
- const topRight = rotatePoint([x2, y1], [cx, cy], element.angle);
- const bottomLeft = rotatePoint([x1, y2], [cx, cy], element.angle);
- const bottomRight = rotatePoint([x2, y2], [cx, cy], element.angle);
- const center: Point = [cx, cy];
+ const topLeft = pointRotateRads(
+ point(x1, y1),
+ point(cx, cy),
+ element.angle,
+ );
+ const topRight = pointRotateRads(
+ point(x2, y1),
+ point(cx, cy),
+ element.angle,
+ );
+ const bottomLeft = pointRotateRads(
+ point(x1, y2),
+ point(cx, cy),
+ element.angle,
+ );
+ const bottomRight = pointRotateRads(
+ point(x2, y2),
+ point(cx, cy),
+ element.angle,
+ );
+ const center = point(cx, cy);
result = omitCenter
? [topLeft, topRight, bottomLeft, bottomRight]
@@ -259,18 +287,18 @@ export const getElementsCorners = (
const width = maxX - minX;
const height = maxY - minY;
- const topLeft: Point = [minX, minY];
- const topRight: Point = [maxX, minY];
- const bottomLeft: Point = [minX, maxY];
- const bottomRight: Point = [maxX, maxY];
- const center: Point = [minX + width / 2, minY + height / 2];
+ const topLeft = point(minX, minY);
+ const topRight = point(maxX, minY);
+ const bottomLeft = point(minX, maxY);
+ const bottomRight = point(maxX, maxY);
+ const center = point(minX + width / 2, minY + height / 2);
result = omitCenter
? [topLeft, topRight, bottomLeft, bottomRight]
: [topLeft, topRight, bottomLeft, bottomRight, center];
}
- return result.map((point) => [round(point[0]), round(point[1])] as Point);
+ return result.map((p) => point(round(p[0]), round(p[1])));
};
const getReferenceElements = (
@@ -339,23 +367,20 @@ export const getVisibleGaps = (
if (
startMaxX < endMinX &&
- rangesOverlap([startMinY, startMaxY], [endMinY, endMaxY])
+ rangesOverlap(
+ rangeInclusive(startMinY, startMaxY),
+ rangeInclusive(endMinY, endMaxY),
+ )
) {
horizontalGaps.push({
startBounds,
endBounds,
- startSide: [
- [startMaxX, startMinY],
- [startMaxX, startMaxY],
- ],
- endSide: [
- [endMinX, endMinY],
- [endMinX, endMaxY],
- ],
+ startSide: [point(startMaxX, startMinY), point(startMaxX, startMaxY)],
+ endSide: [point(endMinX, endMinY), point(endMinX, endMaxY)],
length: endMinX - startMaxX,
overlap: rangeIntersection(
- [startMinY, startMaxY],
- [endMinY, endMaxY],
+ rangeInclusive(startMinY, startMaxY),
+ rangeInclusive(endMinY, endMaxY),
)!,
});
}
@@ -382,23 +407,20 @@ export const getVisibleGaps = (
if (
startMaxY < endMinY &&
- rangesOverlap([startMinX, startMaxX], [endMinX, endMaxX])
+ rangesOverlap(
+ rangeInclusive(startMinX, startMaxX),
+ rangeInclusive(endMinX, endMaxX),
+ )
) {
verticalGaps.push({
startBounds,
endBounds,
- startSide: [
- [startMinX, startMaxY],
- [startMaxX, startMaxY],
- ],
- endSide: [
- [endMinX, endMinY],
- [endMaxX, endMinY],
- ],
+ startSide: [point(startMinX, startMaxY), point(startMaxX, startMaxY)],
+ endSide: [point(endMinX, endMinY), point(endMaxX, endMinY)],
length: endMinY - startMaxY,
overlap: rangeIntersection(
- [startMinX, startMaxX],
- [endMinX, endMaxX],
+ rangeInclusive(startMinX, startMaxX),
+ rangeInclusive(endMinX, endMaxX),
)!,
});
}
@@ -441,7 +463,7 @@ const getGapSnaps = (
const centerY = (minY + maxY) / 2;
for (const gap of horizontalGaps) {
- if (!rangesOverlap([minY, maxY], gap.overlap)) {
+ if (!rangesOverlap(rangeInclusive(minY, maxY), gap.overlap)) {
continue;
}
@@ -510,7 +532,7 @@ const getGapSnaps = (
}
}
for (const gap of verticalGaps) {
- if (!rangesOverlap([minX, maxX], gap.overlap)) {
+ if (!rangesOverlap(rangeInclusive(minX, maxX), gap.overlap)) {
continue;
}
@@ -603,7 +625,7 @@ export const getReferenceSnapPoints = (
const getPointSnaps = (
selectedElements: ExcalidrawElement[],
- selectionSnapPoints: Point[],
+ selectionSnapPoints: GlobalPoint[],
app: AppClassProperties,
event: KeyboardModifiersObject,
nearestSnapsX: Snaps,
@@ -779,8 +801,8 @@ const round = (x: number) => {
return Math.round(x * 10 ** decimalPlaces) / 10 ** decimalPlaces;
};
-const dedupePoints = (points: Point[]): Point[] => {
- const map = new Map();
+const dedupePoints = (points: GlobalPoint[]): GlobalPoint[] => {
+ const map = new Map();
for (const point of points) {
const key = point.join(",");
@@ -797,8 +819,8 @@ const createPointSnapLines = (
nearestSnapsX: Snaps,
nearestSnapsY: Snaps,
): PointSnapLine[] => {
- const snapsX = {} as { [key: string]: Point[] };
- const snapsY = {} as { [key: string]: Point[] };
+ const snapsX = {} as { [key: string]: GlobalPoint[] };
+ const snapsY = {} as { [key: string]: GlobalPoint[] };
if (nearestSnapsX.length > 0) {
for (const snap of nearestSnapsX) {
@@ -809,8 +831,8 @@ const createPointSnapLines = (
snapsX[key] = [];
}
snapsX[key].push(
- ...snap.points.map(
- (point) => [round(point[0]), round(point[1])] as Point,
+ ...snap.points.map((p) =>
+ point(round(p[0]), round(p[1])),
),
);
}
@@ -826,8 +848,8 @@ const createPointSnapLines = (
snapsY[key] = [];
}
snapsY[key].push(
- ...snap.points.map(
- (point) => [round(point[0]), round(point[1])] as Point,
+ ...snap.points.map((p) =>
+ point(round(p[0]), round(p[1])),
),
);
}
@@ -840,8 +862,8 @@ const createPointSnapLines = (
type: "points",
points: dedupePoints(
points
- .map((point) => {
- return [Number(key), point[1]] as Point;
+ .map((p) => {
+ return point(Number(key), p[1]);
})
.sort((a, b) => a[1] - b[1]),
),
@@ -853,8 +875,8 @@ const createPointSnapLines = (
type: "points",
points: dedupePoints(
points
- .map((point) => {
- return [point[0], Number(key)] as Point;
+ .map((p) => {
+ return point(p[0], Number(key));
})
.sort((a, b) => a[0] - b[0]),
),
@@ -898,12 +920,12 @@ const createGapSnapLines = (
const [endMinX, endMinY, endMaxX, endMaxY] = gapSnap.gap.endBounds;
const verticalIntersection = rangeIntersection(
- [minY, maxY],
+ rangeInclusive(minY, maxY),
gapSnap.gap.overlap,
);
const horizontalGapIntersection = rangeIntersection(
- [minX, maxX],
+ rangeInclusive(minX, maxX),
gapSnap.gap.overlap,
);
@@ -918,16 +940,16 @@ const createGapSnapLines = (
type: "gap",
direction: "horizontal",
points: [
- [gapSnap.gap.startSide[0][0], gapLineY],
- [minX, gapLineY],
+ point(gapSnap.gap.startSide[0][0], gapLineY),
+ point(minX, gapLineY),
],
},
{
type: "gap",
direction: "horizontal",
points: [
- [maxX, gapLineY],
- [gapSnap.gap.endSide[0][0], gapLineY],
+ point(maxX, gapLineY),
+ point(gapSnap.gap.endSide[0][0], gapLineY),
],
},
);
@@ -944,16 +966,16 @@ const createGapSnapLines = (
type: "gap",
direction: "vertical",
points: [
- [gapLineX, gapSnap.gap.startSide[0][1]],
- [gapLineX, minY],
+ point(gapLineX, gapSnap.gap.startSide[0][1]),
+ point(gapLineX, minY),
],
},
{
type: "gap",
direction: "vertical",
points: [
- [gapLineX, maxY],
- [gapLineX, gapSnap.gap.endSide[0][1]],
+ point(gapLineX, maxY),
+ point(gapLineX, gapSnap.gap.endSide[0][1]),
],
},
);
@@ -969,18 +991,12 @@ const createGapSnapLines = (
{
type: "gap",
direction: "horizontal",
- points: [
- [startMaxX, gapLineY],
- [endMinX, gapLineY],
- ],
+ points: [point(startMaxX, gapLineY), point(endMinX, gapLineY)],
},
{
type: "gap",
direction: "horizontal",
- points: [
- [endMaxX, gapLineY],
- [minX, gapLineY],
- ],
+ points: [point(endMaxX, gapLineY), point(minX, gapLineY)],
},
);
}
@@ -995,18 +1011,12 @@ const createGapSnapLines = (
{
type: "gap",
direction: "horizontal",
- points: [
- [maxX, gapLineY],
- [startMinX, gapLineY],
- ],
+ points: [point(maxX, gapLineY), point(startMinX, gapLineY)],
},
{
type: "gap",
direction: "horizontal",
- points: [
- [startMaxX, gapLineY],
- [endMinX, gapLineY],
- ],
+ points: [point(startMaxX, gapLineY), point(endMinX, gapLineY)],
},
);
}
@@ -1021,18 +1031,12 @@ const createGapSnapLines = (
{
type: "gap",
direction: "vertical",
- points: [
- [gapLineX, maxY],
- [gapLineX, startMinY],
- ],
+ points: [point(gapLineX, maxY), point(gapLineX, startMinY)],
},
{
type: "gap",
direction: "vertical",
- points: [
- [gapLineX, startMaxY],
- [gapLineX, endMinY],
- ],
+ points: [point(gapLineX, startMaxY), point(gapLineX, endMinY)],
},
);
}
@@ -1047,18 +1051,12 @@ const createGapSnapLines = (
{
type: "gap",
direction: "vertical",
- points: [
- [gapLineX, startMaxY],
- [gapLineX, endMinY],
- ],
+ points: [point(gapLineX, startMaxY), point(gapLineX, endMinY)],
},
{
type: "gap",
direction: "vertical",
- points: [
- [gapLineX, endMaxY],
- [gapLineX, minY],
- ],
+ points: [point(gapLineX, endMaxY), point(gapLineX, minY)],
},
);
}
@@ -1071,8 +1069,8 @@ const createGapSnapLines = (
gapSnapLines.map((gapSnapLine) => {
return {
...gapSnapLine,
- points: gapSnapLine.points.map(
- (point) => [round(point[0]), round(point[1])] as Point,
+ points: gapSnapLine.points.map((p) =>
+ point(round(p[0]), round(p[1])),
) as PointPair,
};
}),
@@ -1117,40 +1115,40 @@ export const snapResizingElements = (
}
}
- const selectionSnapPoints: Point[] = [];
+ const selectionSnapPoints: GlobalPoint[] = [];
if (transformHandle) {
switch (transformHandle) {
case "e": {
- selectionSnapPoints.push([maxX, minY], [maxX, maxY]);
+ selectionSnapPoints.push(point(maxX, minY), point(maxX, maxY));
break;
}
case "w": {
- selectionSnapPoints.push([minX, minY], [minX, maxY]);
+ selectionSnapPoints.push(point(minX, minY), point(minX, maxY));
break;
}
case "n": {
- selectionSnapPoints.push([minX, minY], [maxX, minY]);
+ selectionSnapPoints.push(point(minX, minY), point(maxX, minY));
break;
}
case "s": {
- selectionSnapPoints.push([minX, maxY], [maxX, maxY]);
+ selectionSnapPoints.push(point(minX, maxY), point(maxX, maxY));
break;
}
case "ne": {
- selectionSnapPoints.push([maxX, minY]);
+ selectionSnapPoints.push(point(maxX, minY));
break;
}
case "nw": {
- selectionSnapPoints.push([minX, minY]);
+ selectionSnapPoints.push(point(minX, minY));
break;
}
case "se": {
- selectionSnapPoints.push([maxX, maxY]);
+ selectionSnapPoints.push(point(maxX, maxY));
break;
}
case "sw": {
- selectionSnapPoints.push([minX, maxY]);
+ selectionSnapPoints.push(point(minX, maxY));
break;
}
}
@@ -1192,11 +1190,11 @@ export const snapResizingElements = (
round(bound),
);
- const corners: Point[] = [
- [x1, y1],
- [x1, y2],
- [x2, y1],
- [x2, y2],
+ const corners: GlobalPoint[] = [
+ point(x1, y1),
+ point(x1, y2),
+ point(x2, y1),
+ point(x2, y2),
];
getPointSnaps(
@@ -1232,8 +1230,8 @@ export const snapNewElement = (
};
}
- const selectionSnapPoints: Point[] = [
- [origin.x + dragOffset.x, origin.y + dragOffset.y],
+ const selectionSnapPoints: GlobalPoint[] = [
+ point(origin.x + dragOffset.x, origin.y + dragOffset.y),
];
const snapDistance = getSnapDistance(app.state.zoom.value);
@@ -1333,7 +1331,7 @@ export const getSnapLinesAtPointer = (
verticalSnapLines.push({
type: "pointer",
- points: [corner, [corner[0], pointer.y]],
+ points: [corner, point(corner[0], pointer.y)],
direction: "vertical",
});
@@ -1349,7 +1347,7 @@ export const getSnapLinesAtPointer = (
horizontalSnapLines.push({
type: "pointer",
- points: [corner, [pointer.x, corner[1]]],
+ points: [corner, point(pointer.x, corner[1])],
direction: "horizontal",
});
@@ -1386,3 +1384,18 @@ export const isActiveToolNonLinearSnappable = (
activeToolType === TOOL_TYPE.text
);
};
+
+// TODO: Rounding this point causes some shake when free drawing
+export const getGridPoint = (
+ x: number,
+ y: number,
+ gridSize: NullableGridSize,
+): [number, number] => {
+ if (gridSize) {
+ return [
+ Math.round(x / gridSize) * gridSize,
+ Math.round(y / gridSize) * gridSize,
+ ];
+ }
+ return [x, y];
+};
diff --git a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap
index 7f9904a4d..3a5e14065 100644
--- a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap
+++ b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap
@@ -866,6 +866,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id0": true,
"id1": true,
@@ -1068,6 +1069,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id0": true,
},
@@ -1283,6 +1285,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id0": true,
},
@@ -1613,6 +1616,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id0": true,
},
@@ -1943,6 +1947,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id0": true,
},
@@ -2158,6 +2163,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {},
"selectedElementsAreBeingDragged": false,
"selectedGroupIds": {},
@@ -2397,6 +2403,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id0_copy": true,
},
@@ -2699,6 +2706,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id0": true,
"id1": true,
@@ -3065,6 +3073,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id0": true,
},
@@ -3539,6 +3548,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id1": true,
},
@@ -3861,6 +3871,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id1": true,
},
@@ -4185,6 +4196,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id0": true,
"id1": true,
@@ -5370,6 +5382,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id0": true,
"id1": true,
@@ -6496,6 +6509,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id0": true,
"id1": true,
@@ -7431,6 +7445,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {},
"selectedElementsAreBeingDragged": false,
"selectedGroupIds": {},
@@ -8339,6 +8354,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id0": true,
},
@@ -9235,6 +9251,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
+ "searchMatches": [],
"selectedElementIds": {
"id1": true,
},
diff --git a/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap
index 2994cfc3e..e5e431dfc 100644
--- a/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap
+++ b/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap
@@ -239,6 +239,55 @@ exports[` > Test UIOptions prop > Test canvasActions > should rende
Ctrl+Shift+E