mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Bounds refactor and duplication removal
This commit is contained in:
parent
7b4e989d65
commit
91b6057d9c
28 changed files with 431 additions and 147 deletions
|
@ -10,7 +10,7 @@ import {
|
||||||
import { serializeAsJSON } from "../../packages/excalidraw/data/json";
|
import { serializeAsJSON } from "../../packages/excalidraw/data/json";
|
||||||
import { restore } from "../../packages/excalidraw/data/restore";
|
import { restore } from "../../packages/excalidraw/data/restore";
|
||||||
import type { ImportedDataState } from "../../packages/excalidraw/data/types";
|
import type { ImportedDataState } from "../../packages/excalidraw/data/types";
|
||||||
import type { SceneBounds } from "../../packages/excalidraw/element/bounds";
|
import type { ViewportBounds } from "../../packages/excalidraw/element/bounds";
|
||||||
import { isInvisiblySmallElement } from "../../packages/excalidraw/element/sizeHelpers";
|
import { isInvisiblySmallElement } from "../../packages/excalidraw/element/sizeHelpers";
|
||||||
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
|
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
|
||||||
import type {
|
import type {
|
||||||
|
@ -104,7 +104,7 @@ export type SocketUpdateDataSource = {
|
||||||
payload: {
|
payload: {
|
||||||
socketId: SocketId;
|
socketId: SocketId;
|
||||||
username: string;
|
username: string;
|
||||||
sceneBounds: SceneBounds;
|
sceneBounds: ViewportBounds;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
IDLE_STATUS: {
|
IDLE_STATUS: {
|
||||||
|
|
|
@ -35,7 +35,7 @@ import {
|
||||||
isHandToolActive,
|
isHandToolActive,
|
||||||
} from "../appState";
|
} from "../appState";
|
||||||
import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors";
|
import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors";
|
||||||
import type { SceneBounds } from "../element/bounds";
|
import type { ViewportBounds } from "../element/bounds";
|
||||||
import { setCursor } from "../cursor";
|
import { setCursor } from "../cursor";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
import { clamp, point, roundToStep } from "../../math";
|
import { clamp, point, roundToStep } from "../../math";
|
||||||
|
@ -245,7 +245,7 @@ export const actionResetZoom = register({
|
||||||
});
|
});
|
||||||
|
|
||||||
const zoomValueToFitBoundsOnViewport = (
|
const zoomValueToFitBoundsOnViewport = (
|
||||||
bounds: SceneBounds,
|
bounds: ViewportBounds,
|
||||||
viewportDimensions: { width: number; height: number },
|
viewportDimensions: { width: number; height: number },
|
||||||
viewportZoomFactor: number = 1, // default to 1 if not provided
|
viewportZoomFactor: number = 1, // default to 1 if not provided
|
||||||
) => {
|
) => {
|
||||||
|
@ -271,7 +271,7 @@ export const zoomToFitBounds = ({
|
||||||
minZoom = -Infinity,
|
minZoom = -Infinity,
|
||||||
maxZoom = Infinity,
|
maxZoom = Infinity,
|
||||||
}: {
|
}: {
|
||||||
bounds: SceneBounds;
|
bounds: ViewportBounds;
|
||||||
canvasOffsets?: Offsets;
|
canvasOffsets?: Offsets;
|
||||||
appState: Readonly<AppState>;
|
appState: Readonly<AppState>;
|
||||||
/** whether to fit content to viewport (beyond >100%) */
|
/** whether to fit content to viewport (beyond >100%) */
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { ElementsMap, ExcalidrawElement } from "./element/types";
|
import type { Bounds, ElementsMap, ExcalidrawElement } from "./element/types";
|
||||||
import { newElementWith } from "./element/mutateElement";
|
import { newElementWith } from "./element/mutateElement";
|
||||||
import { getCommonBounds, type Bounds } from "./element/bounds";
|
import { getCommonBounds } from "./element/bounds";
|
||||||
import { getMaximumGroups } from "./groups";
|
import { getMaximumGroups } from "./groups";
|
||||||
|
|
||||||
export interface Alignment {
|
export interface Alignment {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import type { GlobalPoint, Radians } from "../../../math";
|
import type { GlobalPoint, Radians } from "../../../math";
|
||||||
import { point, pointRotateRads } from "../../../math";
|
import { point, pointRotateRads } from "../../../math";
|
||||||
import { MIME_TYPES } from "../../constants";
|
import { MIME_TYPES } from "../../constants";
|
||||||
import type { Bounds } from "../../element/bounds";
|
|
||||||
import { getElementAbsoluteCoords } from "../../element/bounds";
|
import { getElementAbsoluteCoords } from "../../element/bounds";
|
||||||
import { hitElementBoundingBox } from "../../element/collision";
|
import { hitElementBoundingBox } from "../../element/collision";
|
||||||
import type {
|
import type {
|
||||||
|
Bounds,
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "../../element/types";
|
} from "../../element/types";
|
||||||
|
|
|
@ -1,5 +1,236 @@
|
||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`Test Transform > Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 1`] = `
|
||||||
|
{
|
||||||
|
"angle": 0,
|
||||||
|
"backgroundColor": "#d8f5a2",
|
||||||
|
"boundElements": [
|
||||||
|
{
|
||||||
|
"id": "id45",
|
||||||
|
"type": "arrow",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "id46",
|
||||||
|
"type": "arrow",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"customData": undefined,
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"frameId": null,
|
||||||
|
"groupIds": [],
|
||||||
|
"height": 300,
|
||||||
|
"id": Any<String>,
|
||||||
|
"index": "a0",
|
||||||
|
"isDeleted": false,
|
||||||
|
"link": null,
|
||||||
|
"locked": false,
|
||||||
|
"opacity": 100,
|
||||||
|
"roughness": 1,
|
||||||
|
"roundness": null,
|
||||||
|
"seed": Any<Number>,
|
||||||
|
"strokeColor": "#66a80f",
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"type": "ellipse",
|
||||||
|
"updated": 1,
|
||||||
|
"version": 4,
|
||||||
|
"versionNonce": Any<Number>,
|
||||||
|
"width": 300,
|
||||||
|
"x": 630,
|
||||||
|
"y": 316,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test Transform > Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 2`] = `
|
||||||
|
{
|
||||||
|
"angle": 0,
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"boundElements": [
|
||||||
|
{
|
||||||
|
"id": "id46",
|
||||||
|
"type": "arrow",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"customData": undefined,
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"frameId": null,
|
||||||
|
"groupIds": [],
|
||||||
|
"height": 100,
|
||||||
|
"id": Any<String>,
|
||||||
|
"index": "a1",
|
||||||
|
"isDeleted": false,
|
||||||
|
"link": null,
|
||||||
|
"locked": false,
|
||||||
|
"opacity": 100,
|
||||||
|
"roughness": 1,
|
||||||
|
"roundness": null,
|
||||||
|
"seed": Any<Number>,
|
||||||
|
"strokeColor": "#9c36b5",
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"type": "diamond",
|
||||||
|
"updated": 1,
|
||||||
|
"version": 3,
|
||||||
|
"versionNonce": Any<Number>,
|
||||||
|
"width": 140,
|
||||||
|
"x": 96,
|
||||||
|
"y": 400,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test Transform > Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 3`] = `
|
||||||
|
{
|
||||||
|
"angle": 0,
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"boundElements": null,
|
||||||
|
"customData": undefined,
|
||||||
|
"elbowed": false,
|
||||||
|
"endArrowhead": "arrow",
|
||||||
|
"endBinding": {
|
||||||
|
"elementId": "ellipse-1",
|
||||||
|
"fixedPoint": null,
|
||||||
|
"focus": -0.008153707962747813,
|
||||||
|
"gap": 11.562288374879595,
|
||||||
|
},
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"frameId": null,
|
||||||
|
"groupIds": [],
|
||||||
|
"height": 35,
|
||||||
|
"id": Any<String>,
|
||||||
|
"index": "a2",
|
||||||
|
"isDeleted": false,
|
||||||
|
"lastCommittedPoint": null,
|
||||||
|
"link": null,
|
||||||
|
"locked": false,
|
||||||
|
"opacity": 100,
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
0.5,
|
||||||
|
0.5,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
394.5,
|
||||||
|
34.5,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
"roughness": 1,
|
||||||
|
"roundness": null,
|
||||||
|
"seed": Any<Number>,
|
||||||
|
"startArrowhead": null,
|
||||||
|
"startBinding": {
|
||||||
|
"elementId": "id47",
|
||||||
|
"fixedPoint": null,
|
||||||
|
"focus": -0.08139534883720931,
|
||||||
|
"gap": 1,
|
||||||
|
},
|
||||||
|
"strokeColor": "#1864ab",
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"type": "arrow",
|
||||||
|
"updated": 1,
|
||||||
|
"version": 4,
|
||||||
|
"versionNonce": Any<Number>,
|
||||||
|
"width": 395,
|
||||||
|
"x": 247,
|
||||||
|
"y": 420,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test Transform > Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 4`] = `
|
||||||
|
{
|
||||||
|
"angle": 0,
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"boundElements": null,
|
||||||
|
"customData": undefined,
|
||||||
|
"elbowed": false,
|
||||||
|
"endArrowhead": "arrow",
|
||||||
|
"endBinding": {
|
||||||
|
"elementId": "ellipse-1",
|
||||||
|
"fixedPoint": null,
|
||||||
|
"focus": 0.10666666666666667,
|
||||||
|
"gap": 3.8343264684446097,
|
||||||
|
},
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"frameId": null,
|
||||||
|
"groupIds": [],
|
||||||
|
"height": 0,
|
||||||
|
"id": Any<String>,
|
||||||
|
"index": "a3",
|
||||||
|
"isDeleted": false,
|
||||||
|
"lastCommittedPoint": null,
|
||||||
|
"link": null,
|
||||||
|
"locked": false,
|
||||||
|
"opacity": 100,
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
0.5,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
399.5,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
"roughness": 1,
|
||||||
|
"roundness": null,
|
||||||
|
"seed": Any<Number>,
|
||||||
|
"startArrowhead": null,
|
||||||
|
"startBinding": {
|
||||||
|
"elementId": "diamond-1",
|
||||||
|
"fixedPoint": null,
|
||||||
|
"focus": 0,
|
||||||
|
"gap": 5.2311437434718675,
|
||||||
|
},
|
||||||
|
"strokeColor": "#e67700",
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"type": "arrow",
|
||||||
|
"updated": 1,
|
||||||
|
"version": 4,
|
||||||
|
"versionNonce": Any<Number>,
|
||||||
|
"width": 400,
|
||||||
|
"x": 227,
|
||||||
|
"y": 450,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test Transform > Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 5`] = `
|
||||||
|
{
|
||||||
|
"angle": 0,
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"boundElements": [
|
||||||
|
{
|
||||||
|
"id": "id45",
|
||||||
|
"type": "arrow",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"customData": undefined,
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"frameId": null,
|
||||||
|
"groupIds": [],
|
||||||
|
"height": 300,
|
||||||
|
"id": Any<String>,
|
||||||
|
"index": "a4",
|
||||||
|
"isDeleted": false,
|
||||||
|
"link": null,
|
||||||
|
"locked": false,
|
||||||
|
"opacity": 100,
|
||||||
|
"roughness": 1,
|
||||||
|
"roundness": null,
|
||||||
|
"seed": Any<Number>,
|
||||||
|
"strokeColor": "#1e1e1e",
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"type": "rectangle",
|
||||||
|
"updated": 1,
|
||||||
|
"version": 3,
|
||||||
|
"versionNonce": Any<Number>,
|
||||||
|
"width": 300,
|
||||||
|
"x": -53,
|
||||||
|
"y": 270,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`Test Transform > Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 1`] = `
|
exports[`Test Transform > Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 1`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
|
|
|
@ -25,9 +25,9 @@ import type {
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
FixedPoint,
|
FixedPoint,
|
||||||
SceneElementsMap,
|
SceneElementsMap,
|
||||||
|
Bounds,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import type { Bounds } from "./bounds";
|
|
||||||
import { getCenterForBounds, getElementAbsoluteCoords } from "./bounds";
|
import { getCenterForBounds, getElementAbsoluteCoords } from "./bounds";
|
||||||
import type { AppState } from "../types";
|
import type { AppState } from "../types";
|
||||||
import { isPointOnShape } from "../../utils/collision";
|
import { isPointOnShape } from "../../utils/collision";
|
||||||
|
|
|
@ -4,6 +4,7 @@ import type {
|
||||||
ExcalidrawFreeDrawElement,
|
ExcalidrawFreeDrawElement,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
|
Bounds,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||||
|
@ -34,17 +35,7 @@ import { getCurvePathOps } from "../../utils/geometry/shape";
|
||||||
|
|
||||||
type MaybeQuadraticSolution = [number | null, number | null] | false;
|
type MaybeQuadraticSolution = [number | null, number | null] | false;
|
||||||
|
|
||||||
/**
|
export type ViewportBounds = readonly [
|
||||||
* x and y position of top left corner, x and y position of bottom right corner
|
|
||||||
*/
|
|
||||||
export type Bounds = readonly [
|
|
||||||
minX: number,
|
|
||||||
minY: number,
|
|
||||||
maxX: number,
|
|
||||||
maxY: number,
|
|
||||||
];
|
|
||||||
|
|
||||||
export type SceneBounds = readonly [
|
|
||||||
sceneX: number,
|
sceneX: number,
|
||||||
sceneY: number,
|
sceneY: number,
|
||||||
sceneX2: number,
|
sceneX2: number,
|
||||||
|
@ -57,6 +48,7 @@ class ElementBounds {
|
||||||
{
|
{
|
||||||
bounds: Bounds;
|
bounds: Bounds;
|
||||||
version: ExcalidrawElement["version"];
|
version: ExcalidrawElement["version"];
|
||||||
|
versionNonce: ExcalidrawElement["versionNonce"];
|
||||||
}
|
}
|
||||||
>();
|
>();
|
||||||
|
|
||||||
|
@ -66,6 +58,7 @@ class ElementBounds {
|
||||||
if (
|
if (
|
||||||
cachedBounds?.version &&
|
cachedBounds?.version &&
|
||||||
cachedBounds.version === element.version &&
|
cachedBounds.version === element.version &&
|
||||||
|
cachedBounds?.versionNonce === element.versionNonce &&
|
||||||
// we don't invalidate cache when we update containers and not labels,
|
// we don't invalidate cache when we update containers and not labels,
|
||||||
// which is causing problems down the line. Fix TBA.
|
// which is causing problems down the line. Fix TBA.
|
||||||
!isBoundToContainer(element)
|
!isBoundToContainer(element)
|
||||||
|
@ -76,6 +69,7 @@ class ElementBounds {
|
||||||
|
|
||||||
ElementBounds.boundsCache.set(element, {
|
ElementBounds.boundsCache.set(element, {
|
||||||
version: element.version,
|
version: element.version,
|
||||||
|
versionNonce: element.versionNonce,
|
||||||
bounds,
|
bounds,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -93,7 +87,7 @@ class ElementBounds {
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
if (isFreeDrawElement(element)) {
|
if (isFreeDrawElement(element)) {
|
||||||
const [minX, minY, maxX, maxY] = getBoundsFromPoints(
|
const [minX, minY, maxX, maxY] = getBoundsFromFreeDrawPoints(
|
||||||
element.points.map(([x, y]) =>
|
element.points.map(([x, y]) =>
|
||||||
pointRotateRads(
|
pointRotateRads(
|
||||||
point(x, y),
|
point(x, y),
|
||||||
|
@ -177,6 +171,20 @@ class ElementBounds {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the axis-aligned bounds of the given element in global / scene coordinates
|
||||||
|
*
|
||||||
|
* @param element The element to determine the bounding box for
|
||||||
|
* @param elementsMap The elements map to retrieve attached elements (notably text label)
|
||||||
|
* @returns The axis-aligned bounding box in scene (global coordinates)
|
||||||
|
*/
|
||||||
|
export const getElementBounds = (
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
): Bounds => {
|
||||||
|
return ElementBounds.getBounds(element, elementsMap);
|
||||||
|
};
|
||||||
|
|
||||||
// Scene -> Scene coords, but in x1,x2,y1,y2 format.
|
// Scene -> Scene coords, but in x1,x2,y1,y2 format.
|
||||||
//
|
//
|
||||||
// If the element is created from right to left, the width is going to be negative
|
// If the element is created from right to left, the width is going to be negative
|
||||||
|
@ -224,21 +232,6 @@ export const getElementAbsoluteCoords = (
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDiamondPoints = (element: ExcalidrawElement) => {
|
|
||||||
// Here we add +1 to avoid these numbers to be 0
|
|
||||||
// otherwise rough.js will throw an error complaining about it
|
|
||||||
const topX = Math.floor(element.width / 2) + 1;
|
|
||||||
const topY = 0;
|
|
||||||
const rightX = element.width;
|
|
||||||
const rightY = Math.floor(element.height / 2) + 1;
|
|
||||||
const bottomX = topX;
|
|
||||||
const bottomY = element.height;
|
|
||||||
const leftX = 0;
|
|
||||||
const leftY = rightY;
|
|
||||||
|
|
||||||
return [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY];
|
|
||||||
};
|
|
||||||
|
|
||||||
// reference: https://eliot-jones.com/2019/12/cubic-bezier-curve-bounding-boxes
|
// reference: https://eliot-jones.com/2019/12/cubic-bezier-curve-bounding-boxes
|
||||||
const getBezierValueForT = (
|
const getBezierValueForT = (
|
||||||
t: number,
|
t: number,
|
||||||
|
@ -382,7 +375,7 @@ export const getMinMaxXYFromCurvePathOps = (
|
||||||
return [minX, minY, maxX, maxY];
|
return [minX, minY, maxX, maxY];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBoundsFromPoints = (
|
const getBoundsFromFreeDrawPoints = (
|
||||||
points: ExcalidrawFreeDrawElement["points"],
|
points: ExcalidrawFreeDrawElement["points"],
|
||||||
): Bounds => {
|
): Bounds => {
|
||||||
let minX = Infinity;
|
let minX = Infinity;
|
||||||
|
@ -403,7 +396,7 @@ export const getBoundsFromPoints = (
|
||||||
const getFreeDrawElementAbsoluteCoords = (
|
const getFreeDrawElementAbsoluteCoords = (
|
||||||
element: ExcalidrawFreeDrawElement,
|
element: ExcalidrawFreeDrawElement,
|
||||||
): [number, number, number, number, number, number] => {
|
): [number, number, number, number, number, number] => {
|
||||||
const [minX, minY, maxX, maxY] = getBoundsFromPoints(element.points);
|
const [minX, minY, maxX, maxY] = getBoundsFromFreeDrawPoints(element.points);
|
||||||
const x1 = minX + element.x;
|
const x1 = minX + element.x;
|
||||||
const y1 = minY + element.y;
|
const y1 = minY + element.y;
|
||||||
const x2 = maxX + element.x;
|
const x2 = maxX + element.x;
|
||||||
|
@ -496,13 +489,6 @@ const getLinearElementRotatedBounds = (
|
||||||
return coords;
|
return coords;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getElementBounds = (
|
|
||||||
element: ExcalidrawElement,
|
|
||||||
elementsMap: ElementsMap,
|
|
||||||
): Bounds => {
|
|
||||||
return ElementBounds.getBounds(element, elementsMap);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCommonBounds = (
|
export const getCommonBounds = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
elementsMap?: ElementsMap,
|
elementsMap?: ElementsMap,
|
||||||
|
@ -568,7 +554,7 @@ export const getResizedElementAbsoluteCoords = (
|
||||||
|
|
||||||
if (isFreeDrawElement(element)) {
|
if (isFreeDrawElement(element)) {
|
||||||
// Free Draw
|
// Free Draw
|
||||||
bounds = getBoundsFromPoints(points);
|
bounds = getBoundsFromFreeDrawPoints(points);
|
||||||
} else {
|
} else {
|
||||||
// Line
|
// Line
|
||||||
const gen = rough.generator();
|
const gen = rough.generator();
|
||||||
|
@ -651,7 +637,7 @@ export const getVisibleSceneBounds = ({
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
zoom,
|
zoom,
|
||||||
}: AppState): SceneBounds => {
|
}: AppState): ViewportBounds => {
|
||||||
return [
|
return [
|
||||||
-scrollX,
|
-scrollX,
|
||||||
-scrollY,
|
-scrollY,
|
||||||
|
|
|
@ -40,31 +40,33 @@ export const distanceToBindableElement = (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the distance of a point and the provided rectangular-shaped element,
|
||||||
|
* accounting for roundness and rotation
|
||||||
|
*
|
||||||
|
* @param element The rectanguloid element
|
||||||
|
* @param p The point to consider
|
||||||
|
* @returns The eucledian distance to the outline of the rectanguloid element
|
||||||
|
*/
|
||||||
export const distanceToRectangleElement = (
|
export const distanceToRectangleElement = (
|
||||||
element: ExcalidrawRectanguloidElement,
|
element: ExcalidrawRectanguloidElement,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
) => {
|
) => {
|
||||||
const center = point(
|
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
const r = rectangle(
|
const r = rectangle(
|
||||||
pointRotateRads(
|
point(element.x, element.y),
|
||||||
point(element.x, element.y),
|
point(element.x + element.width, element.y + element.height),
|
||||||
center,
|
);
|
||||||
radians(element.angle),
|
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
||||||
),
|
// instead. It's all the same distance-wise.
|
||||||
pointRotateRads(
|
const rotatedPoint = pointRotateRads(
|
||||||
point(element.x + element.width, element.y + element.height),
|
p,
|
||||||
center,
|
point(element.x + element.width / 2, element.y + element.height / 2),
|
||||||
radians(element.angle),
|
radians(-element.angle),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
const roundness = getCornerRadius(
|
const roundness = getCornerRadius(
|
||||||
Math.min(element.width, element.height),
|
Math.min(element.width, element.height),
|
||||||
element,
|
element,
|
||||||
);
|
);
|
||||||
const rotatedPoint = pointRotateRads(p, center, element.angle);
|
|
||||||
const sideDistances = [
|
const sideDistances = [
|
||||||
segment(
|
segment(
|
||||||
point(r[0][0] + roundness, r[0][1]),
|
point(r[0][0] + roundness, r[0][1]),
|
||||||
|
@ -116,10 +118,22 @@ export const distanceToRectangleElement = (
|
||||||
return Math.min(...[...sideDistances, ...cornerDistances]);
|
return Math.min(...[...sideDistances, ...cornerDistances]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const roundedCutoffSegment = (
|
/**
|
||||||
|
* Shortens a segment on both ends to accomodate the arc in the rounded
|
||||||
|
* diamond shape
|
||||||
|
*
|
||||||
|
* @param s The segment to shorten
|
||||||
|
* @param r The radius to shorten by
|
||||||
|
* @returns The segment shortened on both ends by the same radius
|
||||||
|
*/
|
||||||
|
const createDiamondSide = (
|
||||||
s: Segment<GlobalPoint>,
|
s: Segment<GlobalPoint>,
|
||||||
r: number,
|
r: number,
|
||||||
): Segment<GlobalPoint> => {
|
): Segment<GlobalPoint> => {
|
||||||
|
if (r === 0) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
const t = (4 * r) / Math.sqrt(2);
|
const t = (4 * r) / Math.sqrt(2);
|
||||||
|
|
||||||
return segment(
|
return segment(
|
||||||
|
@ -128,17 +142,36 @@ const roundedCutoffSegment = (
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const diamondArc = (left: GlobalPoint, right: GlobalPoint, r: number) => {
|
/**
|
||||||
const c = point((left[0] + right[0]) / 2, left[1]);
|
* Creates an arc for the given roundness and position by taking the start
|
||||||
|
* and end positions and determining the angle points on the hypotethical
|
||||||
|
* circle with center point between start and end and raidus equals provided
|
||||||
|
* roundness. I.e. the created arc is gobal point-aware, or "rotated" in-place.
|
||||||
|
*
|
||||||
|
* @param start
|
||||||
|
* @param end
|
||||||
|
* @param r
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const createDiamondArc = (start: GlobalPoint, end: GlobalPoint, r: number) => {
|
||||||
|
const c = point((start[0] + end[0]) / 2, start[1]);
|
||||||
|
|
||||||
return arc(
|
return arc(
|
||||||
c,
|
c,
|
||||||
r,
|
r,
|
||||||
radians(Math.asin((left[1] - c[1]) / r)),
|
radians(Math.asin((start[1] - c[1]) / r)),
|
||||||
radians(Math.asin((right[1] - c[1]) / r)),
|
radians(Math.asin((end[1] - c[1]) / r)),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the distance of a point and the provided diamond element, accounting
|
||||||
|
* for roundness and rotation
|
||||||
|
*
|
||||||
|
* @param element The diamond element
|
||||||
|
* @param p The point to consider
|
||||||
|
* @returns The eucledian distance to the outline of the diamond
|
||||||
|
*/
|
||||||
export const distanceToDiamondElement = (
|
export const distanceToDiamondElement = (
|
||||||
element: ExcalidrawDiamondElement,
|
element: ExcalidrawDiamondElement,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
|
@ -151,31 +184,19 @@ export const distanceToDiamondElement = (
|
||||||
Math.min(element.width, element.height),
|
Math.min(element.width, element.height),
|
||||||
element,
|
element,
|
||||||
);
|
);
|
||||||
const rotatedPoint = pointRotateRads(p, center, element.angle);
|
// Rotate the point to the inverse direction to simulate the rotated diamond
|
||||||
const top = pointRotateRads<GlobalPoint>(
|
// points. It's all the same distance-wise.
|
||||||
|
const rotatedPoint = pointRotateRads(p, center, radians(-element.angle));
|
||||||
|
const [top, right, bottom, left]: GlobalPoint[] = [
|
||||||
point(element.x + element.width / 2, element.y),
|
point(element.x + element.width / 2, element.y),
|
||||||
center,
|
|
||||||
element.angle,
|
|
||||||
);
|
|
||||||
const right = pointRotateRads<GlobalPoint>(
|
|
||||||
point(element.x + element.width, element.y + element.height / 2),
|
point(element.x + element.width, element.y + element.height / 2),
|
||||||
center,
|
|
||||||
element.angle,
|
|
||||||
);
|
|
||||||
const bottom = pointRotateRads<GlobalPoint>(
|
|
||||||
point(element.x + element.width / 2, element.y + element.height),
|
point(element.x + element.width / 2, element.y + element.height),
|
||||||
center,
|
|
||||||
element.angle,
|
|
||||||
);
|
|
||||||
const left = pointRotateRads<GlobalPoint>(
|
|
||||||
point(element.x, element.y + element.height / 2),
|
point(element.x, element.y + element.height / 2),
|
||||||
center,
|
];
|
||||||
element.angle,
|
const topRight = createDiamondSide(segment(top, right), roundness);
|
||||||
);
|
const bottomRight = createDiamondSide(segment(right, bottom), roundness);
|
||||||
const topRight = roundedCutoffSegment(segment(top, right), roundness);
|
const bottomLeft = createDiamondSide(segment(bottom, left), roundness);
|
||||||
const bottomRight = roundedCutoffSegment(segment(right, bottom), roundness);
|
const topLeft = createDiamondSide(segment(left, top), roundness);
|
||||||
const bottomLeft = roundedCutoffSegment(segment(bottom, left), roundness);
|
|
||||||
const topLeft = roundedCutoffSegment(segment(left, top), roundness);
|
|
||||||
|
|
||||||
return Math.min(
|
return Math.min(
|
||||||
...[
|
...[
|
||||||
|
@ -184,16 +205,24 @@ export const distanceToDiamondElement = (
|
||||||
),
|
),
|
||||||
...(roundness > 0
|
...(roundness > 0
|
||||||
? [
|
? [
|
||||||
diamondArc(topLeft[1], topRight[0], roundness),
|
createDiamondArc(topLeft[1], topRight[0], roundness),
|
||||||
diamondArc(topRight[1], bottomRight[0], roundness),
|
createDiamondArc(topRight[1], bottomRight[0], roundness),
|
||||||
diamondArc(bottomRight[1], bottomLeft[0], roundness),
|
createDiamondArc(bottomRight[1], bottomLeft[0], roundness),
|
||||||
diamondArc(bottomLeft[1], topLeft[0], roundness),
|
createDiamondArc(bottomLeft[1], topLeft[0], roundness),
|
||||||
].map((a) => arcDistanceFromPoint(a, rotatedPoint))
|
].map((a) => arcDistanceFromPoint(a, rotatedPoint))
|
||||||
: []),
|
: []),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the distance of a point and the provided ellipse element, accounting
|
||||||
|
* for roundness and rotation
|
||||||
|
*
|
||||||
|
* @param element The ellipse element
|
||||||
|
* @param p The point to consider
|
||||||
|
* @returns The eucledian distance to the outline of the ellipse
|
||||||
|
*/
|
||||||
export const distanceToEllipseElement = (
|
export const distanceToEllipseElement = (
|
||||||
element: ExcalidrawEllipseElement,
|
element: ExcalidrawEllipseElement,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
|
@ -203,6 +232,7 @@ export const distanceToEllipseElement = (
|
||||||
element.y + element.height / 2,
|
element.y + element.height / 2,
|
||||||
);
|
);
|
||||||
return ellipseDistanceFromPoint(
|
return ellipseDistanceFromPoint(
|
||||||
|
// Instead of rotating the ellipse, rotate the point to the inverse angle
|
||||||
pointRotateRads(p, center, radians(-element.angle)),
|
pointRotateRads(p, center, radians(-element.angle)),
|
||||||
ellipse(center, element.width / 2, element.height / 2),
|
ellipse(center, element.width / 2, element.height / 2),
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { updateBoundElements } from "./binding";
|
import { updateBoundElements } from "./binding";
|
||||||
import type { Bounds } from "./bounds";
|
|
||||||
import { getCommonBounds } from "./bounds";
|
import { getCommonBounds } from "./bounds";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { getPerfectElementSize } from "./sizeHelpers";
|
import { getPerfectElementSize } from "./sizeHelpers";
|
||||||
import type { NonDeletedExcalidrawElement } from "./types";
|
import type { Bounds, NonDeletedExcalidrawElement } from "./types";
|
||||||
import type {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
NormalizedZoomValue,
|
NormalizedZoomValue,
|
||||||
|
|
|
@ -13,8 +13,8 @@ import {
|
||||||
radiansToDegrees,
|
radiansToDegrees,
|
||||||
triangleIncludesPoint,
|
triangleIncludesPoint,
|
||||||
} from "../../math";
|
} from "../../math";
|
||||||
import { getCenterForBounds, type Bounds } from "./bounds";
|
import { getCenterForBounds } from "./bounds";
|
||||||
import type { ExcalidrawBindableElement } from "./types";
|
import type { Bounds, ExcalidrawBindableElement } from "./types";
|
||||||
|
|
||||||
export const HEADING_RIGHT = [1, 0] as Heading;
|
export const HEADING_RIGHT = [1, 0] as Heading;
|
||||||
export const HEADING_DOWN = [0, 1] as Heading;
|
export const HEADING_DOWN = [0, 1] as Heading;
|
||||||
|
|
|
@ -19,9 +19,9 @@ export {
|
||||||
getElementAbsoluteCoords,
|
getElementAbsoluteCoords,
|
||||||
getElementBounds,
|
getElementBounds,
|
||||||
getCommonBounds,
|
getCommonBounds,
|
||||||
getDiamondPoints,
|
|
||||||
getClosestElementBounds,
|
getClosestElementBounds,
|
||||||
} from "./bounds";
|
} from "./bounds";
|
||||||
|
export { getDiamondPoints } from "../scene/Shape";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
|
OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
|
||||||
|
|
|
@ -10,9 +10,9 @@ import type {
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
FixedPointBinding,
|
FixedPointBinding,
|
||||||
SceneElementsMap,
|
SceneElementsMap,
|
||||||
|
Bounds,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from ".";
|
import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from ".";
|
||||||
import type { Bounds } from "./bounds";
|
|
||||||
import { getElementPointsCoords, getMinMaxXYFromCurvePathOps } from "./bounds";
|
import { getElementPointsCoords, getMinMaxXYFromCurvePathOps } from "./bounds";
|
||||||
import type {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
|
|
|
@ -3,6 +3,7 @@ import type {
|
||||||
PointerType,
|
PointerType,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
|
Bounds,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
@ -17,7 +18,6 @@ import {
|
||||||
canResizeFromSides,
|
canResizeFromSides,
|
||||||
} from "./transformHandles";
|
} from "./transformHandles";
|
||||||
import type { AppState, Device, Zoom } from "../types";
|
import type { AppState, Device, Zoom } from "../types";
|
||||||
import type { Bounds } from "./bounds";
|
|
||||||
import { getElementAbsoluteCoords } from "./bounds";
|
import { getElementAbsoluteCoords } from "./bounds";
|
||||||
import { SIDE_RESIZING_THRESHOLD } from "../constants";
|
import { SIDE_RESIZING_THRESHOLD } from "../constants";
|
||||||
import { isLinearElement } from "./typeChecks";
|
import { isLinearElement } from "./typeChecks";
|
||||||
|
|
|
@ -24,7 +24,6 @@ import {
|
||||||
getGlobalFixedPointForBindableElement,
|
getGlobalFixedPointForBindableElement,
|
||||||
snapToMid,
|
snapToMid,
|
||||||
} from "./binding";
|
} from "./binding";
|
||||||
import type { Bounds } from "./bounds";
|
|
||||||
import { distanceToBindableElement } from "./distance";
|
import { distanceToBindableElement } from "./distance";
|
||||||
import type { Heading } from "./heading";
|
import type { Heading } from "./heading";
|
||||||
import {
|
import {
|
||||||
|
@ -40,6 +39,7 @@ import type { ElementUpdate } from "./mutateElement";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { isBindableElement, isRectanguloidElement } from "./typeChecks";
|
import { isBindableElement, isRectanguloidElement } from "./typeChecks";
|
||||||
import type {
|
import type {
|
||||||
|
Bounds,
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
SceneElementsMap,
|
SceneElementsMap,
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import type {
|
import type {
|
||||||
|
Bounds,
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
PointerType,
|
PointerType,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import type { Bounds } from "./bounds";
|
|
||||||
import { getElementAbsoluteCoords } from "./bounds";
|
import { getElementAbsoluteCoords } from "./bounds";
|
||||||
import type { Device, InteractiveCanvasAppState, Zoom } from "../types";
|
import type { Device, InteractiveCanvasAppState, Zoom } from "../types";
|
||||||
import {
|
import {
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { ROUNDNESS } from "../constants";
|
||||||
import type { ElementOrToolType } from "../types";
|
import type { ElementOrToolType } from "../types";
|
||||||
import type { MarkNonNullable } from "../utility-types";
|
import type { MarkNonNullable } from "../utility-types";
|
||||||
import { assertNever } from "../utils";
|
import { assertNever } from "../utils";
|
||||||
import type { Bounds } from "./bounds";
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
|
@ -26,6 +25,7 @@ import type {
|
||||||
PointBinding,
|
PointBinding,
|
||||||
FixedPointBinding,
|
FixedPointBinding,
|
||||||
ExcalidrawFlowchartNodeElement,
|
ExcalidrawFlowchartNodeElement,
|
||||||
|
Bounds,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export const isInitializedImageElement = (
|
export const isInitializedImageElement = (
|
||||||
|
|
|
@ -373,3 +373,13 @@ export type NonDeletedSceneElementsMap = Map<
|
||||||
export type ElementsMapOrArray =
|
export type ElementsMapOrArray =
|
||||||
| readonly ExcalidrawElement[]
|
| readonly ExcalidrawElement[]
|
||||||
| Readonly<ElementsMap>;
|
| Readonly<ElementsMap>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Axis-aligned bounding box (i.e. no rotation)
|
||||||
|
*/
|
||||||
|
export type Bounds = readonly [
|
||||||
|
minX: number,
|
||||||
|
minY: number,
|
||||||
|
maxX: number,
|
||||||
|
maxY: number,
|
||||||
|
];
|
||||||
|
|
|
@ -25,7 +25,7 @@ import type {
|
||||||
import { getElementsWithinSelection, getSelectedElements } from "./scene";
|
import { getElementsWithinSelection, getSelectedElements } from "./scene";
|
||||||
import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
|
import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
|
||||||
import type { ExcalidrawElementsIncludingDeleted } from "./scene/Scene";
|
import type { ExcalidrawElementsIncludingDeleted } from "./scene/Scene";
|
||||||
import { elementsOverlappingBBox } from "../utils/";
|
import { elementsOverlappingBounds } from "../utils/";
|
||||||
import {
|
import {
|
||||||
isFrameElement,
|
isFrameElement,
|
||||||
isFrameLikeElement,
|
isFrameLikeElement,
|
||||||
|
@ -863,7 +863,7 @@ export const getElementsOverlappingFrame = (
|
||||||
frame: ExcalidrawFrameLikeElement,
|
frame: ExcalidrawFrameLikeElement,
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
elementsOverlappingBBox({
|
elementsOverlappingBounds({
|
||||||
elements,
|
elements,
|
||||||
bounds: frame,
|
bounds: frame,
|
||||||
type: "overlap",
|
type: "overlap",
|
||||||
|
|
|
@ -284,9 +284,9 @@ export { convertToExcalidrawElements } from "./data/transform";
|
||||||
export { getCommonBounds, getVisibleSceneBounds } from "./element/bounds";
|
export { getCommonBounds, getVisibleSceneBounds } from "./element/bounds";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
elementsOverlappingBBox,
|
elementsOverlappingBounds as elementsOverlappingBBox,
|
||||||
isElementInsideBBox,
|
isElementInsideBBox,
|
||||||
elementPartiallyOverlapsWithOrContainsBBox,
|
elementPartiallyOverlapsWithOrContainsBounds as elementPartiallyOverlapsWithOrContainsBBox,
|
||||||
} from "../utils/withinBounds";
|
} from "../utils/withinBounds";
|
||||||
|
|
||||||
export { DiagramToCodePlugin } from "./components/DiagramToCodePlugin/DiagramToCodePlugin";
|
export { DiagramToCodePlugin } from "./components/DiagramToCodePlugin/DiagramToCodePlugin";
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||||
import type { Drawable, Options } from "roughjs/bin/core";
|
import type { Drawable, Options } from "roughjs/bin/core";
|
||||||
import type { RoughGenerator } from "roughjs/bin/generator";
|
import type { RoughGenerator } from "roughjs/bin/generator";
|
||||||
import { getArrowheadPoints, getDiamondPoints } from "../element";
|
import { getArrowheadPoints } from "../element";
|
||||||
import type { ElementShapes } from "./types";
|
import type { ElementShapes } from "./types";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
|
@ -278,6 +278,21 @@ const getArrowheadShapes = (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDiamondPoints = (element: ExcalidrawElement) => {
|
||||||
|
// Here we add +1 to avoid these numbers to be 0
|
||||||
|
// otherwise rough.js will throw an error complaining about it
|
||||||
|
const topX = Math.floor(element.width / 2) + 1;
|
||||||
|
const topY = 0;
|
||||||
|
const rightX = element.width;
|
||||||
|
const rightY = Math.floor(element.height / 2) + 1;
|
||||||
|
const bottomX = topX;
|
||||||
|
const bottomY = element.height;
|
||||||
|
const leftX = 0;
|
||||||
|
const leftY = rightY;
|
||||||
|
|
||||||
|
return [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY];
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the roughjs shape for given element.
|
* Generates the roughjs shape for given element.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
import type {
|
import type {
|
||||||
|
Bounds,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawFrameLikeElement,
|
ExcalidrawFrameLikeElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import type { Bounds } from "../element/bounds";
|
|
||||||
import { getCommonBounds, getElementAbsoluteCoords } from "../element/bounds";
|
import { getCommonBounds, getElementAbsoluteCoords } from "../element/bounds";
|
||||||
import { renderSceneToSvg } from "../renderer/staticSvgScene";
|
import { renderSceneToSvg } from "../renderer/staticSvgScene";
|
||||||
import { arrayToMap, getFontString, toBrandedType } from "../utils";
|
import { arrayToMap, getFontString, toBrandedType } from "../utils";
|
||||||
|
|
|
@ -36,11 +36,11 @@ import {
|
||||||
ROUNDNESS,
|
ROUNDNESS,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import { getElementAbsoluteCoords } from "./element";
|
import { getElementAbsoluteCoords } from "./element";
|
||||||
import type { Bounds } from "./element/bounds";
|
|
||||||
import { shouldTestInside } from "./element/collision";
|
import { shouldTestInside } from "./element/collision";
|
||||||
import { LinearElementEditor } from "./element/linearElementEditor";
|
import { LinearElementEditor } from "./element/linearElementEditor";
|
||||||
import { getBoundTextElement } from "./element/textElement";
|
import { getBoundTextElement } from "./element/textElement";
|
||||||
import type {
|
import type {
|
||||||
|
Bounds,
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
|
|
|
@ -8,7 +8,6 @@ import {
|
||||||
type GlobalPoint,
|
type GlobalPoint,
|
||||||
} from "../math";
|
} from "../math";
|
||||||
import { TOOL_TYPE } from "./constants";
|
import { TOOL_TYPE } from "./constants";
|
||||||
import type { Bounds } from "./element/bounds";
|
|
||||||
import {
|
import {
|
||||||
getCommonBounds,
|
getCommonBounds,
|
||||||
getDraggedElementsBounds,
|
getDraggedElementsBounds,
|
||||||
|
@ -17,6 +16,7 @@ import {
|
||||||
import type { MaybeTransformHandleType } from "./element/transformHandles";
|
import type { MaybeTransformHandleType } from "./element/transformHandles";
|
||||||
import { isBoundToContainer, isFrameLikeElement } from "./element/typeChecks";
|
import { isBoundToContainer, isFrameLikeElement } from "./element/typeChecks";
|
||||||
import type {
|
import type {
|
||||||
|
Bounds,
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
|
|
|
@ -4,11 +4,11 @@ import { render } from "./test-utils";
|
||||||
import { reseed } from "../random";
|
import { reseed } from "../random";
|
||||||
import { UI, Keyboard, Pointer } from "./helpers/ui";
|
import { UI, Keyboard, Pointer } from "./helpers/ui";
|
||||||
import type {
|
import type {
|
||||||
|
Bounds,
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
ExcalidrawFreeDrawElement,
|
ExcalidrawFreeDrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import type { Bounds } from "../element/bounds";
|
|
||||||
import { getElementPointsCoords } from "../element/bounds";
|
import { getElementPointsCoords } from "../element/bounds";
|
||||||
import { Excalidraw } from "../index";
|
import { Excalidraw } from "../index";
|
||||||
import { API } from "./helpers/api";
|
import { API } from "./helpers/api";
|
||||||
|
@ -895,7 +895,7 @@ describe("multiple selection", () => {
|
||||||
expect(rightBoundArrow.height).toBeCloseTo(0);
|
expect(rightBoundArrow.height).toBeCloseTo(0);
|
||||||
expect(rightBoundArrow.angle).toEqual(0);
|
expect(rightBoundArrow.angle).toEqual(0);
|
||||||
expect(rightBoundArrow.startBinding).toBeNull();
|
expect(rightBoundArrow.startBinding).toBeNull();
|
||||||
expect(rightBoundArrow.endBinding?.gap).toBeCloseTo(8.0952);
|
expect(rightBoundArrow.endBinding?.gap).toBeCloseTo(7.0952);
|
||||||
expect(rightBoundArrow.endBinding?.elementId).toBe(
|
expect(rightBoundArrow.endBinding?.elementId).toBe(
|
||||||
rightArrowBinding.elementId,
|
rightArrowBinding.elementId,
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { Segment } from "../math";
|
import type { Segment } from "../math";
|
||||||
import { isSegment, segment, point, type GlobalPoint } from "../math";
|
import { isSegment, segment, point, type GlobalPoint } from "../math";
|
||||||
import type { Bounds } from "./element/bounds";
|
|
||||||
import { isBounds } from "./element/typeChecks";
|
import { isBounds } from "./element/typeChecks";
|
||||||
|
import type { Bounds } from "./element/types";
|
||||||
|
|
||||||
// The global data holder to collect the debug operations
|
// The global data holder to collect the debug operations
|
||||||
declare global {
|
declare global {
|
||||||
|
|
|
@ -102,6 +102,22 @@ export function pointRotateRads<Point extends GenericPoint>(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotate multiple points around a common center via the same angle in radians
|
||||||
|
*
|
||||||
|
* @param p The point array to rotate
|
||||||
|
* @param c The common center point
|
||||||
|
* @param angle The common angle to rotate by
|
||||||
|
* @returns The array of rotated points
|
||||||
|
*/
|
||||||
|
function pointsRotateRads<Point extends GenericPoint>(
|
||||||
|
p: Point[],
|
||||||
|
c: Point,
|
||||||
|
angle: Radians,
|
||||||
|
): Point[] {
|
||||||
|
return p.map((x, idx) => pointRotateRads(x, c, angle));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Roate a point by [angle] degree.
|
* Roate a point by [angle] degree.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import type { Bounds } from "../excalidraw/element/bounds";
|
import type { Bounds } from "../excalidraw/element/types";
|
||||||
import { API } from "../excalidraw/tests/helpers/api";
|
import { API } from "../excalidraw/tests/helpers/api";
|
||||||
import {
|
import {
|
||||||
elementPartiallyOverlapsWithOrContainsBBox,
|
elementPartiallyOverlapsWithOrContainsBounds,
|
||||||
elementsOverlappingBBox,
|
elementsOverlappingBounds,
|
||||||
isElementInsideBBox,
|
isElementInsideBBox,
|
||||||
} from "./withinBounds";
|
} from "./withinBounds";
|
||||||
|
|
||||||
|
@ -99,13 +99,13 @@ describe("elementPartiallyOverlapsWithOrContainsBBox()", () => {
|
||||||
|
|
||||||
// bbox contains element
|
// bbox contains element
|
||||||
expect(
|
expect(
|
||||||
elementPartiallyOverlapsWithOrContainsBBox(
|
elementPartiallyOverlapsWithOrContainsBounds(
|
||||||
makeElement(0, 0, 100, 100),
|
makeElement(0, 0, 100, 100),
|
||||||
bbox,
|
bbox,
|
||||||
),
|
),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect(
|
expect(
|
||||||
elementPartiallyOverlapsWithOrContainsBBox(
|
elementPartiallyOverlapsWithOrContainsBounds(
|
||||||
makeElement(10, 10, 90, 90),
|
makeElement(10, 10, 90, 90),
|
||||||
bbox,
|
bbox,
|
||||||
),
|
),
|
||||||
|
@ -113,7 +113,7 @@ describe("elementPartiallyOverlapsWithOrContainsBBox()", () => {
|
||||||
|
|
||||||
// element contains bbox
|
// element contains bbox
|
||||||
expect(
|
expect(
|
||||||
elementPartiallyOverlapsWithOrContainsBBox(
|
elementPartiallyOverlapsWithOrContainsBounds(
|
||||||
makeElement(-10, -10, 110, 110),
|
makeElement(-10, -10, 110, 110),
|
||||||
bbox,
|
bbox,
|
||||||
),
|
),
|
||||||
|
@ -121,28 +121,28 @@ describe("elementPartiallyOverlapsWithOrContainsBBox()", () => {
|
||||||
|
|
||||||
// element overlaps bbox from top-left
|
// element overlaps bbox from top-left
|
||||||
expect(
|
expect(
|
||||||
elementPartiallyOverlapsWithOrContainsBBox(
|
elementPartiallyOverlapsWithOrContainsBounds(
|
||||||
makeElement(-10, -10, 100, 100),
|
makeElement(-10, -10, 100, 100),
|
||||||
bbox,
|
bbox,
|
||||||
),
|
),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
// element overlaps bbox from top-right
|
// element overlaps bbox from top-right
|
||||||
expect(
|
expect(
|
||||||
elementPartiallyOverlapsWithOrContainsBBox(
|
elementPartiallyOverlapsWithOrContainsBounds(
|
||||||
makeElement(90, -10, 100, 100),
|
makeElement(90, -10, 100, 100),
|
||||||
bbox,
|
bbox,
|
||||||
),
|
),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
// element overlaps bbox from bottom-left
|
// element overlaps bbox from bottom-left
|
||||||
expect(
|
expect(
|
||||||
elementPartiallyOverlapsWithOrContainsBBox(
|
elementPartiallyOverlapsWithOrContainsBounds(
|
||||||
makeElement(-10, 90, 100, 100),
|
makeElement(-10, 90, 100, 100),
|
||||||
bbox,
|
bbox,
|
||||||
),
|
),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
// element overlaps bbox from bottom-right
|
// element overlaps bbox from bottom-right
|
||||||
expect(
|
expect(
|
||||||
elementPartiallyOverlapsWithOrContainsBBox(
|
elementPartiallyOverlapsWithOrContainsBounds(
|
||||||
makeElement(90, 90, 100, 100),
|
makeElement(90, 90, 100, 100),
|
||||||
bbox,
|
bbox,
|
||||||
),
|
),
|
||||||
|
@ -154,7 +154,7 @@ describe("elementPartiallyOverlapsWithOrContainsBBox()", () => {
|
||||||
|
|
||||||
// outside diagonally
|
// outside diagonally
|
||||||
expect(
|
expect(
|
||||||
elementPartiallyOverlapsWithOrContainsBBox(
|
elementPartiallyOverlapsWithOrContainsBounds(
|
||||||
makeElement(110, 110, 100, 100),
|
makeElement(110, 110, 100, 100),
|
||||||
bbox,
|
bbox,
|
||||||
),
|
),
|
||||||
|
@ -162,28 +162,28 @@ describe("elementPartiallyOverlapsWithOrContainsBBox()", () => {
|
||||||
|
|
||||||
// outside on the left
|
// outside on the left
|
||||||
expect(
|
expect(
|
||||||
elementPartiallyOverlapsWithOrContainsBBox(
|
elementPartiallyOverlapsWithOrContainsBounds(
|
||||||
makeElement(-110, 10, 50, 50),
|
makeElement(-110, 10, 50, 50),
|
||||||
bbox,
|
bbox,
|
||||||
),
|
),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
// outside on the right
|
// outside on the right
|
||||||
expect(
|
expect(
|
||||||
elementPartiallyOverlapsWithOrContainsBBox(
|
elementPartiallyOverlapsWithOrContainsBounds(
|
||||||
makeElement(110, 10, 50, 50),
|
makeElement(110, 10, 50, 50),
|
||||||
bbox,
|
bbox,
|
||||||
),
|
),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
// outside on the top
|
// outside on the top
|
||||||
expect(
|
expect(
|
||||||
elementPartiallyOverlapsWithOrContainsBBox(
|
elementPartiallyOverlapsWithOrContainsBounds(
|
||||||
makeElement(10, -110, 50, 50),
|
makeElement(10, -110, 50, 50),
|
||||||
bbox,
|
bbox,
|
||||||
),
|
),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
// outside on the bottom
|
// outside on the bottom
|
||||||
expect(
|
expect(
|
||||||
elementPartiallyOverlapsWithOrContainsBBox(
|
elementPartiallyOverlapsWithOrContainsBounds(
|
||||||
makeElement(10, 110, 50, 50),
|
makeElement(10, 110, 50, 50),
|
||||||
bbox,
|
bbox,
|
||||||
),
|
),
|
||||||
|
@ -201,7 +201,7 @@ describe("elementsOverlappingBBox()", () => {
|
||||||
const rectOverlappingTopLeft = makeElement(-10, -10, 50, 50);
|
const rectOverlappingTopLeft = makeElement(-10, -10, 50, 50);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
elementsOverlappingBBox({
|
elementsOverlappingBounds({
|
||||||
bounds: bbox,
|
bounds: bbox,
|
||||||
type: "overlap",
|
type: "overlap",
|
||||||
elements: [
|
elements: [
|
||||||
|
@ -223,7 +223,7 @@ describe("elementsOverlappingBBox()", () => {
|
||||||
const rectOverlappingTopLeft = makeElement(-10, -10, 50, 50);
|
const rectOverlappingTopLeft = makeElement(-10, -10, 50, 50);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
elementsOverlappingBBox({
|
elementsOverlappingBounds({
|
||||||
bounds: bbox,
|
bounds: bbox,
|
||||||
type: "contain",
|
type: "contain",
|
||||||
elements: [
|
elements: [
|
||||||
|
@ -245,7 +245,7 @@ describe("elementsOverlappingBBox()", () => {
|
||||||
const rectOverlappingTopLeft = makeElement(-10, -10, 50, 50);
|
const rectOverlappingTopLeft = makeElement(-10, -10, 50, 50);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
elementsOverlappingBBox({
|
elementsOverlappingBounds({
|
||||||
bounds: bbox,
|
bounds: bbox,
|
||||||
type: "inside",
|
type: "inside",
|
||||||
elements: [
|
elements: [
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import type {
|
import type {
|
||||||
|
Bounds,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawFreeDrawElement,
|
ExcalidrawFreeDrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
|
@ -11,7 +12,6 @@ import {
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "../excalidraw/element/typeChecks";
|
} from "../excalidraw/element/typeChecks";
|
||||||
import type { Bounds } from "../excalidraw/element/bounds";
|
|
||||||
import { getElementBounds } from "../excalidraw/element/bounds";
|
import { getElementBounds } from "../excalidraw/element/bounds";
|
||||||
import { arrayToMap } from "../excalidraw/utils";
|
import { arrayToMap } from "../excalidraw/utils";
|
||||||
import type { LocalPoint } from "../math";
|
import type { LocalPoint } from "../math";
|
||||||
|
@ -22,15 +22,10 @@ import {
|
||||||
rangeInclusive,
|
rangeInclusive,
|
||||||
} from "../math";
|
} from "../math";
|
||||||
|
|
||||||
type Element = NonDeletedExcalidrawElement;
|
|
||||||
type Elements = readonly NonDeletedExcalidrawElement[];
|
|
||||||
|
|
||||||
type Points = readonly LocalPoint[];
|
|
||||||
|
|
||||||
/** @returns vertices relative to element's top-left [0,0] position */
|
/** @returns vertices relative to element's top-left [0,0] position */
|
||||||
const getNonLinearElementRelativePoints = (
|
const getNonLinearElementRelativePoints = (
|
||||||
element: Exclude<
|
element: Exclude<
|
||||||
Element,
|
NonDeletedExcalidrawElement,
|
||||||
ExcalidrawLinearElement | ExcalidrawFreeDrawElement
|
ExcalidrawLinearElement | ExcalidrawFreeDrawElement
|
||||||
>,
|
>,
|
||||||
): [
|
): [
|
||||||
|
@ -56,14 +51,16 @@ const getNonLinearElementRelativePoints = (
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @returns vertices relative to element's top-left [0,0] position */
|
/** @returns vertices relative to element's top-left [0,0] position */
|
||||||
const getElementRelativePoints = (element: ExcalidrawElement): Points => {
|
const getElementRelativePoints = (
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
): readonly LocalPoint[] => {
|
||||||
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
||||||
return element.points;
|
return element.points;
|
||||||
}
|
}
|
||||||
return getNonLinearElementRelativePoints(element);
|
return getNonLinearElementRelativePoints(element);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMinMaxPoints = (points: Points) => {
|
const getMinMaxPoints = (points: readonly LocalPoint[]) => {
|
||||||
const ret = points.reduce(
|
const ret = points.reduce(
|
||||||
(limits, [x, y]) => {
|
(limits, [x, y]) => {
|
||||||
limits.minY = Math.min(limits.minY, y);
|
limits.minY = Math.min(limits.minY, y);
|
||||||
|
@ -90,7 +87,7 @@ const getMinMaxPoints = (points: Points) => {
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRotatedBBox = (element: Element): Bounds => {
|
const getRotatedBBox = (element: NonDeletedExcalidrawElement): Bounds => {
|
||||||
const points = getElementRelativePoints(element);
|
const points = getElementRelativePoints(element);
|
||||||
|
|
||||||
const { cx, cy } = getMinMaxPoints(points);
|
const { cx, cy } = getMinMaxPoints(points);
|
||||||
|
@ -110,7 +107,7 @@ const getRotatedBBox = (element: Element): Bounds => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isElementInsideBBox = (
|
export const isElementInsideBBox = (
|
||||||
element: Element,
|
element: NonDeletedExcalidrawElement,
|
||||||
bbox: Bounds,
|
bbox: Bounds,
|
||||||
eitherDirection = false,
|
eitherDirection = false,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
|
@ -138,8 +135,8 @@ export const isElementInsideBBox = (
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const elementPartiallyOverlapsWithOrContainsBBox = (
|
export const elementPartiallyOverlapsWithOrContainsBounds = (
|
||||||
element: Element,
|
element: NonDeletedExcalidrawElement,
|
||||||
bbox: Bounds,
|
bbox: Bounds,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const elementBBox = getRotatedBBox(element);
|
const elementBBox = getRotatedBBox(element);
|
||||||
|
@ -158,13 +155,13 @@ export const elementPartiallyOverlapsWithOrContainsBBox = (
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const elementsOverlappingBBox = ({
|
export const elementsOverlappingBounds = ({
|
||||||
elements,
|
elements,
|
||||||
bounds,
|
bounds,
|
||||||
type,
|
type,
|
||||||
errorMargin = 0,
|
errorMargin = 0,
|
||||||
}: {
|
}: {
|
||||||
elements: Elements;
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
bounds: Bounds | ExcalidrawElement;
|
bounds: Bounds | ExcalidrawElement;
|
||||||
/** safety offset. Defaults to 0. */
|
/** safety offset. Defaults to 0. */
|
||||||
errorMargin?: number;
|
errorMargin?: number;
|
||||||
|
@ -194,7 +191,7 @@ export const elementsOverlappingBBox = ({
|
||||||
|
|
||||||
const isOverlaping =
|
const isOverlaping =
|
||||||
type === "overlap"
|
type === "overlap"
|
||||||
? elementPartiallyOverlapsWithOrContainsBBox(element, adjustedBBox)
|
? elementPartiallyOverlapsWithOrContainsBounds(element, adjustedBBox)
|
||||||
: type === "inside"
|
: type === "inside"
|
||||||
? isElementInsideBBox(element, adjustedBBox)
|
? isElementInsideBBox(element, adjustedBBox)
|
||||||
: isElementInsideBBox(element, adjustedBBox, true);
|
: isElementInsideBBox(element, adjustedBBox, true);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue