Compare commits
41 Commits
master
...
mtolmacs/f
Author | SHA1 | Date | |
---|---|---|---|
![]() |
99a4428f6e | ||
![]() |
8cc8367335 | ||
![]() |
5081abdde1 | ||
![]() |
9c04e22897 | ||
![]() |
68cb5056ac | ||
![]() |
41c036e1a5 | ||
![]() |
91d36e9b81 | ||
![]() |
27522110df | ||
![]() |
2e61d46cbd | ||
![]() |
18c21593e6 | ||
![]() |
bcdcb21971 | ||
![]() |
624b9927cd | ||
![]() |
aa758fa6d6 | ||
![]() |
9801b9a12f | ||
![]() |
946c366a1c | ||
![]() |
8b248eda94 | ||
![]() |
49613ad0c3 | ||
![]() |
da9cda14d6 | ||
![]() |
4ba1bdaead | ||
![]() |
a9e8c7577b | ||
![]() |
103f036734 | ||
![]() |
9c0a81d42c | ||
![]() |
6c59c5eefb | ||
![]() |
70c447059f | ||
![]() |
74b512a605 | ||
![]() |
8a808b6e91 | ||
![]() |
e25441c8f8 | ||
![]() |
47f12a4d85 | ||
![]() |
13bd8cad96 | ||
![]() |
47cb16822c | ||
![]() |
59a986cb41 | ||
![]() |
2e6d0ecb65 | ||
![]() |
d08414c2a9 | ||
![]() |
e1b81480ac | ||
![]() |
3b037a7d82 | ||
![]() |
65cecf041a | ||
![]() |
8157b570db | ||
![]() |
814520b1f0 | ||
![]() |
9203c99eec | ||
![]() |
52205031ab | ||
![]() |
04e1bf0bc4 |
@ -34,6 +34,9 @@
|
||||
<a href="https://discord.gg/UexuTaE">
|
||||
<img alt="Chat on Discord" src="https://img.shields.io/discord/723672430744174682?color=738ad6&label=Chat%20on%20Discord&logo=discord&logoColor=ffffff&widge=false"/>
|
||||
</a>
|
||||
<a href="https://deepwiki.com/excalidraw/excalidraw">
|
||||
<img alt="Ask DeepWiki" src="https://deepwiki.com/badge.svg" />
|
||||
</a>
|
||||
<a href="https://twitter.com/excalidraw">
|
||||
<img alt="Follow Excalidraw on Twitter" src="https://img.shields.io/twitter/follow/excalidraw.svg?label=follow+@excalidraw&style=social&logo=twitter"/>
|
||||
</a>
|
||||
|
@ -1,8 +1,17 @@
|
||||
import { simplify } from "points-on-curve";
|
||||
|
||||
import { pointFrom, pointDistance, type LocalPoint } from "@excalidraw/math";
|
||||
import {
|
||||
pointFrom,
|
||||
pointDistance,
|
||||
type LocalPoint,
|
||||
pointRotateRads,
|
||||
} from "@excalidraw/math";
|
||||
import { ROUGHNESS, isTransparent, assertNever } from "@excalidraw/common";
|
||||
|
||||
import { RoughGenerator } from "roughjs/bin/generator";
|
||||
|
||||
import type { GlobalPoint, Radians } from "@excalidraw/math";
|
||||
|
||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||
|
||||
import type { EmbedsValidationStatus } from "@excalidraw/excalidraw/types";
|
||||
@ -20,7 +29,11 @@ import { headingForPointIsHorizontal } from "./heading";
|
||||
|
||||
import { canChangeRoundness } from "./comparisons";
|
||||
import { generateFreeDrawShape } from "./renderElement";
|
||||
import { getArrowheadPoints, getDiamondPoints } from "./bounds";
|
||||
import {
|
||||
getArrowheadPoints,
|
||||
getDiamondPoints,
|
||||
getElementBounds,
|
||||
} from "./bounds";
|
||||
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
@ -28,10 +41,10 @@ import type {
|
||||
ExcalidrawSelectionElement,
|
||||
ExcalidrawLinearElement,
|
||||
Arrowhead,
|
||||
ExcalidrawFreeDrawElement,
|
||||
} from "./types";
|
||||
|
||||
import type { Drawable, Options } from "roughjs/bin/core";
|
||||
import type { RoughGenerator } from "roughjs/bin/generator";
|
||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||
|
||||
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
||||
@ -303,6 +316,125 @@ const getArrowheadShapes = (
|
||||
}
|
||||
};
|
||||
|
||||
export const generateLinearCollisionShape = (
|
||||
element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
|
||||
) => {
|
||||
const generator = new RoughGenerator();
|
||||
const options: Options = {
|
||||
seed: element.seed,
|
||||
disableMultiStroke: true,
|
||||
disableMultiStrokeFill: true,
|
||||
roughness: 0,
|
||||
preserveVertices: true,
|
||||
};
|
||||
|
||||
switch (element.type) {
|
||||
case "line":
|
||||
case "arrow": {
|
||||
// points array can be empty in the beginning, so it is important to add
|
||||
// initial position to it
|
||||
const points = element.points.length
|
||||
? element.points
|
||||
: [pointFrom<LocalPoint>(0, 0)];
|
||||
const [x1, y1, x2, y2] = getElementBounds(
|
||||
{
|
||||
...element,
|
||||
angle: 0 as Radians,
|
||||
},
|
||||
new Map(),
|
||||
);
|
||||
const center = pointFrom<GlobalPoint>((x1 + x2) / 2, (y1 + y2) / 2);
|
||||
|
||||
if (isElbowArrow(element)) {
|
||||
return generator.path(generateElbowArrowShape(points, 16), options)
|
||||
.sets[0].ops;
|
||||
} else if (!element.roundness) {
|
||||
return points.map((point, idx) => {
|
||||
const p = pointRotateRads(
|
||||
pointFrom<GlobalPoint>(element.x + point[0], element.y + point[1]),
|
||||
center,
|
||||
element.angle,
|
||||
);
|
||||
|
||||
return {
|
||||
op: idx === 0 ? "move" : "lineTo",
|
||||
data: pointFrom<LocalPoint>(p[0] - element.x, p[1] - element.y),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return generator
|
||||
.curve(points as unknown as RoughPoint[], options)
|
||||
.sets[0].ops.slice(0, element.points.length)
|
||||
.map((op, i, arr) => {
|
||||
if (i === 0) {
|
||||
const p = pointRotateRads<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + op.data[0],
|
||||
element.y + op.data[1],
|
||||
),
|
||||
center,
|
||||
element.angle,
|
||||
);
|
||||
|
||||
return {
|
||||
op: "move",
|
||||
data: pointFrom<LocalPoint>(p[0] - element.x, p[1] - element.y),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
op: "bcurveTo",
|
||||
data: [
|
||||
pointRotateRads(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + op.data[0],
|
||||
element.y + op.data[1],
|
||||
),
|
||||
center,
|
||||
element.angle,
|
||||
),
|
||||
pointRotateRads(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + op.data[2],
|
||||
element.y + op.data[3],
|
||||
),
|
||||
center,
|
||||
element.angle,
|
||||
),
|
||||
pointRotateRads(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + op.data[4],
|
||||
element.y + op.data[5],
|
||||
),
|
||||
center,
|
||||
element.angle,
|
||||
),
|
||||
]
|
||||
.map((p) =>
|
||||
pointFrom<LocalPoint>(p[0] - element.x, p[1] - element.y),
|
||||
)
|
||||
.flat(),
|
||||
};
|
||||
});
|
||||
}
|
||||
case "freedraw": {
|
||||
if (element.points.length < 2) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const simplifiedPoints = simplify(
|
||||
element.points as Mutable<LocalPoint[]>,
|
||||
0.75,
|
||||
);
|
||||
|
||||
return generator
|
||||
.curve(simplifiedPoints as [number, number][], options)
|
||||
.sets[0].ops.slice(0, element.points.length);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates the roughjs shape for given element.
|
||||
*
|
||||
|
@ -27,8 +27,6 @@ import {
|
||||
PRECISION,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import { isPointOnShape } from "@excalidraw/utils/collision";
|
||||
|
||||
import type { LocalPoint, Radians } from "@excalidraw/math";
|
||||
|
||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
@ -41,7 +39,7 @@ import {
|
||||
doBoundsIntersect,
|
||||
} from "./bounds";
|
||||
import { intersectElementWithLineSegment } from "./collision";
|
||||
import { distanceToBindableElement } from "./distance";
|
||||
import { distanceToElement } from "./distance";
|
||||
import {
|
||||
headingForPointFromElement,
|
||||
headingIsHorizontal,
|
||||
@ -63,7 +61,7 @@ import {
|
||||
isTextElement,
|
||||
} from "./typeChecks";
|
||||
|
||||
import { aabbForElement, getElementShape, pointInsideBounds } from "./shapes";
|
||||
import { aabbForElement } from "./shapes";
|
||||
import { updateElbowArrowPoints } from "./elbowArrow";
|
||||
|
||||
import type { Scene } from "./Scene";
|
||||
@ -109,7 +107,6 @@ export const isBindingEnabled = (appState: AppState): boolean => {
|
||||
|
||||
export const FIXED_BINDING_DISTANCE = 5;
|
||||
export const BINDING_HIGHLIGHT_THICKNESS = 10;
|
||||
export const BINDING_HIGHLIGHT_OFFSET = 4;
|
||||
|
||||
const getNonDeletedElements = (
|
||||
scene: Scene,
|
||||
@ -216,43 +213,29 @@ const bindOrUnbindLinearElementEdge = (
|
||||
}
|
||||
};
|
||||
|
||||
const getOriginalBindingIfStillCloseOfLinearElementEdge = (
|
||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
edge: "start" | "end",
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
zoom?: AppState["zoom"],
|
||||
): NonDeleted<ExcalidrawElement> | null => {
|
||||
const coors = getLinearElementEdgeCoors(linearElement, edge, elementsMap);
|
||||
const elementId =
|
||||
edge === "start"
|
||||
? linearElement.startBinding?.elementId
|
||||
: linearElement.endBinding?.elementId;
|
||||
if (elementId) {
|
||||
const element = elementsMap.get(elementId);
|
||||
if (
|
||||
isBindableElement(element) &&
|
||||
bindingBorderTest(element, coors, elementsMap, zoom)
|
||||
) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getOriginalBindingsIfStillCloseToArrowEnds = (
|
||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
zoom?: AppState["zoom"],
|
||||
): (NonDeleted<ExcalidrawElement> | null)[] =>
|
||||
["start", "end"].map((edge) =>
|
||||
getOriginalBindingIfStillCloseOfLinearElementEdge(
|
||||
linearElement,
|
||||
edge as "start" | "end",
|
||||
elementsMap,
|
||||
zoom,
|
||||
),
|
||||
);
|
||||
(["start", "end"] as const).map((edge) => {
|
||||
const coors = getLinearElementEdgeCoors(linearElement, edge, elementsMap);
|
||||
const elementId =
|
||||
edge === "start"
|
||||
? linearElement.startBinding?.elementId
|
||||
: linearElement.endBinding?.elementId;
|
||||
if (elementId) {
|
||||
const element = elementsMap.get(elementId);
|
||||
if (
|
||||
isBindableElement(element) &&
|
||||
bindingBorderTest(element, coors, elementsMap, zoom)
|
||||
) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const getBindingStrategyForDraggingArrowEndpoints = (
|
||||
selectedElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
@ -268,7 +251,7 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
||||
const endDragged = draggingPoints.findIndex((i) => i === endIdx) > -1;
|
||||
const start = startDragged
|
||||
? isBindingEnabled
|
||||
? getElligibleElementForBindingElement(
|
||||
? getEligibleElementForBindingElement(
|
||||
selectedElement,
|
||||
"start",
|
||||
elementsMap,
|
||||
@ -279,7 +262,7 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
||||
: "keep";
|
||||
const end = endDragged
|
||||
? isBindingEnabled
|
||||
? getElligibleElementForBindingElement(
|
||||
? getEligibleElementForBindingElement(
|
||||
selectedElement,
|
||||
"end",
|
||||
elementsMap,
|
||||
@ -311,7 +294,7 @@ const getBindingStrategyForDraggingArrowOrJoints = (
|
||||
);
|
||||
const start = startIsClose
|
||||
? isBindingEnabled
|
||||
? getElligibleElementForBindingElement(
|
||||
? getEligibleElementForBindingElement(
|
||||
selectedElement,
|
||||
"start",
|
||||
elementsMap,
|
||||
@ -322,7 +305,7 @@ const getBindingStrategyForDraggingArrowOrJoints = (
|
||||
: null;
|
||||
const end = endIsClose
|
||||
? isBindingEnabled
|
||||
? getElligibleElementForBindingElement(
|
||||
? getEligibleElementForBindingElement(
|
||||
selectedElement,
|
||||
"end",
|
||||
elementsMap,
|
||||
@ -441,22 +424,13 @@ export const maybeBindLinearElement = (
|
||||
const normalizePointBinding = (
|
||||
binding: { focus: number; gap: number },
|
||||
hoveredElement: ExcalidrawBindableElement,
|
||||
) => {
|
||||
let gap = binding.gap;
|
||||
const maxGap = maxBindingGap(
|
||||
hoveredElement,
|
||||
hoveredElement.width,
|
||||
hoveredElement.height,
|
||||
);
|
||||
|
||||
if (gap > maxGap) {
|
||||
gap = BINDING_HIGHLIGHT_THICKNESS + BINDING_HIGHLIGHT_OFFSET;
|
||||
}
|
||||
return {
|
||||
...binding,
|
||||
gap,
|
||||
};
|
||||
};
|
||||
) => ({
|
||||
...binding,
|
||||
gap: Math.min(
|
||||
binding.gap,
|
||||
maxBindingGap(hoveredElement, hoveredElement.width, hoveredElement.height),
|
||||
),
|
||||
});
|
||||
|
||||
export const bindLinearElement = (
|
||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
@ -704,7 +678,7 @@ const calculateFocusAndGap = (
|
||||
|
||||
return {
|
||||
focus: determineFocusDistance(hoveredElement, adjacentPoint, edgePoint),
|
||||
gap: Math.max(1, distanceToBindableElement(hoveredElement, edgePoint)),
|
||||
gap: Math.max(1, distanceToElement(hoveredElement, edgePoint)),
|
||||
};
|
||||
};
|
||||
|
||||
@ -898,7 +872,7 @@ const getDistanceForBinding = (
|
||||
bindableElement: ExcalidrawBindableElement,
|
||||
zoom?: AppState["zoom"],
|
||||
) => {
|
||||
const distance = distanceToBindableElement(bindableElement, point);
|
||||
const distance = distanceToElement(bindableElement, point);
|
||||
const bindDistance = maxBindingGap(
|
||||
bindableElement,
|
||||
bindableElement.width,
|
||||
@ -945,22 +919,25 @@ export const bindPointToSnapToElementOutline = (
|
||||
const isHorizontal = headingIsHorizontal(
|
||||
headingForPointFromElement(bindableElement, aabb, globalP),
|
||||
);
|
||||
const snapPoint = snapToMid(bindableElement, edgePoint);
|
||||
const otherPoint = pointFrom<GlobalPoint>(
|
||||
isHorizontal ? center[0] : edgePoint[0],
|
||||
!isHorizontal ? center[1] : edgePoint[1],
|
||||
isHorizontal ? center[0] : snapPoint[0],
|
||||
!isHorizontal ? center[1] : snapPoint[1],
|
||||
);
|
||||
const intersector = lineSegment(
|
||||
otherPoint,
|
||||
pointFromVector(
|
||||
vectorScale(
|
||||
vectorNormalize(vectorFromPoint(snapPoint, otherPoint)),
|
||||
Math.max(bindableElement.width, bindableElement.height) * 2,
|
||||
),
|
||||
otherPoint,
|
||||
),
|
||||
);
|
||||
intersection = intersectElementWithLineSegment(
|
||||
bindableElement,
|
||||
lineSegment(
|
||||
otherPoint,
|
||||
pointFromVector(
|
||||
vectorScale(
|
||||
vectorNormalize(vectorFromPoint(edgePoint, otherPoint)),
|
||||
Math.max(bindableElement.width, bindableElement.height) * 2,
|
||||
),
|
||||
otherPoint,
|
||||
),
|
||||
),
|
||||
intersector,
|
||||
FIXED_BINDING_DISTANCE,
|
||||
)[0];
|
||||
} else {
|
||||
intersection = intersectElementWithLineSegment(
|
||||
@ -991,24 +968,7 @@ export const bindPointToSnapToElementOutline = (
|
||||
return edgePoint;
|
||||
}
|
||||
|
||||
if (elbowed) {
|
||||
const scalar =
|
||||
pointDistanceSq(edgePoint, center) -
|
||||
pointDistanceSq(intersection, center) >
|
||||
0
|
||||
? FIXED_BINDING_DISTANCE
|
||||
: -FIXED_BINDING_DISTANCE;
|
||||
|
||||
return pointFromVector(
|
||||
vectorScale(
|
||||
vectorNormalize(vectorFromPoint(edgePoint, intersection)),
|
||||
scalar,
|
||||
),
|
||||
intersection,
|
||||
);
|
||||
}
|
||||
|
||||
return edgePoint;
|
||||
return elbowed ? intersection : edgePoint;
|
||||
};
|
||||
|
||||
export const avoidRectangularCorner = (
|
||||
@ -1112,31 +1072,29 @@ export const snapToMid = (
|
||||
tolerance: number = 0.05,
|
||||
): GlobalPoint => {
|
||||
const { x, y, width, height, angle } = element;
|
||||
|
||||
const center = elementCenterPoint(element, -0.1, -0.1);
|
||||
|
||||
const nonRotated = pointRotateRads(p, center, -angle as Radians);
|
||||
|
||||
// snap-to-center point is adaptive to element size, but we don't want to go
|
||||
// above and below certain px distance
|
||||
const verticalThrehsold = clamp(tolerance * height, 5, 80);
|
||||
const horizontalThrehsold = clamp(tolerance * width, 5, 80);
|
||||
const verticalThreshold = clamp(tolerance * height, 5, 80);
|
||||
const horizontalThreshold = clamp(tolerance * width, 5, 80);
|
||||
|
||||
if (
|
||||
nonRotated[0] <= x + width / 2 &&
|
||||
nonRotated[1] > center[1] - verticalThrehsold &&
|
||||
nonRotated[1] < center[1] + verticalThrehsold
|
||||
nonRotated[1] > center[1] - verticalThreshold &&
|
||||
nonRotated[1] < center[1] + verticalThreshold
|
||||
) {
|
||||
// LEFT
|
||||
return pointRotateRads(
|
||||
return pointRotateRads<GlobalPoint>(
|
||||
pointFrom(x - FIXED_BINDING_DISTANCE, center[1]),
|
||||
center,
|
||||
angle,
|
||||
);
|
||||
} else if (
|
||||
nonRotated[1] <= y + height / 2 &&
|
||||
nonRotated[0] > center[0] - horizontalThrehsold &&
|
||||
nonRotated[0] < center[0] + horizontalThrehsold
|
||||
nonRotated[0] > center[0] - horizontalThreshold &&
|
||||
nonRotated[0] < center[0] + horizontalThreshold
|
||||
) {
|
||||
// TOP
|
||||
return pointRotateRads(
|
||||
@ -1146,8 +1104,8 @@ export const snapToMid = (
|
||||
);
|
||||
} else if (
|
||||
nonRotated[0] >= x + width / 2 &&
|
||||
nonRotated[1] > center[1] - verticalThrehsold &&
|
||||
nonRotated[1] < center[1] + verticalThrehsold
|
||||
nonRotated[1] > center[1] - verticalThreshold &&
|
||||
nonRotated[1] < center[1] + verticalThreshold
|
||||
) {
|
||||
// RIGHT
|
||||
return pointRotateRads(
|
||||
@ -1157,8 +1115,8 @@ export const snapToMid = (
|
||||
);
|
||||
} else if (
|
||||
nonRotated[1] >= y + height / 2 &&
|
||||
nonRotated[0] > center[0] - horizontalThrehsold &&
|
||||
nonRotated[0] < center[0] + horizontalThrehsold
|
||||
nonRotated[0] > center[0] - horizontalThreshold &&
|
||||
nonRotated[0] < center[0] + horizontalThreshold
|
||||
) {
|
||||
// DOWN
|
||||
return pointRotateRads(
|
||||
@ -1167,7 +1125,7 @@ export const snapToMid = (
|
||||
angle,
|
||||
);
|
||||
} else if (element.type === "diamond") {
|
||||
const distance = FIXED_BINDING_DISTANCE - 1;
|
||||
const distance = FIXED_BINDING_DISTANCE;
|
||||
const topLeft = pointFrom<GlobalPoint>(
|
||||
x + width / 4 - distance,
|
||||
y + height / 4 - distance,
|
||||
@ -1184,27 +1142,28 @@ export const snapToMid = (
|
||||
x + (3 * width) / 4 + distance,
|
||||
y + (3 * height) / 4 + distance,
|
||||
);
|
||||
|
||||
if (
|
||||
pointDistance(topLeft, nonRotated) <
|
||||
Math.max(horizontalThrehsold, verticalThrehsold)
|
||||
Math.max(horizontalThreshold, verticalThreshold)
|
||||
) {
|
||||
return pointRotateRads(topLeft, center, angle);
|
||||
}
|
||||
if (
|
||||
pointDistance(topRight, nonRotated) <
|
||||
Math.max(horizontalThrehsold, verticalThrehsold)
|
||||
Math.max(horizontalThreshold, verticalThreshold)
|
||||
) {
|
||||
return pointRotateRads(topRight, center, angle);
|
||||
}
|
||||
if (
|
||||
pointDistance(bottomLeft, nonRotated) <
|
||||
Math.max(horizontalThrehsold, verticalThrehsold)
|
||||
Math.max(horizontalThreshold, verticalThreshold)
|
||||
) {
|
||||
return pointRotateRads(bottomLeft, center, angle);
|
||||
}
|
||||
if (
|
||||
pointDistance(bottomRight, nonRotated) <
|
||||
Math.max(horizontalThrehsold, verticalThrehsold)
|
||||
Math.max(horizontalThreshold, verticalThreshold)
|
||||
) {
|
||||
return pointRotateRads(bottomRight, center, angle);
|
||||
}
|
||||
@ -1396,7 +1355,7 @@ const maybeCalculateNewGapWhenScaling = (
|
||||
return { ...currentBinding, gap: newGap };
|
||||
};
|
||||
|
||||
const getElligibleElementForBindingElement = (
|
||||
const getEligibleElementForBindingElement = (
|
||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
startOrEnd: "start" | "end",
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
@ -1548,14 +1507,22 @@ export const bindingBorderTest = (
|
||||
zoom?: AppState["zoom"],
|
||||
fullShape?: boolean,
|
||||
): boolean => {
|
||||
const p = pointFrom<GlobalPoint>(x, y);
|
||||
const threshold = maxBindingGap(element, element.width, element.height, zoom);
|
||||
|
||||
const shape = getElementShape(element, elementsMap);
|
||||
return (
|
||||
isPointOnShape(pointFrom(x, y), shape, threshold) ||
|
||||
(fullShape === true &&
|
||||
pointInsideBounds(pointFrom(x, y), aabbForElement(element)))
|
||||
const shouldTestInside =
|
||||
// disable fullshape snapping for frame elements so we
|
||||
// can bind to frame children
|
||||
(fullShape || !isBindingFallthroughEnabled(element)) &&
|
||||
!isFrameLikeElement(element);
|
||||
const intersections = intersectElementWithLineSegment(
|
||||
element,
|
||||
lineSegment(elementCenterPoint(element), p),
|
||||
);
|
||||
const distance = distanceToElement(element, p);
|
||||
|
||||
return shouldTestInside
|
||||
? intersections.length === 0 || distance <= threshold
|
||||
: intersections.length > 0 && distance <= threshold;
|
||||
};
|
||||
|
||||
export const maxBindingGap = (
|
||||
@ -1575,7 +1542,7 @@ export const maxBindingGap = (
|
||||
// bigger bindable boundary for bigger elements
|
||||
Math.min(0.25 * smallerDimension, 32),
|
||||
// keep in sync with the zoomed highlight
|
||||
BINDING_HIGHLIGHT_THICKNESS / zoomValue + BINDING_HIGHLIGHT_OFFSET,
|
||||
BINDING_HIGHLIGHT_THICKNESS / zoomValue + FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,52 +1,61 @@
|
||||
import { isTransparent, elementCenterPoint } from "@excalidraw/common";
|
||||
import {
|
||||
isTransparent,
|
||||
elementCenterPoint,
|
||||
arrayToMap,
|
||||
} from "@excalidraw/common";
|
||||
import {
|
||||
curveIntersectLineSegment,
|
||||
isPointWithinBounds,
|
||||
line,
|
||||
lineSegment,
|
||||
lineSegmentIntersectionPoints,
|
||||
pointDistanceSq,
|
||||
pointFrom,
|
||||
pointFromVector,
|
||||
pointRotateRads,
|
||||
pointsEqual,
|
||||
vectorFromPoint,
|
||||
vectorNormalize,
|
||||
vectorScale,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import {
|
||||
ellipse,
|
||||
ellipseLineIntersectionPoints,
|
||||
ellipseSegmentInterceptPoints,
|
||||
} from "@excalidraw/math/ellipse";
|
||||
|
||||
import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision";
|
||||
import { type GeometricShape, getPolygonShape } from "@excalidraw/utils/shape";
|
||||
|
||||
import type {
|
||||
GlobalPoint,
|
||||
LineSegment,
|
||||
LocalPoint,
|
||||
Polygon,
|
||||
Radians,
|
||||
} from "@excalidraw/math";
|
||||
import type { GlobalPoint, LineSegment, Radians } from "@excalidraw/math";
|
||||
|
||||
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { getBoundTextShape, isPathALoop } from "./shapes";
|
||||
import { isPathALoop } from "./shapes";
|
||||
import { getElementBounds } from "./bounds";
|
||||
import {
|
||||
hasBoundTextElement,
|
||||
isFreeDrawElement,
|
||||
isIframeLikeElement,
|
||||
isImageElement,
|
||||
isLinearElement,
|
||||
isTextElement,
|
||||
} from "./typeChecks";
|
||||
import {
|
||||
deconstructDiamondElement,
|
||||
deconstructLinearOrFreeDrawElement,
|
||||
deconstructRectanguloidElement,
|
||||
} from "./utils";
|
||||
|
||||
import { getBoundTextElement } from "./textElement";
|
||||
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
|
||||
import { distanceToElement } from "./distance";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawDiamondElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawEllipseElement,
|
||||
ExcalidrawRectangleElement,
|
||||
ExcalidrawFreeDrawElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawRectanguloidElement,
|
||||
} from "./types";
|
||||
|
||||
@ -72,45 +81,49 @@ export const shouldTestInside = (element: ExcalidrawElement) => {
|
||||
return isDraggableFromInside || isImageElement(element);
|
||||
};
|
||||
|
||||
export type HitTestArgs<Point extends GlobalPoint | LocalPoint> = {
|
||||
x: number;
|
||||
y: number;
|
||||
export type HitTestArgs = {
|
||||
point: GlobalPoint;
|
||||
element: ExcalidrawElement;
|
||||
shape: GeometricShape<Point>;
|
||||
threshold?: number;
|
||||
frameNameBound?: FrameNameBounds | null;
|
||||
};
|
||||
|
||||
export const hitElementItself = <Point extends GlobalPoint | LocalPoint>({
|
||||
x,
|
||||
y,
|
||||
export const hitElementItself = ({
|
||||
point,
|
||||
element,
|
||||
shape,
|
||||
threshold = 10,
|
||||
frameNameBound = null,
|
||||
}: HitTestArgs<Point>) => {
|
||||
let hit = shouldTestInside(element)
|
||||
? // Since `inShape` tests STRICTLY againt the insides of a shape
|
||||
// we would need `onShape` as well to include the "borders"
|
||||
isPointInShape(pointFrom(x, y), shape) ||
|
||||
isPointOnShape(pointFrom(x, y), shape, threshold)
|
||||
: isPointOnShape(pointFrom(x, y), shape, threshold);
|
||||
}: HitTestArgs) => {
|
||||
// First check if the element is in the bounding box because it's MUCH faster
|
||||
// than checking if the point is in the element's shape
|
||||
let hit = hitElementBoundingBox(
|
||||
point,
|
||||
element,
|
||||
arrayToMap([element]),
|
||||
threshold,
|
||||
)
|
||||
? shouldTestInside(element)
|
||||
? // Since `inShape` tests STRICTLY againt the insides of a shape
|
||||
// we would need `onShape` as well to include the "borders"
|
||||
isPointInElement(point, element) ||
|
||||
isPointOnElementOutline(point, element, threshold)
|
||||
: isPointOnElementOutline(point, element, threshold)
|
||||
: false;
|
||||
|
||||
// hit test against a frame's name
|
||||
if (!hit && frameNameBound) {
|
||||
hit = isPointInShape(pointFrom(x, y), {
|
||||
type: "polygon",
|
||||
data: getPolygonShape(frameNameBound as ExcalidrawRectangleElement)
|
||||
.data as Polygon<Point>,
|
||||
});
|
||||
const x1 = frameNameBound.x - threshold;
|
||||
const y1 = frameNameBound.y - threshold;
|
||||
const x2 = frameNameBound.x + frameNameBound.width + threshold;
|
||||
const y2 = frameNameBound.y + frameNameBound.height + threshold;
|
||||
hit = isPointWithinBounds(pointFrom(x1, y1), point, pointFrom(x2, y2));
|
||||
}
|
||||
|
||||
return hit;
|
||||
};
|
||||
|
||||
export const hitElementBoundingBox = (
|
||||
x: number,
|
||||
y: number,
|
||||
point: GlobalPoint,
|
||||
element: ExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
tolerance = 0,
|
||||
@ -120,37 +133,45 @@ export const hitElementBoundingBox = (
|
||||
y1 -= tolerance;
|
||||
x2 += tolerance;
|
||||
y2 += tolerance;
|
||||
return isPointWithinBounds(
|
||||
pointFrom(x1, y1),
|
||||
pointFrom(x, y),
|
||||
pointFrom(x2, y2),
|
||||
);
|
||||
return isPointWithinBounds(pointFrom(x1, y1), point, pointFrom(x2, y2));
|
||||
};
|
||||
|
||||
export const hitElementBoundingBoxOnly = <
|
||||
Point extends GlobalPoint | LocalPoint,
|
||||
>(
|
||||
hitArgs: HitTestArgs<Point>,
|
||||
export const hitElementBoundingBoxOnly = (
|
||||
hitArgs: HitTestArgs,
|
||||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
return (
|
||||
!hitElementItself(hitArgs) &&
|
||||
// bound text is considered part of the element (even if it's outside the bounding box)
|
||||
!hitElementBoundText(
|
||||
hitArgs.x,
|
||||
hitArgs.y,
|
||||
getBoundTextShape(hitArgs.element, elementsMap),
|
||||
) &&
|
||||
hitElementBoundingBox(hitArgs.x, hitArgs.y, hitArgs.element, elementsMap)
|
||||
!hitElementBoundText(hitArgs.point, hitArgs.element, elementsMap) &&
|
||||
hitElementBoundingBox(hitArgs.point, hitArgs.element, elementsMap)
|
||||
);
|
||||
};
|
||||
|
||||
export const hitElementBoundText = <Point extends GlobalPoint | LocalPoint>(
|
||||
x: number,
|
||||
y: number,
|
||||
textShape: GeometricShape<Point> | null,
|
||||
export const hitElementBoundText = (
|
||||
point: GlobalPoint,
|
||||
element: ExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
): boolean => {
|
||||
return !!textShape && isPointInShape(pointFrom(x, y), textShape);
|
||||
const boundTextElementCandidate = getBoundTextElement(element, elementsMap);
|
||||
|
||||
if (!boundTextElementCandidate) {
|
||||
return false;
|
||||
}
|
||||
const boundTextElement = isLinearElement(element)
|
||||
? {
|
||||
...boundTextElementCandidate,
|
||||
// arrow's bound text accurate position is not stored in the element's property
|
||||
// but rather calculated and returned from the following static method
|
||||
...LinearElementEditor.getBoundTextElementPosition(
|
||||
element,
|
||||
boundTextElementCandidate,
|
||||
elementsMap,
|
||||
),
|
||||
}
|
||||
: boundTextElementCandidate;
|
||||
|
||||
return isPointInElement(point, boundTextElement);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -173,17 +194,33 @@ export const intersectElementWithLineSegment = (
|
||||
case "iframe":
|
||||
case "embeddable":
|
||||
case "frame":
|
||||
case "selection":
|
||||
case "magicframe":
|
||||
return intersectRectanguloidWithLineSegment(element, line, offset);
|
||||
case "diamond":
|
||||
return intersectDiamondWithLineSegment(element, line, offset);
|
||||
case "ellipse":
|
||||
return intersectEllipseWithLineSegment(element, line, offset);
|
||||
default:
|
||||
throw new Error(`Unimplemented element type '${element.type}'`);
|
||||
case "line":
|
||||
case "freedraw":
|
||||
case "arrow":
|
||||
return intersectLinearOrFreeDrawWithLineSegment(element, line);
|
||||
}
|
||||
};
|
||||
|
||||
const intersectLinearOrFreeDrawWithLineSegment = (
|
||||
element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
|
||||
segment: LineSegment<GlobalPoint>,
|
||||
): GlobalPoint[] => {
|
||||
const [lines, curves] = deconstructLinearOrFreeDrawElement(element);
|
||||
return [
|
||||
...lines
|
||||
.map((l) => lineSegmentIntersectionPoints(l, segment))
|
||||
.filter((p): p is GlobalPoint => p != null),
|
||||
...curves.flatMap((c) => curveIntersectLineSegment(c, segment)),
|
||||
].sort(pointDistanceSq);
|
||||
};
|
||||
|
||||
const intersectRectanguloidWithLineSegment = (
|
||||
element: ExcalidrawRectanguloidElement,
|
||||
l: LineSegment<GlobalPoint>,
|
||||
@ -301,8 +338,46 @@ const intersectEllipseWithLineSegment = (
|
||||
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
|
||||
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
|
||||
|
||||
return ellipseLineIntersectionPoints(
|
||||
return ellipseSegmentInterceptPoints(
|
||||
ellipse(center, element.width / 2 + offset, element.height / 2 + offset),
|
||||
line(rotatedA, rotatedB),
|
||||
lineSegment(rotatedA, rotatedB),
|
||||
).map((p) => pointRotateRads(p, center, element.angle));
|
||||
};
|
||||
|
||||
// check if the given point is considered on the given shape's border
|
||||
const isPointOnElementOutline = (
|
||||
point: GlobalPoint,
|
||||
element: ExcalidrawElement,
|
||||
tolerance = 1,
|
||||
) => distanceToElement(element, point) <= tolerance;
|
||||
|
||||
// check if the given point is considered inside the element's border
|
||||
export const isPointInElement = (
|
||||
point: GlobalPoint,
|
||||
element: ExcalidrawElement,
|
||||
) => {
|
||||
if (
|
||||
(isLinearElement(element) || isFreeDrawElement(element)) &&
|
||||
!isPathALoop(element.points)
|
||||
) {
|
||||
// There isn't any "inside" for a non-looping path
|
||||
return false;
|
||||
}
|
||||
|
||||
const [x1, y1, x2, y2] = getElementBounds(element, new Map());
|
||||
const center = pointFrom<GlobalPoint>((x1 + x2) / 2, (y1 + y2) / 2);
|
||||
const otherPoint = pointFromVector(
|
||||
vectorScale(
|
||||
vectorNormalize(vectorFromPoint(point, center, 0.1)),
|
||||
Math.max(element.width, element.height) * 2,
|
||||
),
|
||||
center,
|
||||
);
|
||||
const intersector = lineSegment(point, otherPoint);
|
||||
const intersections = intersectElementWithLineSegment(
|
||||
element,
|
||||
intersector,
|
||||
).filter((p, pos, arr) => arr.findIndex((q) => pointsEqual(q, p)) === pos);
|
||||
|
||||
return intersections.length % 2 === 1;
|
||||
};
|
||||
|
@ -12,21 +12,25 @@ import type { GlobalPoint, Radians } from "@excalidraw/math";
|
||||
|
||||
import {
|
||||
deconstructDiamondElement,
|
||||
deconstructLinearOrFreeDrawElement,
|
||||
deconstructRectanguloidElement,
|
||||
} from "./utils";
|
||||
|
||||
import type {
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawDiamondElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawEllipseElement,
|
||||
ExcalidrawFreeDrawElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawRectanguloidElement,
|
||||
} from "./types";
|
||||
|
||||
export const distanceToBindableElement = (
|
||||
element: ExcalidrawBindableElement,
|
||||
export const distanceToElement = (
|
||||
element: ExcalidrawElement,
|
||||
p: GlobalPoint,
|
||||
): number => {
|
||||
switch (element.type) {
|
||||
case "selection":
|
||||
case "rectangle":
|
||||
case "image":
|
||||
case "text":
|
||||
@ -39,6 +43,10 @@ export const distanceToBindableElement = (
|
||||
return distanceToDiamondElement(element, p);
|
||||
case "ellipse":
|
||||
return distanceToEllipseElement(element, p);
|
||||
case "line":
|
||||
case "arrow":
|
||||
case "freedraw":
|
||||
return distanceToLinearOrFreeDraElement(element, p);
|
||||
}
|
||||
};
|
||||
|
||||
@ -117,3 +125,14 @@ const distanceToEllipseElement = (
|
||||
ellipse(center, element.width / 2, element.height / 2),
|
||||
);
|
||||
};
|
||||
|
||||
const distanceToLinearOrFreeDraElement = (
|
||||
element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
|
||||
p: GlobalPoint,
|
||||
) => {
|
||||
const [lines, curves] = deconstructLinearOrFreeDrawElement(element);
|
||||
return Math.min(
|
||||
...lines.map((s) => distanceToLineSegment(p, s)),
|
||||
...curves.map((a) => curvePointDistance(a, p)),
|
||||
);
|
||||
};
|
||||
|
@ -29,10 +29,9 @@ import {
|
||||
FIXED_BINDING_DISTANCE,
|
||||
getHeadingForElbowArrowSnap,
|
||||
getGlobalFixedPointForBindableElement,
|
||||
snapToMid,
|
||||
getHoveredElementForBinding,
|
||||
} from "./binding";
|
||||
import { distanceToBindableElement } from "./distance";
|
||||
import { distanceToElement } from "./distance";
|
||||
import {
|
||||
compareHeading,
|
||||
flipHeading,
|
||||
@ -898,50 +897,6 @@ export const updateElbowArrowPoints = (
|
||||
return { points: updates.points ?? arrow.points };
|
||||
}
|
||||
|
||||
// NOTE (mtolmacs): This is a temporary check to ensure that the incoming elbow
|
||||
// arrow size is valid. This check will be removed once the issue is identified
|
||||
if (
|
||||
arrow.x < -MAX_POS ||
|
||||
arrow.x > MAX_POS ||
|
||||
arrow.y < -MAX_POS ||
|
||||
arrow.y > MAX_POS ||
|
||||
arrow.x + (updates?.points?.[updates?.points?.length - 1]?.[0] ?? 0) <
|
||||
-MAX_POS ||
|
||||
arrow.x + (updates?.points?.[updates?.points?.length - 1]?.[0] ?? 0) >
|
||||
MAX_POS ||
|
||||
arrow.y + (updates?.points?.[updates?.points?.length - 1]?.[1] ?? 0) <
|
||||
-MAX_POS ||
|
||||
arrow.y + (updates?.points?.[updates?.points?.length - 1]?.[1] ?? 0) >
|
||||
MAX_POS ||
|
||||
arrow.x + (arrow?.points?.[arrow?.points?.length - 1]?.[0] ?? 0) <
|
||||
-MAX_POS ||
|
||||
arrow.x + (arrow?.points?.[arrow?.points?.length - 1]?.[0] ?? 0) >
|
||||
MAX_POS ||
|
||||
arrow.y + (arrow?.points?.[arrow?.points?.length - 1]?.[1] ?? 0) <
|
||||
-MAX_POS ||
|
||||
arrow.y + (arrow?.points?.[arrow?.points?.length - 1]?.[1] ?? 0) > MAX_POS
|
||||
) {
|
||||
console.error(
|
||||
"Elbow arrow (or update) is outside reasonable bounds (> 1e6)",
|
||||
{
|
||||
arrow,
|
||||
updates,
|
||||
},
|
||||
);
|
||||
}
|
||||
// @ts-ignore See above note
|
||||
arrow.x = clamp(arrow.x, -MAX_POS, MAX_POS);
|
||||
// @ts-ignore See above note
|
||||
arrow.y = clamp(arrow.y, -MAX_POS, MAX_POS);
|
||||
if (updates.points) {
|
||||
updates.points = updates.points.map(([x, y]) =>
|
||||
pointFrom<LocalPoint>(
|
||||
clamp(x, -MAX_POS, MAX_POS),
|
||||
clamp(y, -MAX_POS, MAX_POS),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!import.meta.env.PROD) {
|
||||
invariant(
|
||||
!updates.points || updates.points.length >= 2,
|
||||
@ -974,6 +929,25 @@ export const updateElbowArrowPoints = (
|
||||
),
|
||||
"Elbow arrow segments must be either horizontal or vertical",
|
||||
);
|
||||
|
||||
invariant(
|
||||
updates.fixedSegments?.find(
|
||||
(segment) =>
|
||||
segment.index === 1 &&
|
||||
pointsEqual(segment.start, (updates.points ?? arrow.points)[0]),
|
||||
) == null &&
|
||||
updates.fixedSegments?.find(
|
||||
(segment) =>
|
||||
segment.index === (updates.points ?? arrow.points).length - 1 &&
|
||||
pointsEqual(
|
||||
segment.end,
|
||||
(updates.points ?? arrow.points)[
|
||||
(updates.points ?? arrow.points).length - 1
|
||||
],
|
||||
),
|
||||
) == null,
|
||||
"The first and last segments cannot be fixed",
|
||||
);
|
||||
}
|
||||
|
||||
const fixedSegments = updates.fixedSegments ?? arrow.fixedSegments ?? [];
|
||||
@ -2214,13 +2188,7 @@ const getGlobalPoint = (
|
||||
): GlobalPoint => {
|
||||
if (isDragging) {
|
||||
if (element) {
|
||||
const snapPoint = bindPointToSnapToElementOutline(
|
||||
arrow,
|
||||
element,
|
||||
startOrEnd,
|
||||
);
|
||||
|
||||
return snapToMid(element, snapPoint);
|
||||
return bindPointToSnapToElementOutline(arrow, element, startOrEnd);
|
||||
}
|
||||
|
||||
return initialPoint;
|
||||
@ -2234,8 +2202,7 @@ const getGlobalPoint = (
|
||||
|
||||
// NOTE: Resize scales the binding position point too, so we need to update it
|
||||
return Math.abs(
|
||||
distanceToBindableElement(element, fixedGlobalPoint) -
|
||||
FIXED_BINDING_DISTANCE,
|
||||
distanceToElement(element, fixedGlobalPoint) - FIXED_BINDING_DISTANCE,
|
||||
) > 0.01
|
||||
? bindPointToSnapToElementOutline(arrow, element, startOrEnd)
|
||||
: fixedGlobalPoint;
|
||||
@ -2257,7 +2224,7 @@ const getBindPointHeading = (
|
||||
hoveredElement &&
|
||||
aabbForElement(
|
||||
hoveredElement,
|
||||
Array(4).fill(distanceToBindableElement(hoveredElement, p)) as [
|
||||
Array(4).fill(distanceToElement(hoveredElement, p)) as [
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
|
@ -195,7 +195,8 @@ export type ExcalidrawRectanguloidElement =
|
||||
| ExcalidrawFreeDrawElement
|
||||
| ExcalidrawIframeLikeElement
|
||||
| ExcalidrawFrameLikeElement
|
||||
| ExcalidrawEmbeddableElement;
|
||||
| ExcalidrawEmbeddableElement
|
||||
| ExcalidrawSelectionElement;
|
||||
|
||||
/**
|
||||
* ExcalidrawElement should be JSON serializable and (eventually) contain
|
||||
|
@ -1,28 +1,166 @@
|
||||
import {
|
||||
curve,
|
||||
curveCatmullRomCubicApproxPoints,
|
||||
curveOffsetPoints,
|
||||
lineSegment,
|
||||
pointFrom,
|
||||
pointFromVector,
|
||||
pointFromArray,
|
||||
rectangle,
|
||||
vectorFromPoint,
|
||||
vectorNormalize,
|
||||
vectorScale,
|
||||
type GlobalPoint,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import { elementCenterPoint } from "@excalidraw/common";
|
||||
|
||||
import type { Curve, LineSegment } from "@excalidraw/math";
|
||||
import type { Curve, LineSegment, LocalPoint } from "@excalidraw/math";
|
||||
|
||||
import { getCornerRadius } from "./shapes";
|
||||
|
||||
import { getDiamondPoints } from "./bounds";
|
||||
|
||||
import { generateLinearCollisionShape } from "./Shape";
|
||||
|
||||
import type {
|
||||
ExcalidrawDiamondElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawFreeDrawElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawRectanguloidElement,
|
||||
} from "./types";
|
||||
|
||||
type ElementShape = [LineSegment<GlobalPoint>[], Curve<GlobalPoint>[]];
|
||||
|
||||
const ElementShapesCache = new WeakMap<
|
||||
ExcalidrawElement,
|
||||
{ version: ExcalidrawElement["version"]; shapes: Map<number, ElementShape> }
|
||||
>();
|
||||
|
||||
const getElementShapesCacheEntry = <T extends ExcalidrawElement>(
|
||||
element: T,
|
||||
offset: number,
|
||||
): ElementShape | undefined => {
|
||||
const record = ElementShapesCache.get(element);
|
||||
|
||||
if (!record) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { version, shapes } = record;
|
||||
|
||||
if (version !== element.version) {
|
||||
ElementShapesCache.delete(element);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return shapes.get(offset);
|
||||
};
|
||||
|
||||
const setElementShapesCacheEntry = <T extends ExcalidrawElement>(
|
||||
element: T,
|
||||
shape: ElementShape,
|
||||
offset: number,
|
||||
) => {
|
||||
const record = ElementShapesCache.get(element);
|
||||
|
||||
if (!record) {
|
||||
ElementShapesCache.set(element, {
|
||||
version: element.version,
|
||||
shapes: new Map([[offset, shape]]),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { version, shapes } = record;
|
||||
|
||||
if (version !== element.version) {
|
||||
ElementShapesCache.set(element, {
|
||||
version: element.version,
|
||||
shapes: new Map([[offset, shape]]),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
shapes.set(offset, shape);
|
||||
};
|
||||
|
||||
export function deconstructLinearOrFreeDrawElement(
|
||||
element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
|
||||
): [LineSegment<GlobalPoint>[], Curve<GlobalPoint>[]] {
|
||||
const cachedShape = getElementShapesCacheEntry(element, 0);
|
||||
|
||||
if (cachedShape) {
|
||||
return cachedShape;
|
||||
}
|
||||
|
||||
const ops = generateLinearCollisionShape(element) as {
|
||||
op: string;
|
||||
data: number[];
|
||||
}[];
|
||||
const lines = [];
|
||||
const curves = [];
|
||||
|
||||
for (let idx = 0; idx < ops.length; idx += 1) {
|
||||
const op = ops[idx];
|
||||
const prevPoint =
|
||||
ops[idx - 1] && pointFromArray<LocalPoint>(ops[idx - 1].data.slice(-2));
|
||||
switch (op.op) {
|
||||
case "move":
|
||||
continue;
|
||||
case "lineTo":
|
||||
if (!prevPoint) {
|
||||
throw new Error("prevPoint is undefined");
|
||||
}
|
||||
|
||||
lines.push(
|
||||
lineSegment<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + prevPoint[0],
|
||||
element.y + prevPoint[1],
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + op.data[0],
|
||||
element.y + op.data[1],
|
||||
),
|
||||
),
|
||||
);
|
||||
continue;
|
||||
case "bcurveTo":
|
||||
if (!prevPoint) {
|
||||
throw new Error("prevPoint is undefined");
|
||||
}
|
||||
|
||||
curves.push(
|
||||
curve<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + prevPoint[0],
|
||||
element.y + prevPoint[1],
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + op.data[0],
|
||||
element.y + op.data[1],
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + op.data[2],
|
||||
element.y + op.data[3],
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + op.data[4],
|
||||
element.y + op.data[5],
|
||||
),
|
||||
),
|
||||
);
|
||||
continue;
|
||||
default: {
|
||||
console.error("Unknown op type", op.op);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const shape = [lines, curves] as ElementShape;
|
||||
setElementShapesCacheEntry(element, shape, 0);
|
||||
|
||||
return shape;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the building components of a rectanguloid element in the form of
|
||||
* line segments and curves.
|
||||
@ -35,175 +173,132 @@ export function deconstructRectanguloidElement(
|
||||
element: ExcalidrawRectanguloidElement,
|
||||
offset: number = 0,
|
||||
): [LineSegment<GlobalPoint>[], Curve<GlobalPoint>[]] {
|
||||
const roundness = getCornerRadius(
|
||||
const cachedShape = getElementShapesCacheEntry(element, offset);
|
||||
|
||||
if (cachedShape) {
|
||||
return cachedShape;
|
||||
}
|
||||
|
||||
let radius = getCornerRadius(
|
||||
Math.min(element.width, element.height),
|
||||
element,
|
||||
);
|
||||
|
||||
if (roundness <= 0) {
|
||||
const r = rectangle(
|
||||
pointFrom(element.x - offset, element.y - offset),
|
||||
pointFrom(
|
||||
element.x + element.width + offset,
|
||||
element.y + element.height + offset,
|
||||
),
|
||||
);
|
||||
|
||||
const top = lineSegment<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(r[0][0] + roundness, r[0][1]),
|
||||
pointFrom<GlobalPoint>(r[1][0] - roundness, r[0][1]),
|
||||
);
|
||||
const right = lineSegment<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(r[1][0], r[0][1] + roundness),
|
||||
pointFrom<GlobalPoint>(r[1][0], r[1][1] - roundness),
|
||||
);
|
||||
const bottom = lineSegment<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(r[0][0] + roundness, r[1][1]),
|
||||
pointFrom<GlobalPoint>(r[1][0] - roundness, r[1][1]),
|
||||
);
|
||||
const left = lineSegment<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(r[0][0], r[1][1] - roundness),
|
||||
pointFrom<GlobalPoint>(r[0][0], r[0][1] + roundness),
|
||||
);
|
||||
const sides = [top, right, bottom, left];
|
||||
|
||||
return [sides, []];
|
||||
if (radius === 0) {
|
||||
radius = 0.01;
|
||||
}
|
||||
|
||||
const center = elementCenterPoint(element);
|
||||
|
||||
const r = rectangle(
|
||||
pointFrom(element.x, element.y),
|
||||
pointFrom(element.x + element.width, element.y + element.height),
|
||||
);
|
||||
|
||||
const top = lineSegment<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(r[0][0] + roundness, r[0][1]),
|
||||
pointFrom<GlobalPoint>(r[1][0] - roundness, r[0][1]),
|
||||
pointFrom<GlobalPoint>(r[0][0] + radius, r[0][1]),
|
||||
pointFrom<GlobalPoint>(r[1][0] - radius, r[0][1]),
|
||||
);
|
||||
const right = lineSegment<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(r[1][0], r[0][1] + roundness),
|
||||
pointFrom<GlobalPoint>(r[1][0], r[1][1] - roundness),
|
||||
pointFrom<GlobalPoint>(r[1][0], r[0][1] + radius),
|
||||
pointFrom<GlobalPoint>(r[1][0], r[1][1] - radius),
|
||||
);
|
||||
const bottom = lineSegment<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(r[0][0] + roundness, r[1][1]),
|
||||
pointFrom<GlobalPoint>(r[1][0] - roundness, r[1][1]),
|
||||
pointFrom<GlobalPoint>(r[0][0] + radius, r[1][1]),
|
||||
pointFrom<GlobalPoint>(r[1][0] - radius, r[1][1]),
|
||||
);
|
||||
const left = lineSegment<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(r[0][0], r[1][1] - roundness),
|
||||
pointFrom<GlobalPoint>(r[0][0], r[0][1] + roundness),
|
||||
pointFrom<GlobalPoint>(r[0][0], r[1][1] - radius),
|
||||
pointFrom<GlobalPoint>(r[0][0], r[0][1] + radius),
|
||||
);
|
||||
|
||||
const offsets = [
|
||||
vectorScale(
|
||||
vectorNormalize(
|
||||
vectorFromPoint(pointFrom(r[0][0] - offset, r[0][1] - offset), center),
|
||||
),
|
||||
offset,
|
||||
), // TOP LEFT
|
||||
vectorScale(
|
||||
vectorNormalize(
|
||||
vectorFromPoint(pointFrom(r[1][0] + offset, r[0][1] - offset), center),
|
||||
),
|
||||
offset,
|
||||
), //TOP RIGHT
|
||||
vectorScale(
|
||||
vectorNormalize(
|
||||
vectorFromPoint(pointFrom(r[1][0] + offset, r[1][1] + offset), center),
|
||||
),
|
||||
offset,
|
||||
), // BOTTOM RIGHT
|
||||
vectorScale(
|
||||
vectorNormalize(
|
||||
vectorFromPoint(pointFrom(r[0][0] - offset, r[1][1] + offset), center),
|
||||
),
|
||||
offset,
|
||||
), // BOTTOM LEFT
|
||||
];
|
||||
|
||||
const corners = [
|
||||
const baseCorners = [
|
||||
curve(
|
||||
pointFromVector(offsets[0], left[1]),
|
||||
pointFromVector(
|
||||
offsets[0],
|
||||
pointFrom<GlobalPoint>(
|
||||
left[1][0] + (2 / 3) * (r[0][0] - left[1][0]),
|
||||
left[1][1] + (2 / 3) * (r[0][1] - left[1][1]),
|
||||
),
|
||||
left[1],
|
||||
pointFrom<GlobalPoint>(
|
||||
left[1][0] + (2 / 3) * (r[0][0] - left[1][0]),
|
||||
left[1][1] + (2 / 3) * (r[0][1] - left[1][1]),
|
||||
),
|
||||
pointFromVector(
|
||||
offsets[0],
|
||||
pointFrom<GlobalPoint>(
|
||||
top[0][0] + (2 / 3) * (r[0][0] - top[0][0]),
|
||||
top[0][1] + (2 / 3) * (r[0][1] - top[0][1]),
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
top[0][0] + (2 / 3) * (r[0][0] - top[0][0]),
|
||||
top[0][1] + (2 / 3) * (r[0][1] - top[0][1]),
|
||||
),
|
||||
pointFromVector(offsets[0], top[0]),
|
||||
top[0],
|
||||
), // TOP LEFT
|
||||
curve(
|
||||
pointFromVector(offsets[1], top[1]),
|
||||
pointFromVector(
|
||||
offsets[1],
|
||||
pointFrom<GlobalPoint>(
|
||||
top[1][0] + (2 / 3) * (r[1][0] - top[1][0]),
|
||||
top[1][1] + (2 / 3) * (r[0][1] - top[1][1]),
|
||||
),
|
||||
top[1],
|
||||
pointFrom<GlobalPoint>(
|
||||
top[1][0] + (2 / 3) * (r[1][0] - top[1][0]),
|
||||
top[1][1] + (2 / 3) * (r[0][1] - top[1][1]),
|
||||
),
|
||||
pointFromVector(
|
||||
offsets[1],
|
||||
pointFrom<GlobalPoint>(
|
||||
right[0][0] + (2 / 3) * (r[1][0] - right[0][0]),
|
||||
right[0][1] + (2 / 3) * (r[0][1] - right[0][1]),
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
right[0][0] + (2 / 3) * (r[1][0] - right[0][0]),
|
||||
right[0][1] + (2 / 3) * (r[0][1] - right[0][1]),
|
||||
),
|
||||
pointFromVector(offsets[1], right[0]),
|
||||
right[0],
|
||||
), // TOP RIGHT
|
||||
curve(
|
||||
pointFromVector(offsets[2], right[1]),
|
||||
pointFromVector(
|
||||
offsets[2],
|
||||
pointFrom<GlobalPoint>(
|
||||
right[1][0] + (2 / 3) * (r[1][0] - right[1][0]),
|
||||
right[1][1] + (2 / 3) * (r[1][1] - right[1][1]),
|
||||
),
|
||||
right[1],
|
||||
pointFrom<GlobalPoint>(
|
||||
right[1][0] + (2 / 3) * (r[1][0] - right[1][0]),
|
||||
right[1][1] + (2 / 3) * (r[1][1] - right[1][1]),
|
||||
),
|
||||
pointFromVector(
|
||||
offsets[2],
|
||||
pointFrom<GlobalPoint>(
|
||||
bottom[1][0] + (2 / 3) * (r[1][0] - bottom[1][0]),
|
||||
bottom[1][1] + (2 / 3) * (r[1][1] - bottom[1][1]),
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
bottom[1][0] + (2 / 3) * (r[1][0] - bottom[1][0]),
|
||||
bottom[1][1] + (2 / 3) * (r[1][1] - bottom[1][1]),
|
||||
),
|
||||
pointFromVector(offsets[2], bottom[1]),
|
||||
bottom[1],
|
||||
), // BOTTOM RIGHT
|
||||
curve(
|
||||
pointFromVector(offsets[3], bottom[0]),
|
||||
pointFromVector(
|
||||
offsets[3],
|
||||
pointFrom<GlobalPoint>(
|
||||
bottom[0][0] + (2 / 3) * (r[0][0] - bottom[0][0]),
|
||||
bottom[0][1] + (2 / 3) * (r[1][1] - bottom[0][1]),
|
||||
),
|
||||
bottom[0],
|
||||
pointFrom<GlobalPoint>(
|
||||
bottom[0][0] + (2 / 3) * (r[0][0] - bottom[0][0]),
|
||||
bottom[0][1] + (2 / 3) * (r[1][1] - bottom[0][1]),
|
||||
),
|
||||
pointFromVector(
|
||||
offsets[3],
|
||||
pointFrom<GlobalPoint>(
|
||||
left[0][0] + (2 / 3) * (r[0][0] - left[0][0]),
|
||||
left[0][1] + (2 / 3) * (r[1][1] - left[0][1]),
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
left[0][0] + (2 / 3) * (r[0][0] - left[0][0]),
|
||||
left[0][1] + (2 / 3) * (r[1][1] - left[0][1]),
|
||||
),
|
||||
pointFromVector(offsets[3], left[0]),
|
||||
left[0],
|
||||
), // BOTTOM LEFT
|
||||
];
|
||||
|
||||
const sides = [
|
||||
lineSegment<GlobalPoint>(corners[0][3], corners[1][0]),
|
||||
lineSegment<GlobalPoint>(corners[1][3], corners[2][0]),
|
||||
lineSegment<GlobalPoint>(corners[2][3], corners[3][0]),
|
||||
lineSegment<GlobalPoint>(corners[3][3], corners[0][0]),
|
||||
];
|
||||
const corners =
|
||||
offset > 0
|
||||
? baseCorners.map(
|
||||
(corner) =>
|
||||
curveCatmullRomCubicApproxPoints(
|
||||
curveOffsetPoints(corner, offset),
|
||||
)!,
|
||||
)
|
||||
: [
|
||||
[baseCorners[0]],
|
||||
[baseCorners[1]],
|
||||
[baseCorners[2]],
|
||||
[baseCorners[3]],
|
||||
];
|
||||
|
||||
return [sides, corners];
|
||||
const sides = [
|
||||
lineSegment<GlobalPoint>(
|
||||
corners[0][corners[0].length - 1][3],
|
||||
corners[1][0][0],
|
||||
),
|
||||
lineSegment<GlobalPoint>(
|
||||
corners[1][corners[1].length - 1][3],
|
||||
corners[2][0][0],
|
||||
),
|
||||
lineSegment<GlobalPoint>(
|
||||
corners[2][corners[2].length - 1][3],
|
||||
corners[3][0][0],
|
||||
),
|
||||
lineSegment<GlobalPoint>(
|
||||
corners[3][corners[3].length - 1][3],
|
||||
corners[0][0][0],
|
||||
),
|
||||
];
|
||||
const shape = [sides, corners.flat()] as ElementShape;
|
||||
|
||||
setElementShapesCacheEntry(element, shape, offset);
|
||||
|
||||
return shape;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -218,42 +313,20 @@ export function deconstructDiamondElement(
|
||||
element: ExcalidrawDiamondElement,
|
||||
offset: number = 0,
|
||||
): [LineSegment<GlobalPoint>[], Curve<GlobalPoint>[]] {
|
||||
const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
|
||||
getDiamondPoints(element);
|
||||
const verticalRadius = getCornerRadius(Math.abs(topX - leftX), element);
|
||||
const horizontalRadius = getCornerRadius(Math.abs(rightY - topY), element);
|
||||
const cachedShape = getElementShapesCacheEntry(element, offset);
|
||||
|
||||
if (element.roundness?.type == null) {
|
||||
const [top, right, bottom, left]: GlobalPoint[] = [
|
||||
pointFrom(element.x + topX, element.y + topY - offset),
|
||||
pointFrom(element.x + rightX + offset, element.y + rightY),
|
||||
pointFrom(element.x + bottomX, element.y + bottomY + offset),
|
||||
pointFrom(element.x + leftX - offset, element.y + leftY),
|
||||
];
|
||||
|
||||
// Create the line segment parts of the diamond
|
||||
// NOTE: Horizontal and vertical seems to be flipped here
|
||||
const topRight = lineSegment<GlobalPoint>(
|
||||
pointFrom(top[0] + verticalRadius, top[1] + horizontalRadius),
|
||||
pointFrom(right[0] - verticalRadius, right[1] - horizontalRadius),
|
||||
);
|
||||
const bottomRight = lineSegment<GlobalPoint>(
|
||||
pointFrom(right[0] - verticalRadius, right[1] + horizontalRadius),
|
||||
pointFrom(bottom[0] + verticalRadius, bottom[1] - horizontalRadius),
|
||||
);
|
||||
const bottomLeft = lineSegment<GlobalPoint>(
|
||||
pointFrom(bottom[0] - verticalRadius, bottom[1] - horizontalRadius),
|
||||
pointFrom(left[0] + verticalRadius, left[1] + horizontalRadius),
|
||||
);
|
||||
const topLeft = lineSegment<GlobalPoint>(
|
||||
pointFrom(left[0] + verticalRadius, left[1] - horizontalRadius),
|
||||
pointFrom(top[0] - verticalRadius, top[1] + horizontalRadius),
|
||||
);
|
||||
|
||||
return [[topRight, bottomRight, bottomLeft, topLeft], []];
|
||||
if (cachedShape) {
|
||||
return cachedShape;
|
||||
}
|
||||
|
||||
const center = elementCenterPoint(element);
|
||||
const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
|
||||
getDiamondPoints(element);
|
||||
const verticalRadius = element.roundness
|
||||
? getCornerRadius(Math.abs(topX - leftX), element)
|
||||
: (topX - leftX) * 0.01;
|
||||
const horizontalRadius = element.roundness
|
||||
? getCornerRadius(Math.abs(rightY - topY), element)
|
||||
: (rightY - topY) * 0.01;
|
||||
|
||||
const [top, right, bottom, left]: GlobalPoint[] = [
|
||||
pointFrom(element.x + topX, element.y + topY),
|
||||
@ -262,94 +335,94 @@ export function deconstructDiamondElement(
|
||||
pointFrom(element.x + leftX, element.y + leftY),
|
||||
];
|
||||
|
||||
const offsets = [
|
||||
vectorScale(vectorNormalize(vectorFromPoint(right, center)), offset), // RIGHT
|
||||
vectorScale(vectorNormalize(vectorFromPoint(bottom, center)), offset), // BOTTOM
|
||||
vectorScale(vectorNormalize(vectorFromPoint(left, center)), offset), // LEFT
|
||||
vectorScale(vectorNormalize(vectorFromPoint(top, center)), offset), // TOP
|
||||
];
|
||||
|
||||
const corners = [
|
||||
const baseCorners = [
|
||||
curve(
|
||||
pointFromVector(
|
||||
offsets[0],
|
||||
pointFrom<GlobalPoint>(
|
||||
right[0] - verticalRadius,
|
||||
right[1] - horizontalRadius,
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
right[0] - verticalRadius,
|
||||
right[1] - horizontalRadius,
|
||||
),
|
||||
pointFromVector(offsets[0], right),
|
||||
pointFromVector(offsets[0], right),
|
||||
pointFromVector(
|
||||
offsets[0],
|
||||
pointFrom<GlobalPoint>(
|
||||
right[0] - verticalRadius,
|
||||
right[1] + horizontalRadius,
|
||||
),
|
||||
right,
|
||||
right,
|
||||
pointFrom<GlobalPoint>(
|
||||
right[0] - verticalRadius,
|
||||
right[1] + horizontalRadius,
|
||||
),
|
||||
), // RIGHT
|
||||
curve(
|
||||
pointFromVector(
|
||||
offsets[1],
|
||||
pointFrom<GlobalPoint>(
|
||||
bottom[0] + verticalRadius,
|
||||
bottom[1] - horizontalRadius,
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
bottom[0] + verticalRadius,
|
||||
bottom[1] - horizontalRadius,
|
||||
),
|
||||
pointFromVector(offsets[1], bottom),
|
||||
pointFromVector(offsets[1], bottom),
|
||||
pointFromVector(
|
||||
offsets[1],
|
||||
pointFrom<GlobalPoint>(
|
||||
bottom[0] - verticalRadius,
|
||||
bottom[1] - horizontalRadius,
|
||||
),
|
||||
bottom,
|
||||
bottom,
|
||||
pointFrom<GlobalPoint>(
|
||||
bottom[0] - verticalRadius,
|
||||
bottom[1] - horizontalRadius,
|
||||
),
|
||||
), // BOTTOM
|
||||
curve(
|
||||
pointFromVector(
|
||||
offsets[2],
|
||||
pointFrom<GlobalPoint>(
|
||||
left[0] + verticalRadius,
|
||||
left[1] + horizontalRadius,
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
left[0] + verticalRadius,
|
||||
left[1] + horizontalRadius,
|
||||
),
|
||||
pointFromVector(offsets[2], left),
|
||||
pointFromVector(offsets[2], left),
|
||||
pointFromVector(
|
||||
offsets[2],
|
||||
pointFrom<GlobalPoint>(
|
||||
left[0] + verticalRadius,
|
||||
left[1] - horizontalRadius,
|
||||
),
|
||||
left,
|
||||
left,
|
||||
pointFrom<GlobalPoint>(
|
||||
left[0] + verticalRadius,
|
||||
left[1] - horizontalRadius,
|
||||
),
|
||||
), // LEFT
|
||||
curve(
|
||||
pointFromVector(
|
||||
offsets[3],
|
||||
pointFrom<GlobalPoint>(
|
||||
top[0] - verticalRadius,
|
||||
top[1] + horizontalRadius,
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
top[0] - verticalRadius,
|
||||
top[1] + horizontalRadius,
|
||||
),
|
||||
pointFromVector(offsets[3], top),
|
||||
pointFromVector(offsets[3], top),
|
||||
pointFromVector(
|
||||
offsets[3],
|
||||
pointFrom<GlobalPoint>(
|
||||
top[0] + verticalRadius,
|
||||
top[1] + horizontalRadius,
|
||||
),
|
||||
top,
|
||||
top,
|
||||
pointFrom<GlobalPoint>(
|
||||
top[0] + verticalRadius,
|
||||
top[1] + horizontalRadius,
|
||||
),
|
||||
), // TOP
|
||||
];
|
||||
|
||||
const corners =
|
||||
offset > 0
|
||||
? baseCorners.map(
|
||||
(corner) =>
|
||||
curveCatmullRomCubicApproxPoints(
|
||||
curveOffsetPoints(corner, offset),
|
||||
)!,
|
||||
)
|
||||
: [
|
||||
[baseCorners[0]],
|
||||
[baseCorners[1]],
|
||||
[baseCorners[2]],
|
||||
[baseCorners[3]],
|
||||
];
|
||||
|
||||
const sides = [
|
||||
lineSegment<GlobalPoint>(corners[0][3], corners[1][0]),
|
||||
lineSegment<GlobalPoint>(corners[1][3], corners[2][0]),
|
||||
lineSegment<GlobalPoint>(corners[2][3], corners[3][0]),
|
||||
lineSegment<GlobalPoint>(corners[3][3], corners[0][0]),
|
||||
lineSegment<GlobalPoint>(
|
||||
corners[0][corners[0].length - 1][3],
|
||||
corners[1][0][0],
|
||||
),
|
||||
lineSegment<GlobalPoint>(
|
||||
corners[1][corners[1].length - 1][3],
|
||||
corners[2][0][0],
|
||||
),
|
||||
lineSegment<GlobalPoint>(
|
||||
corners[2][corners[2].length - 1][3],
|
||||
corners[3][0][0],
|
||||
),
|
||||
lineSegment<GlobalPoint>(
|
||||
corners[3][corners[3].length - 1][3],
|
||||
corners[0][0][0],
|
||||
),
|
||||
];
|
||||
|
||||
return [sides, corners];
|
||||
const shape = [sides, corners.flat()] as ElementShape;
|
||||
|
||||
setElementShapesCacheEntry(element, shape, offset);
|
||||
|
||||
return shape;
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ const createAndSelectTwoRectangles = () => {
|
||||
// The second rectangle is already reselected because it was the last element created
|
||||
mouse.reset();
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.moveTo(10, 0);
|
||||
mouse.click();
|
||||
});
|
||||
};
|
||||
@ -52,6 +53,7 @@ const createAndSelectTwoRectanglesWithDifferentSizes = () => {
|
||||
// The second rectangle is already reselected because it was the last element created
|
||||
mouse.reset();
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.moveTo(10, 0);
|
||||
mouse.click();
|
||||
});
|
||||
};
|
||||
@ -202,6 +204,7 @@ describe("aligning", () => {
|
||||
// The second rectangle is already reselected because it was the last element created
|
||||
mouse.reset();
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.moveTo(10, 0);
|
||||
mouse.click();
|
||||
});
|
||||
|
||||
@ -215,6 +218,7 @@ describe("aligning", () => {
|
||||
// Add the created group to the current selection
|
||||
mouse.restorePosition(0, 0);
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.moveTo(10, 0);
|
||||
mouse.click();
|
||||
});
|
||||
};
|
||||
@ -316,6 +320,7 @@ describe("aligning", () => {
|
||||
// The second rectangle is already selected because it was the last element created
|
||||
mouse.reset();
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.moveTo(10, 0);
|
||||
mouse.click();
|
||||
});
|
||||
|
||||
@ -330,7 +335,7 @@ describe("aligning", () => {
|
||||
mouse.down();
|
||||
mouse.up(100, 100);
|
||||
|
||||
mouse.restorePosition(200, 200);
|
||||
mouse.restorePosition(210, 200);
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.click();
|
||||
});
|
||||
@ -341,6 +346,7 @@ describe("aligning", () => {
|
||||
// The second group is already selected because it was the last group created
|
||||
mouse.reset();
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.moveTo(10, 0);
|
||||
mouse.click();
|
||||
});
|
||||
};
|
||||
@ -454,6 +460,7 @@ describe("aligning", () => {
|
||||
// The second rectangle is already reselected because it was the last element created
|
||||
mouse.reset();
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.moveTo(10, 0);
|
||||
mouse.click();
|
||||
});
|
||||
|
||||
@ -466,7 +473,7 @@ describe("aligning", () => {
|
||||
mouse.up(100, 100);
|
||||
|
||||
// Add group to current selection
|
||||
mouse.restorePosition(0, 0);
|
||||
mouse.restorePosition(10, 0);
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.click();
|
||||
});
|
||||
@ -482,6 +489,7 @@ describe("aligning", () => {
|
||||
// Select the nested group, the rectangle is already selected
|
||||
mouse.reset();
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.moveTo(10, 0);
|
||||
mouse.click();
|
||||
});
|
||||
};
|
||||
|
@ -172,13 +172,13 @@ describe("element binding", () => {
|
||||
const arrow = UI.createElement("arrow", {
|
||||
x: 0,
|
||||
y: 0,
|
||||
size: 50,
|
||||
size: 49,
|
||||
});
|
||||
|
||||
expect(arrow.endBinding).toBe(null);
|
||||
|
||||
mouse.downAt(50, 50);
|
||||
mouse.moveTo(51, 0);
|
||||
mouse.moveTo(57, 0);
|
||||
mouse.up(0, 0);
|
||||
|
||||
// Test sticky connection
|
||||
|
@ -1262,7 +1262,7 @@ describe("Test Linear Elements", () => {
|
||||
mouse.downAt(rect.x, rect.y);
|
||||
mouse.moveTo(200, 0);
|
||||
mouse.upAt(200, 0);
|
||||
expect(arrow.width).toBeCloseTo(204, 0);
|
||||
expect(arrow.width).toBeCloseTo(200, 0);
|
||||
expect(rect.x).toBe(200);
|
||||
expect(rect.y).toBe(0);
|
||||
expect(handleBindTextResizeSpy).toHaveBeenCalledWith(
|
||||
|
@ -510,12 +510,12 @@ describe("arrow element", () => {
|
||||
h.state,
|
||||
)[0] as ExcalidrawElbowArrowElement;
|
||||
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||
|
||||
UI.resize(rectangle, "se", [-200, -150]);
|
||||
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||
});
|
||||
|
||||
@ -538,11 +538,11 @@ describe("arrow element", () => {
|
||||
h.state,
|
||||
)[0] as ExcalidrawElbowArrowElement;
|
||||
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||
|
||||
UI.resize([rectangle, arrow], "nw", [300, 350]);
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(0);
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(-0.05);
|
||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.25);
|
||||
});
|
||||
});
|
||||
@ -819,7 +819,7 @@ describe("image element", () => {
|
||||
|
||||
UI.resize(image, "ne", [40, 0]);
|
||||
|
||||
expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(31, 0);
|
||||
expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(30, 0);
|
||||
|
||||
const imageWidth = image.width;
|
||||
const scale = 20 / image.height;
|
||||
@ -1033,7 +1033,7 @@ describe("multiple selection", () => {
|
||||
|
||||
expect(leftBoundArrow.x).toBeCloseTo(-110);
|
||||
expect(leftBoundArrow.y).toBeCloseTo(50);
|
||||
expect(leftBoundArrow.width).toBeCloseTo(143, 0);
|
||||
expect(leftBoundArrow.width).toBeCloseTo(140, 0);
|
||||
expect(leftBoundArrow.height).toBeCloseTo(7, 0);
|
||||
expect(leftBoundArrow.angle).toEqual(0);
|
||||
expect(leftBoundArrow.startBinding).toBeNull();
|
||||
|
@ -18,7 +18,6 @@ import {
|
||||
arrayToMap,
|
||||
getFontFamilyString,
|
||||
getShortcutKey,
|
||||
tupleToCoors,
|
||||
getLineHeight,
|
||||
reduceToCommonValue,
|
||||
} from "@excalidraw/common";
|
||||
@ -27,9 +26,7 @@ import { getNonDeletedElements } from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
bindLinearElement,
|
||||
bindPointToSnapToElementOutline,
|
||||
calculateFixedPointForElbowArrowBinding,
|
||||
getHoveredElementForBinding,
|
||||
updateBoundElements,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
@ -1483,13 +1480,13 @@ const getArrowheadOptions = (flip: boolean) => {
|
||||
value: "crowfoot_one",
|
||||
text: t("labels.arrowhead_crowfoot_one"),
|
||||
icon: <ArrowheadCrowfootOneIcon flip={flip} />,
|
||||
keyBinding: "c",
|
||||
keyBinding: "x",
|
||||
},
|
||||
{
|
||||
value: "crowfoot_many",
|
||||
text: t("labels.arrowhead_crowfoot_many"),
|
||||
icon: <ArrowheadCrowfootIcon flip={flip} />,
|
||||
keyBinding: "x",
|
||||
keyBinding: "c",
|
||||
},
|
||||
{
|
||||
value: "crowfoot_one_or_many",
|
||||
@ -1626,63 +1623,16 @@ export const actionChangeArrowType = register({
|
||||
-1,
|
||||
elementsMap,
|
||||
);
|
||||
const startHoveredElement =
|
||||
!newElement.startBinding &&
|
||||
getHoveredElementForBinding(
|
||||
tupleToCoors(startGlobalPoint),
|
||||
elements,
|
||||
elementsMap,
|
||||
appState.zoom,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
const endHoveredElement =
|
||||
!newElement.endBinding &&
|
||||
getHoveredElementForBinding(
|
||||
tupleToCoors(endGlobalPoint),
|
||||
elements,
|
||||
elementsMap,
|
||||
appState.zoom,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
const startElement = startHoveredElement
|
||||
? startHoveredElement
|
||||
: newElement.startBinding &&
|
||||
(elementsMap.get(
|
||||
newElement.startBinding.elementId,
|
||||
) as ExcalidrawBindableElement);
|
||||
const endElement = endHoveredElement
|
||||
? endHoveredElement
|
||||
: newElement.endBinding &&
|
||||
(elementsMap.get(
|
||||
newElement.endBinding.elementId,
|
||||
) as ExcalidrawBindableElement);
|
||||
|
||||
const finalStartPoint = startHoveredElement
|
||||
? bindPointToSnapToElementOutline(
|
||||
newElement,
|
||||
startHoveredElement,
|
||||
"start",
|
||||
)
|
||||
: startGlobalPoint;
|
||||
const finalEndPoint = endHoveredElement
|
||||
? bindPointToSnapToElementOutline(
|
||||
newElement,
|
||||
endHoveredElement,
|
||||
"end",
|
||||
)
|
||||
: endGlobalPoint;
|
||||
|
||||
startHoveredElement &&
|
||||
bindLinearElement(
|
||||
newElement,
|
||||
startHoveredElement,
|
||||
"start",
|
||||
app.scene,
|
||||
);
|
||||
endHoveredElement &&
|
||||
bindLinearElement(newElement, endHoveredElement, "end", app.scene);
|
||||
const startElement =
|
||||
newElement.startBinding &&
|
||||
(elementsMap.get(
|
||||
newElement.startBinding.elementId,
|
||||
) as ExcalidrawBindableElement);
|
||||
const endElement =
|
||||
newElement.endBinding &&
|
||||
(elementsMap.get(
|
||||
newElement.endBinding.elementId,
|
||||
) as ExcalidrawBindableElement);
|
||||
|
||||
const startBinding =
|
||||
startElement && newElement.startBinding
|
||||
@ -1714,7 +1664,7 @@ export const actionChangeArrowType = register({
|
||||
startBinding,
|
||||
endBinding,
|
||||
...updateElbowArrowPoints(newElement, elementsMap, {
|
||||
points: [finalStartPoint, finalEndPoint].map(
|
||||
points: [startGlobalPoint, endGlobalPoint].map(
|
||||
(p): LocalPoint =>
|
||||
pointFrom(p[0] - newElement.x, p[1] - newElement.y),
|
||||
),
|
||||
|
@ -17,8 +17,6 @@ import {
|
||||
vectorDot,
|
||||
vectorNormalize,
|
||||
} from "@excalidraw/math";
|
||||
import { isPointInShape } from "@excalidraw/utils/collision";
|
||||
import { getSelectionBoxShape } from "@excalidraw/utils/shape";
|
||||
|
||||
import {
|
||||
COLOR_PALETTE,
|
||||
@ -104,9 +102,9 @@ import {
|
||||
Emitter,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import { getCommonBounds, getElementAbsoluteCoords } from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
getCommonBounds,
|
||||
getElementAbsoluteCoords,
|
||||
bindOrUnbindLinearElement,
|
||||
bindOrUnbindLinearElements,
|
||||
fixBindingsAfterDeletion,
|
||||
@ -117,13 +115,8 @@ import {
|
||||
shouldEnableBindingForPointerEvent,
|
||||
updateBoundElements,
|
||||
getSuggestedBindingsForArrows,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import { LinearElementEditor } from "@excalidraw/element";
|
||||
|
||||
import { newElementWith } from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
LinearElementEditor,
|
||||
newElementWith,
|
||||
newFrameElement,
|
||||
newFreeDrawElement,
|
||||
newEmbeddableElement,
|
||||
@ -135,11 +128,9 @@ import {
|
||||
newLinearElement,
|
||||
newTextElement,
|
||||
refreshTextDimensions,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import { deepCopyElement, duplicateElements } from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
deepCopyElement,
|
||||
duplicateElements,
|
||||
isPointInElement,
|
||||
hasBoundTextElement,
|
||||
isArrowElement,
|
||||
isBindingElement,
|
||||
@ -160,48 +151,27 @@ import {
|
||||
isFlowchartNodeElement,
|
||||
isBindableElement,
|
||||
isTextElement,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
getLockedLinearCursorAlignSize,
|
||||
getNormalizedDimensions,
|
||||
isElementCompletelyInViewport,
|
||||
isElementInViewport,
|
||||
isInvisiblySmallElement,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
getBoundTextShape,
|
||||
getCornerRadius,
|
||||
getElementShape,
|
||||
isPathALoop,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
createSrcDoc,
|
||||
embeddableURLValidator,
|
||||
maybeParseEmbedSrc,
|
||||
getEmbedLink,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
getInitializedImageElements,
|
||||
loadHTMLImageElement,
|
||||
normalizeSVG,
|
||||
updateImageCache as _updateImageCache,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
getBoundTextElement,
|
||||
getContainerCenter,
|
||||
getContainerElement,
|
||||
isValidTextContainer,
|
||||
redrawTextBoundingBox,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import { shouldShowBoundingBox } from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
shouldShowBoundingBox,
|
||||
getFrameChildren,
|
||||
isCursorInFrame,
|
||||
addElementsToFrame,
|
||||
@ -216,29 +186,17 @@ import {
|
||||
getFrameLikeTitle,
|
||||
getElementsOverlappingFrame,
|
||||
filterElementsEligibleAsFrameChildren,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
hitElementBoundText,
|
||||
hitElementBoundingBoxOnly,
|
||||
hitElementItself,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import { getVisibleSceneBounds } from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
getVisibleSceneBounds,
|
||||
FlowChartCreator,
|
||||
FlowChartNavigator,
|
||||
getLinkDirectionFromKey,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import { cropElement } from "@excalidraw/element";
|
||||
|
||||
import { wrapText } from "@excalidraw/element";
|
||||
|
||||
import { isElementLink, parseElementLinkFromURL } from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
cropElement,
|
||||
wrapText,
|
||||
isElementLink,
|
||||
parseElementLinkFromURL,
|
||||
isMeasureTextSupported,
|
||||
normalizeText,
|
||||
measureText,
|
||||
@ -246,13 +204,8 @@ import {
|
||||
getApproxMinLineWidth,
|
||||
getApproxMinLineHeight,
|
||||
getMinTextElementWidth,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import { ShapeCache } from "@excalidraw/element";
|
||||
|
||||
import { getRenderOpacity } from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
ShapeCache,
|
||||
getRenderOpacity,
|
||||
editGroupForSelectedElement,
|
||||
getElementsInGroup,
|
||||
getSelectedGroupIdForElement,
|
||||
@ -260,42 +213,27 @@ import {
|
||||
isElementInGroup,
|
||||
isSelectedViaGroup,
|
||||
selectGroupsForSelectedElements,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import { syncInvalidIndices, syncMovedIndices } from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
syncInvalidIndices,
|
||||
syncMovedIndices,
|
||||
excludeElementsInFramesFromSelection,
|
||||
getSelectionStateForElements,
|
||||
makeNextSelectedElementIds,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
getResizeOffsetXY,
|
||||
getResizeArrowDirection,
|
||||
transformElements,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
getCursorForResizingElement,
|
||||
getElementWithTransformHandleType,
|
||||
getTransformHandleTypeFromCoords,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
dragNewElement,
|
||||
dragSelectedElements,
|
||||
getDragOffsetXY,
|
||||
isNonDeletedElement,
|
||||
Scene,
|
||||
Store,
|
||||
CaptureUpdateAction,
|
||||
type ElementUpdate,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import { isNonDeletedElement } from "@excalidraw/element";
|
||||
|
||||
import { Scene } from "@excalidraw/element";
|
||||
|
||||
import { Store, CaptureUpdateAction } from "@excalidraw/element";
|
||||
|
||||
import type { ElementUpdate } from "@excalidraw/element";
|
||||
|
||||
import type { LocalPoint, Radians } from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
@ -5158,13 +5096,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
// If we're hitting element with highest z-index only on its bounding box
|
||||
// while also hitting other element figure, the latter should be considered.
|
||||
return hitElementItself({
|
||||
x,
|
||||
y,
|
||||
point: pointFrom(x, y),
|
||||
element: elementWithHighestZIndex,
|
||||
shape: getElementShape(
|
||||
elementWithHighestZIndex,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
// when overlapping, we would like to be more precise
|
||||
// this also avoids the need to update past tests
|
||||
threshold: this.getElementHitThreshold() / 2,
|
||||
@ -5248,34 +5181,26 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.state.selectedElementIds[element.id] &&
|
||||
shouldShowBoundingBox([element], this.state)
|
||||
) {
|
||||
const selectionShape = getSelectionBoxShape(
|
||||
element,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
isImageElement(element) ? 0 : this.getElementHitThreshold(),
|
||||
);
|
||||
|
||||
// if hitting the bounding box, return early
|
||||
// but if not, we should check for other cases as well (e.g. frame name)
|
||||
if (isPointInShape(pointFrom(x, y), selectionShape)) {
|
||||
if (isPointInElement(pointFrom(x, y), element)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// take bound text element into consideration for hit collision as well
|
||||
const hitBoundTextOfElement = hitElementBoundText(
|
||||
x,
|
||||
y,
|
||||
getBoundTextShape(element, this.scene.getNonDeletedElementsMap()),
|
||||
pointFrom(x, y),
|
||||
element,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (hitBoundTextOfElement) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return hitElementItself({
|
||||
x,
|
||||
y,
|
||||
point: pointFrom(x, y),
|
||||
element,
|
||||
shape: getElementShape(element, this.scene.getNonDeletedElementsMap()),
|
||||
threshold: this.getElementHitThreshold(),
|
||||
frameNameBound: isFrameLikeElement(element)
|
||||
? this.frameNameBoundsCache.get(element)
|
||||
@ -5304,13 +5229,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (
|
||||
isArrowElement(elements[index]) &&
|
||||
hitElementItself({
|
||||
x,
|
||||
y,
|
||||
point: pointFrom(x, y),
|
||||
element: elements[index],
|
||||
shape: getElementShape(
|
||||
elements[index],
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
threshold: this.getElementHitThreshold(),
|
||||
})
|
||||
) {
|
||||
@ -5656,13 +5576,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
hasBoundTextElement(container) ||
|
||||
!isTransparent(container.backgroundColor) ||
|
||||
hitElementItself({
|
||||
x: sceneX,
|
||||
y: sceneY,
|
||||
point: pointFrom(sceneX, sceneY),
|
||||
element: container,
|
||||
shape: getElementShape(
|
||||
container,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
threshold: this.getElementHitThreshold(),
|
||||
})
|
||||
) {
|
||||
@ -6353,13 +6268,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
let segmentMidPointHoveredCoords = null;
|
||||
if (
|
||||
hitElementItself({
|
||||
x: scenePointerX,
|
||||
y: scenePointerY,
|
||||
point: pointFrom(scenePointerX, scenePointerY),
|
||||
element,
|
||||
shape: getElementShape(
|
||||
element,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
})
|
||||
) {
|
||||
hoverPointIndex = LinearElementEditor.getPointIndexUnderCursor(
|
||||
@ -9822,13 +9732,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
((hitElement &&
|
||||
hitElementBoundingBoxOnly(
|
||||
{
|
||||
x: pointerDownState.origin.x,
|
||||
y: pointerDownState.origin.y,
|
||||
element: hitElement,
|
||||
shape: getElementShape(
|
||||
hitElement,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
point: pointFrom(
|
||||
pointerDownState.origin.x,
|
||||
pointerDownState.origin.y,
|
||||
),
|
||||
element: hitElement,
|
||||
threshold: this.getElementHitThreshold(),
|
||||
frameNameBound: isFrameLikeElement(hitElement)
|
||||
? this.frameNameBoundsCache.get(hitElement)
|
||||
|
@ -564,7 +564,7 @@ export const convertElementTypes = (
|
||||
continue;
|
||||
}
|
||||
const fixedSegments: FixedSegment[] = [];
|
||||
for (let i = 0; i < nextPoints.length - 1; i++) {
|
||||
for (let i = 1; i < nextPoints.length - 2; i++) {
|
||||
fixedSegments.push({
|
||||
start: nextPoints[i],
|
||||
end: nextPoints[i + 1],
|
||||
@ -581,6 +581,7 @@ export const convertElementTypes = (
|
||||
);
|
||||
mutateElement(element, app.scene.getNonDeletedElementsMap(), {
|
||||
...updates,
|
||||
endArrowhead: "arrow",
|
||||
});
|
||||
} else {
|
||||
// if we're converting to non-elbow linear element, check if
|
||||
|
@ -133,10 +133,9 @@ describe("binding with linear elements", () => {
|
||||
const inputX = UI.queryStatsProperty("X")?.querySelector(
|
||||
".drag-input",
|
||||
) as HTMLInputElement;
|
||||
|
||||
expect(linear.startBinding).not.toBe(null);
|
||||
expect(inputX).not.toBeNull();
|
||||
UI.updateInput(inputX, String("204"));
|
||||
UI.updateInput(inputX, String("199"));
|
||||
expect(linear.startBinding).not.toBe(null);
|
||||
});
|
||||
|
||||
@ -657,6 +656,7 @@ describe("stats for multiple elements", () => {
|
||||
|
||||
mouse.reset();
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.moveTo(10, 0);
|
||||
mouse.click();
|
||||
});
|
||||
|
||||
|
@ -463,7 +463,7 @@ const shouldHideLinkPopup = (
|
||||
|
||||
const threshold = 15 / appState.zoom.value;
|
||||
// hitbox to prevent hiding when hovered in element bounding box
|
||||
if (hitElementBoundingBox(sceneX, sceneY, element, elementsMap)) {
|
||||
if (hitElementBoundingBox(pointFrom(sceneX, sceneY), element, elementsMap)) {
|
||||
return false;
|
||||
}
|
||||
const [x1, y1, x2] = getElementAbsoluteCoords(element, elementsMap);
|
||||
|
@ -92,7 +92,7 @@ export const isPointHittingLink = (
|
||||
if (
|
||||
!isMobile &&
|
||||
appState.viewModeEnabled &&
|
||||
hitElementBoundingBox(x, y, element, elementsMap)
|
||||
hitElementBoundingBox(pointFrom(x, y), element, elementsMap)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
@ -175,7 +175,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||
"startBinding": {
|
||||
"elementId": "diamond-1",
|
||||
"focus": 0,
|
||||
"gap": 4.545343408287929,
|
||||
"gap": 4.535423522449215,
|
||||
},
|
||||
"strokeColor": "#e67700",
|
||||
"strokeStyle": "solid",
|
||||
@ -335,7 +335,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
||||
"endBinding": {
|
||||
"elementId": "text-2",
|
||||
"focus": 0,
|
||||
"gap": 14,
|
||||
"gap": 16,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
@ -1538,7 +1538,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
||||
"endBinding": {
|
||||
"elementId": "B",
|
||||
"focus": 0,
|
||||
"gap": 14,
|
||||
"gap": 32,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
@ -781,7 +781,7 @@ describe("Test Transform", () => {
|
||||
expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
|
||||
elementId: "rect-1",
|
||||
focus: -0,
|
||||
gap: 14,
|
||||
gap: 25,
|
||||
});
|
||||
expect(rect.boundElements).toStrictEqual([
|
||||
{
|
||||
|
@ -1,20 +1,17 @@
|
||||
import { arrayToMap, easeOut, THEME } from "@excalidraw/common";
|
||||
import { getElementLineSegments } from "@excalidraw/element";
|
||||
import {
|
||||
lineSegment,
|
||||
lineSegmentIntersectionPoints,
|
||||
pointFrom,
|
||||
} from "@excalidraw/math";
|
||||
getBoundTextElement,
|
||||
intersectElementWithLineSegment,
|
||||
isPointInElement,
|
||||
} from "@excalidraw/element";
|
||||
import { lineSegment, pointFrom } from "@excalidraw/math";
|
||||
|
||||
import { getElementsInGroup } from "@excalidraw/element";
|
||||
|
||||
import { getElementShape } from "@excalidraw/element";
|
||||
import { shouldTestInside } from "@excalidraw/element";
|
||||
import { isPointInShape } from "@excalidraw/utils/collision";
|
||||
import { hasBoundTextElement, isBoundToContainer } from "@excalidraw/element";
|
||||
import { getBoundTextElementId } from "@excalidraw/element";
|
||||
|
||||
import type { GeometricShape } from "@excalidraw/utils/shape";
|
||||
import type {
|
||||
ElementsSegmentsMap,
|
||||
GlobalPoint,
|
||||
@ -35,8 +32,6 @@ export class EraserTrail extends AnimatedTrail {
|
||||
private elementsToErase: Set<ExcalidrawElement["id"]> = new Set();
|
||||
private groupsToErase: Set<ExcalidrawElement["id"]> = new Set();
|
||||
private segmentsCache: Map<string, LineSegment<GlobalPoint>[]> = new Map();
|
||||
private geometricShapesCache: Map<string, GeometricShape<GlobalPoint>> =
|
||||
new Map();
|
||||
|
||||
constructor(animationFrameHandler: AnimationFrameHandler, app: App) {
|
||||
super(animationFrameHandler, app, {
|
||||
@ -113,7 +108,6 @@ export class EraserTrail extends AnimatedTrail {
|
||||
pathSegments,
|
||||
element,
|
||||
this.segmentsCache,
|
||||
this.geometricShapesCache,
|
||||
candidateElementsMap,
|
||||
this.app,
|
||||
);
|
||||
@ -151,7 +145,6 @@ export class EraserTrail extends AnimatedTrail {
|
||||
pathSegments,
|
||||
element,
|
||||
this.segmentsCache,
|
||||
this.geometricShapesCache,
|
||||
candidateElementsMap,
|
||||
this.app,
|
||||
);
|
||||
@ -204,37 +197,23 @@ const eraserTest = (
|
||||
pathSegments: LineSegment<GlobalPoint>[],
|
||||
element: ExcalidrawElement,
|
||||
elementsSegments: ElementsSegmentsMap,
|
||||
shapesCache: Map<string, GeometricShape<GlobalPoint>>,
|
||||
elementsMap: ElementsMap,
|
||||
app: App,
|
||||
): boolean => {
|
||||
let shape = shapesCache.get(element.id);
|
||||
|
||||
if (!shape) {
|
||||
shape = getElementShape<GlobalPoint>(element, elementsMap);
|
||||
shapesCache.set(element.id, shape);
|
||||
}
|
||||
|
||||
const lastPoint = pathSegments[pathSegments.length - 1][1];
|
||||
if (shouldTestInside(element) && isPointInShape(lastPoint, shape)) {
|
||||
if (shouldTestInside(element) && isPointInElement(lastPoint, element)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let elementSegments = elementsSegments.get(element.id);
|
||||
const offset = app.getElementHitThreshold();
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
|
||||
if (!elementSegments) {
|
||||
elementSegments = getElementLineSegments(element, elementsMap);
|
||||
elementsSegments.set(element.id, elementSegments);
|
||||
}
|
||||
|
||||
return pathSegments.some((pathSegment) =>
|
||||
elementSegments?.some(
|
||||
(elementSegment) =>
|
||||
lineSegmentIntersectionPoints(
|
||||
pathSegment,
|
||||
elementSegment,
|
||||
app.getElementHitThreshold(),
|
||||
) !== null,
|
||||
),
|
||||
return pathSegments.some(
|
||||
(pathSegment) =>
|
||||
intersectElementWithLineSegment(element, pathSegment, offset).length >
|
||||
0 ||
|
||||
(boundTextElement &&
|
||||
intersectElementWithLineSegment(boundTextElement, pathSegment, offset)
|
||||
.length > 0),
|
||||
);
|
||||
};
|
||||
|
@ -203,6 +203,7 @@ export class LassoTrail extends AnimatedTrail {
|
||||
intersectedElements: this.intersectedElements,
|
||||
enclosedElements: this.enclosedElements,
|
||||
simplifyDistance: 5 / this.app.state.zoom.value,
|
||||
hitThreshold: this.app.getElementHitThreshold(),
|
||||
});
|
||||
|
||||
this.selectElementsFromIds(selectedElementIds);
|
||||
|
@ -3,15 +3,12 @@ import { simplify } from "points-on-curve";
|
||||
import {
|
||||
polygonFromPoints,
|
||||
lineSegment,
|
||||
lineSegmentIntersectionPoints,
|
||||
polygonIncludesPointNonZero,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
ElementsSegmentsMap,
|
||||
GlobalPoint,
|
||||
LineSegment,
|
||||
} from "@excalidraw/math/types";
|
||||
import { intersectElementWithLineSegment } from "@excalidraw/element";
|
||||
|
||||
import type { ElementsSegmentsMap, GlobalPoint } from "@excalidraw/math/types";
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
export const getLassoSelectedElementIds = (input: {
|
||||
@ -21,6 +18,7 @@ export const getLassoSelectedElementIds = (input: {
|
||||
intersectedElements: Set<ExcalidrawElement["id"]>;
|
||||
enclosedElements: Set<ExcalidrawElement["id"]>;
|
||||
simplifyDistance?: number;
|
||||
hitThreshold: number;
|
||||
}): {
|
||||
selectedElementIds: string[];
|
||||
} => {
|
||||
@ -31,6 +29,7 @@ export const getLassoSelectedElementIds = (input: {
|
||||
intersectedElements,
|
||||
enclosedElements,
|
||||
simplifyDistance,
|
||||
hitThreshold,
|
||||
} = input;
|
||||
// simplify the path to reduce the number of points
|
||||
let path: GlobalPoint[] = lassoPath;
|
||||
@ -49,7 +48,7 @@ export const getLassoSelectedElementIds = (input: {
|
||||
if (enclosed) {
|
||||
enclosedElements.add(element.id);
|
||||
} else {
|
||||
const intersects = intersectionTest(path, element, elementsSegments);
|
||||
const intersects = intersectionTest(path, element, hitThreshold);
|
||||
if (intersects) {
|
||||
intersectedElements.add(element.id);
|
||||
}
|
||||
@ -85,26 +84,16 @@ const enclosureTest = (
|
||||
const intersectionTest = (
|
||||
lassoPath: GlobalPoint[],
|
||||
element: ExcalidrawElement,
|
||||
elementsSegments: ElementsSegmentsMap,
|
||||
hitThreshold: number,
|
||||
): boolean => {
|
||||
const elementSegments = elementsSegments.get(element.id);
|
||||
if (!elementSegments) {
|
||||
return false;
|
||||
}
|
||||
const lassoSegments = lassoPath
|
||||
.slice(1)
|
||||
.map((point: GlobalPoint, index) => lineSegment(lassoPath[index], point))
|
||||
.concat([lineSegment(lassoPath[lassoPath.length - 1], lassoPath[0])]);
|
||||
|
||||
const lassoSegments = lassoPath.reduce((acc, point, index) => {
|
||||
if (index === 0) {
|
||||
return acc;
|
||||
}
|
||||
acc.push(lineSegment(lassoPath[index - 1], point));
|
||||
return acc;
|
||||
}, [] as LineSegment<GlobalPoint>[]);
|
||||
|
||||
return lassoSegments.some((lassoSegment) =>
|
||||
elementSegments.some(
|
||||
(elementSegment) =>
|
||||
// introduce a bit of tolerance to account for roughness and simplification of paths
|
||||
lineSegmentIntersectionPoints(lassoSegment, elementSegment, 1) !== null,
|
||||
),
|
||||
return lassoSegments.some(
|
||||
(lassoSegment) =>
|
||||
intersectElementWithLineSegment(element, lassoSegment, hitThreshold)
|
||||
.length > 0,
|
||||
);
|
||||
};
|
||||
|
@ -5,17 +5,14 @@ import { getDiamondPoints } from "@excalidraw/element";
|
||||
import { getCornerRadius } from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
bezierEquation,
|
||||
curve,
|
||||
curveTangent,
|
||||
curveCatmullRomCubicApproxPoints,
|
||||
curveCatmullRomQuadraticApproxPoints,
|
||||
curveOffsetPoints,
|
||||
type GlobalPoint,
|
||||
offsetPointsForQuadraticBezier,
|
||||
pointFrom,
|
||||
pointFromVector,
|
||||
pointRotateRads,
|
||||
vector,
|
||||
vectorNormal,
|
||||
vectorNormalize,
|
||||
vectorScale,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
@ -99,25 +96,14 @@ export const bootstrapCanvas = ({
|
||||
function drawCatmullRomQuadraticApprox(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
points: GlobalPoint[],
|
||||
segments = 20,
|
||||
tension = 0.5,
|
||||
) {
|
||||
ctx.lineTo(points[0][0], points[0][1]);
|
||||
const pointSets = curveCatmullRomQuadraticApproxPoints(points, tension);
|
||||
if (pointSets) {
|
||||
for (let i = 0; i < pointSets.length - 1; i++) {
|
||||
const [[cpX, cpY], [p2X, p2Y]] = pointSets[i];
|
||||
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
const p0 = points[i - 1 < 0 ? 0 : i - 1];
|
||||
const p1 = points[i];
|
||||
const p2 = points[i + 1 >= points.length ? points.length - 1 : i + 1];
|
||||
|
||||
for (let t = 0; t <= 1; t += 1 / segments) {
|
||||
const t2 = t * t;
|
||||
|
||||
const x =
|
||||
(1 - t) * (1 - t) * p0[0] + 2 * (1 - t) * t * p1[0] + t2 * p2[0];
|
||||
|
||||
const y =
|
||||
(1 - t) * (1 - t) * p0[1] + 2 * (1 - t) * t * p1[1] + t2 * p2[1];
|
||||
|
||||
ctx.lineTo(x, y);
|
||||
ctx.quadraticCurveTo(cpX, cpY, p2X, p2Y);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -125,35 +111,13 @@ function drawCatmullRomQuadraticApprox(
|
||||
function drawCatmullRomCubicApprox(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
points: GlobalPoint[],
|
||||
segments = 20,
|
||||
tension = 0.5,
|
||||
) {
|
||||
ctx.lineTo(points[0][0], points[0][1]);
|
||||
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
const p0 = points[i - 1 < 0 ? 0 : i - 1];
|
||||
const p1 = points[i];
|
||||
const p2 = points[i + 1 >= points.length ? points.length - 1 : i + 1];
|
||||
const p3 = points[i + 2 >= points.length ? points.length - 1 : i + 2];
|
||||
|
||||
for (let t = 0; t <= 1; t += 1 / segments) {
|
||||
const t2 = t * t;
|
||||
const t3 = t2 * t;
|
||||
|
||||
const x =
|
||||
0.5 *
|
||||
(2 * p1[0] +
|
||||
(-p0[0] + p2[0]) * t +
|
||||
(2 * p0[0] - 5 * p1[0] + 4 * p2[0] - p3[0]) * t2 +
|
||||
(-p0[0] + 3 * p1[0] - 3 * p2[0] + p3[0]) * t3);
|
||||
|
||||
const y =
|
||||
0.5 *
|
||||
(2 * p1[1] +
|
||||
(-p0[1] + p2[1]) * t +
|
||||
(2 * p0[1] - 5 * p1[1] + 4 * p2[1] - p3[1]) * t2 +
|
||||
(-p0[1] + 3 * p1[1] - 3 * p2[1] + p3[1]) * t3);
|
||||
|
||||
ctx.lineTo(x, y);
|
||||
const pointSets = curveCatmullRomCubicApproxPoints(points, tension);
|
||||
if (pointSets) {
|
||||
for (let i = 0; i < pointSets.length; i++) {
|
||||
const [[cp1x, cp1y], [cp2x, cp2y], [x, y]] = pointSets[i];
|
||||
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -184,25 +148,25 @@ export const drawHighlightForRectWithRotation = (
|
||||
context.beginPath();
|
||||
|
||||
{
|
||||
const topLeftApprox = offsetQuadraticBezier(
|
||||
const topLeftApprox = offsetPointsForQuadraticBezier(
|
||||
pointFrom(0, 0 + radius),
|
||||
pointFrom(0, 0),
|
||||
pointFrom(0 + radius, 0),
|
||||
padding,
|
||||
);
|
||||
const topRightApprox = offsetQuadraticBezier(
|
||||
const topRightApprox = offsetPointsForQuadraticBezier(
|
||||
pointFrom(element.width - radius, 0),
|
||||
pointFrom(element.width, 0),
|
||||
pointFrom(element.width, radius),
|
||||
padding,
|
||||
);
|
||||
const bottomRightApprox = offsetQuadraticBezier(
|
||||
const bottomRightApprox = offsetPointsForQuadraticBezier(
|
||||
pointFrom(element.width, element.height - radius),
|
||||
pointFrom(element.width, element.height),
|
||||
pointFrom(element.width - radius, element.height),
|
||||
padding,
|
||||
);
|
||||
const bottomLeftApprox = offsetQuadraticBezier(
|
||||
const bottomLeftApprox = offsetPointsForQuadraticBezier(
|
||||
pointFrom(radius, element.height),
|
||||
pointFrom(0, element.height),
|
||||
pointFrom(0, element.height - radius),
|
||||
@ -227,25 +191,25 @@ export const drawHighlightForRectWithRotation = (
|
||||
// mask" on a filled shape for the diamond highlight, because stroking creates
|
||||
// sharp inset edges on line joins < 90 degrees.
|
||||
{
|
||||
const topLeftApprox = offsetQuadraticBezier(
|
||||
const topLeftApprox = offsetPointsForQuadraticBezier(
|
||||
pointFrom(0 + radius, 0),
|
||||
pointFrom(0, 0),
|
||||
pointFrom(0, 0 + radius),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const topRightApprox = offsetQuadraticBezier(
|
||||
const topRightApprox = offsetPointsForQuadraticBezier(
|
||||
pointFrom(element.width, radius),
|
||||
pointFrom(element.width, 0),
|
||||
pointFrom(element.width - radius, 0),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const bottomRightApprox = offsetQuadraticBezier(
|
||||
const bottomRightApprox = offsetPointsForQuadraticBezier(
|
||||
pointFrom(element.width - radius, element.height),
|
||||
pointFrom(element.width, element.height),
|
||||
pointFrom(element.width, element.height - radius),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const bottomLeftApprox = offsetQuadraticBezier(
|
||||
const bottomLeftApprox = offsetPointsForQuadraticBezier(
|
||||
pointFrom(0, element.height - radius),
|
||||
pointFrom(0, element.height),
|
||||
pointFrom(radius, element.height),
|
||||
@ -340,32 +304,40 @@ export const drawHighlightForDiamondWithRotation = (
|
||||
const horizontalRadius = element.roundness
|
||||
? getCornerRadius(Math.abs(rightY - topY), element)
|
||||
: (rightY - topY) * 0.01;
|
||||
const topApprox = offsetCubicBezier(
|
||||
pointFrom(topX - verticalRadius, topY + horizontalRadius),
|
||||
pointFrom(topX, topY),
|
||||
pointFrom(topX, topY),
|
||||
pointFrom(topX + verticalRadius, topY + horizontalRadius),
|
||||
const topApprox = curveOffsetPoints(
|
||||
curve(
|
||||
pointFrom(topX - verticalRadius, topY + horizontalRadius),
|
||||
pointFrom(topX, topY),
|
||||
pointFrom(topX, topY),
|
||||
pointFrom(topX + verticalRadius, topY + horizontalRadius),
|
||||
),
|
||||
padding,
|
||||
);
|
||||
const rightApprox = offsetCubicBezier(
|
||||
pointFrom(rightX - verticalRadius, rightY - horizontalRadius),
|
||||
pointFrom(rightX, rightY),
|
||||
pointFrom(rightX, rightY),
|
||||
pointFrom(rightX - verticalRadius, rightY + horizontalRadius),
|
||||
const rightApprox = curveOffsetPoints(
|
||||
curve(
|
||||
pointFrom(rightX - verticalRadius, rightY - horizontalRadius),
|
||||
pointFrom(rightX, rightY),
|
||||
pointFrom(rightX, rightY),
|
||||
pointFrom(rightX - verticalRadius, rightY + horizontalRadius),
|
||||
),
|
||||
padding,
|
||||
);
|
||||
const bottomApprox = offsetCubicBezier(
|
||||
pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius),
|
||||
pointFrom(bottomX, bottomY),
|
||||
pointFrom(bottomX, bottomY),
|
||||
pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius),
|
||||
const bottomApprox = curveOffsetPoints(
|
||||
curve(
|
||||
pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius),
|
||||
pointFrom(bottomX, bottomY),
|
||||
pointFrom(bottomX, bottomY),
|
||||
pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius),
|
||||
),
|
||||
padding,
|
||||
);
|
||||
const leftApprox = offsetCubicBezier(
|
||||
pointFrom(leftX + verticalRadius, leftY + horizontalRadius),
|
||||
pointFrom(leftX, leftY),
|
||||
pointFrom(leftX, leftY),
|
||||
pointFrom(leftX + verticalRadius, leftY - horizontalRadius),
|
||||
const leftApprox = curveOffsetPoints(
|
||||
curve(
|
||||
pointFrom(leftX + verticalRadius, leftY + horizontalRadius),
|
||||
pointFrom(leftX, leftY),
|
||||
pointFrom(leftX, leftY),
|
||||
pointFrom(leftX + verticalRadius, leftY - horizontalRadius),
|
||||
),
|
||||
padding,
|
||||
);
|
||||
|
||||
@ -373,13 +345,13 @@ export const drawHighlightForDiamondWithRotation = (
|
||||
topApprox[topApprox.length - 1][0],
|
||||
topApprox[topApprox.length - 1][1],
|
||||
);
|
||||
context.lineTo(rightApprox[0][0], rightApprox[0][1]);
|
||||
context.lineTo(rightApprox[1][0], rightApprox[1][1]);
|
||||
drawCatmullRomCubicApprox(context, rightApprox);
|
||||
context.lineTo(bottomApprox[0][0], bottomApprox[0][1]);
|
||||
context.lineTo(bottomApprox[1][0], bottomApprox[1][1]);
|
||||
drawCatmullRomCubicApprox(context, bottomApprox);
|
||||
context.lineTo(leftApprox[0][0], leftApprox[0][1]);
|
||||
context.lineTo(leftApprox[1][0], leftApprox[1][1]);
|
||||
drawCatmullRomCubicApprox(context, leftApprox);
|
||||
context.lineTo(topApprox[0][0], topApprox[0][1]);
|
||||
context.lineTo(topApprox[1][0], topApprox[1][1]);
|
||||
drawCatmullRomCubicApprox(context, topApprox);
|
||||
}
|
||||
|
||||
@ -395,32 +367,40 @@ export const drawHighlightForDiamondWithRotation = (
|
||||
const horizontalRadius = element.roundness
|
||||
? getCornerRadius(Math.abs(rightY - topY), element)
|
||||
: (rightY - topY) * 0.01;
|
||||
const topApprox = offsetCubicBezier(
|
||||
pointFrom(topX + verticalRadius, topY + horizontalRadius),
|
||||
pointFrom(topX, topY),
|
||||
pointFrom(topX, topY),
|
||||
pointFrom(topX - verticalRadius, topY + horizontalRadius),
|
||||
const topApprox = curveOffsetPoints(
|
||||
curve(
|
||||
pointFrom(topX + verticalRadius, topY + horizontalRadius),
|
||||
pointFrom(topX, topY),
|
||||
pointFrom(topX, topY),
|
||||
pointFrom(topX - verticalRadius, topY + horizontalRadius),
|
||||
),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const rightApprox = offsetCubicBezier(
|
||||
pointFrom(rightX - verticalRadius, rightY + horizontalRadius),
|
||||
pointFrom(rightX, rightY),
|
||||
pointFrom(rightX, rightY),
|
||||
pointFrom(rightX - verticalRadius, rightY - horizontalRadius),
|
||||
const rightApprox = curveOffsetPoints(
|
||||
curve(
|
||||
pointFrom(rightX - verticalRadius, rightY + horizontalRadius),
|
||||
pointFrom(rightX, rightY),
|
||||
pointFrom(rightX, rightY),
|
||||
pointFrom(rightX - verticalRadius, rightY - horizontalRadius),
|
||||
),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const bottomApprox = offsetCubicBezier(
|
||||
pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius),
|
||||
pointFrom(bottomX, bottomY),
|
||||
pointFrom(bottomX, bottomY),
|
||||
pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius),
|
||||
const bottomApprox = curveOffsetPoints(
|
||||
curve(
|
||||
pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius),
|
||||
pointFrom(bottomX, bottomY),
|
||||
pointFrom(bottomX, bottomY),
|
||||
pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius),
|
||||
),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const leftApprox = offsetCubicBezier(
|
||||
pointFrom(leftX + verticalRadius, leftY - horizontalRadius),
|
||||
pointFrom(leftX, leftY),
|
||||
pointFrom(leftX, leftY),
|
||||
pointFrom(leftX + verticalRadius, leftY + horizontalRadius),
|
||||
const leftApprox = curveOffsetPoints(
|
||||
curve(
|
||||
pointFrom(leftX + verticalRadius, leftY - horizontalRadius),
|
||||
pointFrom(leftX, leftY),
|
||||
pointFrom(leftX, leftY),
|
||||
pointFrom(leftX + verticalRadius, leftY + horizontalRadius),
|
||||
),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
|
||||
@ -428,66 +408,16 @@ export const drawHighlightForDiamondWithRotation = (
|
||||
topApprox[topApprox.length - 1][0],
|
||||
topApprox[topApprox.length - 1][1],
|
||||
);
|
||||
context.lineTo(leftApprox[0][0], leftApprox[0][1]);
|
||||
context.lineTo(leftApprox[1][0], leftApprox[1][1]);
|
||||
drawCatmullRomCubicApprox(context, leftApprox);
|
||||
context.lineTo(bottomApprox[0][0], bottomApprox[0][1]);
|
||||
context.lineTo(bottomApprox[1][0], bottomApprox[1][1]);
|
||||
drawCatmullRomCubicApprox(context, bottomApprox);
|
||||
context.lineTo(rightApprox[0][0], rightApprox[0][1]);
|
||||
context.lineTo(rightApprox[1][0], rightApprox[1][1]);
|
||||
drawCatmullRomCubicApprox(context, rightApprox);
|
||||
context.lineTo(topApprox[0][0], topApprox[0][1]);
|
||||
context.lineTo(topApprox[1][0], topApprox[1][1]);
|
||||
drawCatmullRomCubicApprox(context, topApprox);
|
||||
}
|
||||
context.closePath();
|
||||
context.fill();
|
||||
context.restore();
|
||||
};
|
||||
|
||||
function offsetCubicBezier(
|
||||
p0: GlobalPoint,
|
||||
p1: GlobalPoint,
|
||||
p2: GlobalPoint,
|
||||
p3: GlobalPoint,
|
||||
offsetDist: number,
|
||||
steps = 20,
|
||||
) {
|
||||
const offsetPoints = [];
|
||||
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const t = i / steps;
|
||||
const c = curve(p0, p1, p2, p3);
|
||||
const point = bezierEquation(c, t);
|
||||
const tangent = vectorNormalize(curveTangent(c, t));
|
||||
const normal = vectorNormal(tangent);
|
||||
|
||||
offsetPoints.push(pointFromVector(vectorScale(normal, offsetDist), point));
|
||||
}
|
||||
|
||||
return offsetPoints;
|
||||
}
|
||||
|
||||
function offsetQuadraticBezier(
|
||||
p0: GlobalPoint,
|
||||
p1: GlobalPoint,
|
||||
p2: GlobalPoint,
|
||||
offsetDist: number,
|
||||
steps = 20,
|
||||
) {
|
||||
const offsetPoints = [];
|
||||
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const t = i / steps;
|
||||
const t1 = 1 - t;
|
||||
const point = pointFrom<GlobalPoint>(
|
||||
t1 * t1 * p0[0] + 2 * t1 * t * p1[0] + t * t * p2[0],
|
||||
t1 * t1 * p0[1] + 2 * t1 * t * p1[1] + t * t * p2[1],
|
||||
);
|
||||
const tangentX = 2 * (1 - t) * (p1[0] - p0[0]) + 2 * t * (p2[0] - p1[0]);
|
||||
const tangentY = 2 * (1 - t) * (p1[1] - p0[1]) + 2 * t * (p2[1] - p1[1]);
|
||||
const tangent = vectorNormalize(vector(tangentX, tangentY));
|
||||
const normal = vectorNormal(tangent);
|
||||
|
||||
offsetPoints.push(pointFromVector(vectorScale(normal, offsetDist), point));
|
||||
}
|
||||
|
||||
return offsetPoints;
|
||||
}
|
||||
|
@ -187,16 +187,10 @@ const renderBindingHighlightForBindableElement = (
|
||||
elementsMap: ElementsMap,
|
||||
zoom: InteractiveCanvasAppState["zoom"],
|
||||
) => {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
||||
const width = x2 - x1;
|
||||
const height = y2 - y1;
|
||||
|
||||
context.strokeStyle = "rgba(0,0,0,.05)";
|
||||
context.fillStyle = "rgba(0,0,0,.05)";
|
||||
|
||||
// To ensure the binding highlight doesn't overlap the element itself
|
||||
const padding = maxBindingGap(element, element.width, element.height, zoom);
|
||||
|
||||
context.fillStyle = "rgba(0,0,0,.05)";
|
||||
|
||||
switch (element.type) {
|
||||
case "rectangle":
|
||||
case "text":
|
||||
@ -210,10 +204,13 @@ const renderBindingHighlightForBindableElement = (
|
||||
case "diamond":
|
||||
drawHighlightForDiamondWithRotation(context, padding, element);
|
||||
break;
|
||||
case "ellipse":
|
||||
context.lineWidth =
|
||||
maxBindingGap(element, element.width, element.height, zoom) -
|
||||
FIXED_BINDING_DISTANCE;
|
||||
case "ellipse": {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
||||
const width = x2 - x1;
|
||||
const height = y2 - y1;
|
||||
|
||||
context.strokeStyle = "rgba(0,0,0,.05)";
|
||||
context.lineWidth = padding - FIXED_BINDING_DISTANCE;
|
||||
|
||||
strokeEllipseWithRotation(
|
||||
context,
|
||||
@ -224,6 +221,7 @@ const renderBindingHighlightForBindableElement = (
|
||||
element.angle,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1225,7 +1225,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
||||
"version": 3,
|
||||
"versionNonce": 1150084233,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"x": -7,
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
@ -1278,7 +1278,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"x": -7,
|
||||
"y": 0,
|
||||
},
|
||||
"inserted": {
|
||||
@ -2107,7 +2107,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
||||
"version": 3,
|
||||
"versionNonce": 1150084233,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"x": -7,
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
@ -2160,7 +2160,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"x": -7,
|
||||
"y": 0,
|
||||
},
|
||||
"inserted": {
|
||||
@ -2319,7 +2319,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
||||
"version": 4,
|
||||
"versionNonce": 1014066025,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"x": -7,
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
@ -2372,7 +2372,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"x": -7,
|
||||
"y": 0,
|
||||
},
|
||||
"inserted": {
|
||||
@ -2562,7 +2562,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
||||
"version": 3,
|
||||
"versionNonce": 1150084233,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"x": -7,
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
@ -2596,7 +2596,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
||||
"version": 5,
|
||||
"versionNonce": 400692809,
|
||||
"width": 20,
|
||||
"x": 0,
|
||||
"x": 3,
|
||||
"y": 10,
|
||||
}
|
||||
`;
|
||||
@ -2649,7 +2649,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"x": -7,
|
||||
"y": 0,
|
||||
},
|
||||
"inserted": {
|
||||
@ -2703,7 +2703,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"width": 20,
|
||||
"x": 0,
|
||||
"x": 3,
|
||||
"y": 10,
|
||||
},
|
||||
"inserted": {
|
||||
@ -8670,8 +8670,8 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
||||
},
|
||||
},
|
||||
],
|
||||
"left": -17,
|
||||
"top": -7,
|
||||
"left": 10,
|
||||
"top": 20,
|
||||
},
|
||||
"croppingElementId": null,
|
||||
"currentChartType": "bar",
|
||||
|
@ -200,7 +200,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "102.45605",
|
||||
"height": "102.44561",
|
||||
"id": "id691",
|
||||
"index": "a2",
|
||||
"isDeleted": false,
|
||||
@ -214,8 +214,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
0,
|
||||
],
|
||||
[
|
||||
"102.80179",
|
||||
"102.45605",
|
||||
"102.69685",
|
||||
"102.44561",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
@ -230,8 +230,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 37,
|
||||
"width": "102.80179",
|
||||
"x": "-0.42182",
|
||||
"width": "102.69685",
|
||||
"x": "-0.30656",
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
@ -314,15 +314,15 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
},
|
||||
"height": "70.45017",
|
||||
"height": "70.31130",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
"100.70774",
|
||||
"70.45017",
|
||||
"100.51087",
|
||||
"70.31130",
|
||||
],
|
||||
],
|
||||
"startBinding": {
|
||||
@ -337,15 +337,15 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"focus": "-0.02000",
|
||||
"gap": 1,
|
||||
},
|
||||
"height": "0.09250",
|
||||
"height": "0.10543",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"0.09250",
|
||||
"98.00000",
|
||||
"0.10543",
|
||||
],
|
||||
],
|
||||
"startBinding": {
|
||||
@ -398,30 +398,30 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
},
|
||||
"id691": {
|
||||
"deleted": {
|
||||
"height": "102.45584",
|
||||
"height": "102.44538",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
"102.79971",
|
||||
"102.45584",
|
||||
"102.69463",
|
||||
"102.44538",
|
||||
],
|
||||
],
|
||||
"startBinding": null,
|
||||
"y": 0,
|
||||
},
|
||||
"inserted": {
|
||||
"height": "70.33521",
|
||||
"height": "70.20818",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
"100.78887",
|
||||
"70.33521",
|
||||
"100.62538",
|
||||
"70.20818",
|
||||
],
|
||||
],
|
||||
"startBinding": {
|
||||
@ -429,7 +429,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"focus": "0.02970",
|
||||
"gap": 1,
|
||||
},
|
||||
"y": "35.20327",
|
||||
"y": "35.26761",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -814,7 +814,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"updated": 1,
|
||||
"version": 33,
|
||||
"width": 100,
|
||||
"x": "149.29289",
|
||||
"x": 149,
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
@ -1235,7 +1235,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "1.30038",
|
||||
"height": "1.33342",
|
||||
"id": "id715",
|
||||
"index": "Zz",
|
||||
"isDeleted": false,
|
||||
@ -1249,8 +1249,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"1.30038",
|
||||
98,
|
||||
"1.33342",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
@ -1273,8 +1273,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 11,
|
||||
"width": "98.58579",
|
||||
"x": "0.70711",
|
||||
"width": 98,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
@ -1603,7 +1603,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "1.30038",
|
||||
"height": "1.33342",
|
||||
"id": "id725",
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
@ -1617,8 +1617,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"1.30038",
|
||||
98,
|
||||
"1.33342",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
@ -1641,8 +1641,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 11,
|
||||
"width": "98.58579",
|
||||
"x": "0.70711",
|
||||
"width": 98,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
@ -1759,7 +1759,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "11.27227",
|
||||
"height": "11.38145",
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
@ -1772,8 +1772,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"11.27227",
|
||||
"98.00000",
|
||||
"11.38145",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
@ -1794,8 +1794,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"width": "98.58579",
|
||||
"x": "0.70711",
|
||||
"width": "98.00000",
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
},
|
||||
"inserted": {
|
||||
@ -2308,7 +2308,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "374.05754",
|
||||
"height": "373.94428",
|
||||
"id": "id740",
|
||||
"index": "a2",
|
||||
"isDeleted": false,
|
||||
@ -2322,8 +2322,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
0,
|
||||
],
|
||||
[
|
||||
"502.78936",
|
||||
"-374.05754",
|
||||
"502.66843",
|
||||
"-373.94428",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
@ -2342,9 +2342,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 10,
|
||||
"width": "502.78936",
|
||||
"x": "-0.83465",
|
||||
"y": "-36.58211",
|
||||
"width": "502.66843",
|
||||
"x": "-0.74818",
|
||||
"y": "-36.64616",
|
||||
}
|
||||
`;
|
||||
|
||||
@ -15033,7 +15033,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
98,
|
||||
0,
|
||||
],
|
||||
],
|
||||
@ -15053,8 +15053,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 10,
|
||||
"width": "98.58579",
|
||||
"x": "0.70711",
|
||||
"width": 98,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
@ -15734,7 +15734,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
98,
|
||||
0,
|
||||
],
|
||||
],
|
||||
@ -15754,8 +15754,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 10,
|
||||
"width": "98.58579",
|
||||
"x": "0.70711",
|
||||
"width": 98,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
@ -16354,7 +16354,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
98,
|
||||
0,
|
||||
],
|
||||
],
|
||||
@ -16374,8 +16374,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 10,
|
||||
"width": "98.58579",
|
||||
"x": "0.70711",
|
||||
"width": 98,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
@ -16972,7 +16972,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
98,
|
||||
0,
|
||||
],
|
||||
],
|
||||
@ -16992,8 +16992,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 10,
|
||||
"width": "98.58579",
|
||||
"x": "0.70711",
|
||||
"width": 98,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
@ -17690,7 +17690,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
98,
|
||||
0,
|
||||
],
|
||||
],
|
||||
@ -17710,8 +17710,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 11,
|
||||
"width": "98.58579",
|
||||
"x": "0.70711",
|
||||
"width": 98,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
|
@ -196,7 +196,7 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "87.29887",
|
||||
"height": "81.40630",
|
||||
"id": "id6",
|
||||
"index": "a2",
|
||||
"isDeleted": false,
|
||||
@ -210,8 +210,8 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
||||
0,
|
||||
],
|
||||
[
|
||||
"86.85786",
|
||||
"87.29887",
|
||||
"81.00000",
|
||||
"81.40630",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
@ -232,8 +232,8 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
||||
"updated": 1,
|
||||
"version": 11,
|
||||
"versionNonce": 1051383431,
|
||||
"width": "86.85786",
|
||||
"x": "107.07107",
|
||||
"y": "47.07107",
|
||||
"width": "81.00000",
|
||||
"x": "110.00000",
|
||||
"y": 50,
|
||||
}
|
||||
`;
|
||||
|
@ -2481,7 +2481,7 @@ exports[`regression tests > can drag element that covers another element, while
|
||||
"scrolledOutside": false,
|
||||
"searchMatches": null,
|
||||
"selectedElementIds": {
|
||||
"id3": true,
|
||||
"id0": true,
|
||||
},
|
||||
"selectedElementsAreBeingDragged": false,
|
||||
"selectedGroupIds": {},
|
||||
@ -2683,7 +2683,7 @@ exports[`regression tests > can drag element that covers another element, while
|
||||
"delta": Delta {
|
||||
"deleted": {
|
||||
"selectedElementIds": {
|
||||
"id3": true,
|
||||
"id0": true,
|
||||
},
|
||||
},
|
||||
"inserted": {
|
||||
@ -2697,7 +2697,7 @@ exports[`regression tests > can drag element that covers another element, while
|
||||
"added": {},
|
||||
"removed": {},
|
||||
"updated": {
|
||||
"id3": {
|
||||
"id0": {
|
||||
"deleted": {
|
||||
"x": 300,
|
||||
"y": 300,
|
||||
|
@ -115,9 +115,10 @@ describe("contextMenu element", () => {
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 3,
|
||||
clientY: 3,
|
||||
clientX: 30,
|
||||
clientY: 30,
|
||||
});
|
||||
|
||||
const contextMenu = UI.queryContextMenu();
|
||||
const contextMenuOptions =
|
||||
contextMenu?.querySelectorAll(".context-menu li");
|
||||
@ -304,12 +305,12 @@ describe("contextMenu element", () => {
|
||||
|
||||
it("selecting 'Copy styles' in context menu copies styles", () => {
|
||||
UI.clickTool("rectangle");
|
||||
mouse.down(10, 10);
|
||||
mouse.down(13, 10);
|
||||
mouse.up(20, 20);
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 3,
|
||||
clientX: 13,
|
||||
clientY: 3,
|
||||
});
|
||||
const contextMenu = UI.queryContextMenu();
|
||||
@ -389,12 +390,12 @@ describe("contextMenu element", () => {
|
||||
|
||||
it("selecting 'Delete' in context menu deletes element", () => {
|
||||
UI.clickTool("rectangle");
|
||||
mouse.down(10, 10);
|
||||
mouse.down(13, 10);
|
||||
mouse.up(20, 20);
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 3,
|
||||
clientX: 13,
|
||||
clientY: 3,
|
||||
});
|
||||
const contextMenu = UI.queryContextMenu();
|
||||
@ -405,12 +406,12 @@ describe("contextMenu element", () => {
|
||||
|
||||
it("selecting 'Add to library' in context menu adds element to library", async () => {
|
||||
UI.clickTool("rectangle");
|
||||
mouse.down(10, 10);
|
||||
mouse.down(13, 10);
|
||||
mouse.up(20, 20);
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 3,
|
||||
clientX: 13,
|
||||
clientY: 3,
|
||||
});
|
||||
const contextMenu = UI.queryContextMenu();
|
||||
@ -424,12 +425,12 @@ describe("contextMenu element", () => {
|
||||
|
||||
it("selecting 'Duplicate' in context menu duplicates element", () => {
|
||||
UI.clickTool("rectangle");
|
||||
mouse.down(10, 10);
|
||||
mouse.down(13, 10);
|
||||
mouse.up(20, 20);
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 3,
|
||||
clientX: 13,
|
||||
clientY: 3,
|
||||
});
|
||||
const contextMenu = UI.queryContextMenu();
|
||||
|
@ -73,6 +73,7 @@ const updatePath = (startPoint: GlobalPoint, points: LocalPoint[]) => {
|
||||
elementsSegments,
|
||||
intersectedElements: new Set(),
|
||||
enclosedElements: new Set(),
|
||||
hitThreshold: h.app.getElementHitThreshold(),
|
||||
});
|
||||
|
||||
act(() =>
|
||||
|
@ -124,8 +124,8 @@ describe("move element", () => {
|
||||
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
||||
expect([rectA.x, rectA.y]).toEqual([0, 0]);
|
||||
expect([rectB.x, rectB.y]).toEqual([201, 2]);
|
||||
expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[107.07, 47.07]]);
|
||||
expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([[86.86, 87.3]]);
|
||||
expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[110, 50]]);
|
||||
expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([[81, 81.4]]);
|
||||
|
||||
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
||||
});
|
||||
|
@ -704,7 +704,7 @@ describe("regression tests", () => {
|
||||
|
||||
// pointer down on rectangle
|
||||
mouse.reset();
|
||||
mouse.down(100, 100);
|
||||
mouse.down(110, 100); // Rectangle is rounded, there is no selection at the corner
|
||||
mouse.up(200, 200);
|
||||
|
||||
expect(API.getSelectedElement().type).toBe("rectangle");
|
||||
@ -989,6 +989,7 @@ describe("regression tests", () => {
|
||||
|
||||
// select rectangle
|
||||
mouse.reset();
|
||||
mouse.moveTo(30, 0); // Rectangle is rounded, there is no selection at the corner
|
||||
mouse.click();
|
||||
|
||||
// click on intersection between ellipse and rectangle
|
||||
@ -1155,6 +1156,7 @@ it(
|
||||
// Select first rectangle while keeping third one selected.
|
||||
// Third rectangle is selected because it was the last element to be created.
|
||||
mouse.reset();
|
||||
mouse.moveTo(30, 0);
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.click();
|
||||
});
|
||||
@ -1176,6 +1178,7 @@ it(
|
||||
|
||||
// Pointer down o first rectangle that is part of the group
|
||||
mouse.reset();
|
||||
mouse.moveTo(30, 0);
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.down();
|
||||
});
|
||||
|
@ -35,7 +35,7 @@ test("unselected bound arrow updates when rotating its target element", async ()
|
||||
expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
|
||||
expect(arrow.x).toBeCloseTo(-80);
|
||||
expect(arrow.y).toBeCloseTo(50);
|
||||
expect(arrow.width).toBeCloseTo(116.7, 1);
|
||||
expect(arrow.width).toBeCloseTo(110.7, 1);
|
||||
expect(arrow.height).toBeCloseTo(0);
|
||||
});
|
||||
|
||||
|
@ -682,7 +682,7 @@ describe("textWysiwyg", () => {
|
||||
expect(diamond.height).toBe(70);
|
||||
});
|
||||
|
||||
it("should bind text to container when double clicked on center of transparent container", async () => {
|
||||
it("should bind text to container when double clicked inside of the transparent container", async () => {
|
||||
const rectangle = API.createElement({
|
||||
type: "rectangle",
|
||||
x: 10,
|
||||
@ -693,7 +693,7 @@ describe("textWysiwyg", () => {
|
||||
});
|
||||
API.setElements([rectangle]);
|
||||
|
||||
mouse.doubleClickAt(rectangle.x + 10, rectangle.y + 10);
|
||||
mouse.doubleClickAt(rectangle.x + 20, rectangle.y + 20);
|
||||
expect(h.elements.length).toBe(2);
|
||||
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||
expect(text.type).toBe("text");
|
||||
|
@ -1,8 +1,8 @@
|
||||
import type { Bounds } from "@excalidraw/element";
|
||||
|
||||
import { isPoint, pointDistance, pointFrom } from "./point";
|
||||
import { isPoint, pointDistance, pointFrom, pointFromVector } from "./point";
|
||||
import { rectangle, rectangleIntersectLineSegment } from "./rectangle";
|
||||
import { vector } from "./vector";
|
||||
import { vector, vectorNormal, vectorNormalize, vectorScale } from "./vector";
|
||||
|
||||
import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types";
|
||||
|
||||
@ -303,3 +303,108 @@ function curveBounds<Point extends GlobalPoint | LocalPoint>(
|
||||
const y = [P0[1], P1[1], P2[1], P3[1]];
|
||||
return [Math.min(...x), Math.min(...y), Math.max(...x), Math.max(...y)];
|
||||
}
|
||||
|
||||
export function curveCatmullRomQuadraticApproxPoints(
|
||||
points: GlobalPoint[],
|
||||
tension = 0.5,
|
||||
) {
|
||||
if (points.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pointSets: [GlobalPoint, GlobalPoint][] = [];
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
const p0 = points[i - 1 < 0 ? 0 : i - 1];
|
||||
const p1 = points[i];
|
||||
const p2 = points[i + 1 >= points.length ? points.length - 1 : i + 1];
|
||||
const cpX = p1[0] + ((p2[0] - p0[0]) * tension) / 2;
|
||||
const cpY = p1[1] + ((p2[1] - p0[1]) * tension) / 2;
|
||||
|
||||
pointSets.push([
|
||||
pointFrom<GlobalPoint>(cpX, cpY),
|
||||
pointFrom<GlobalPoint>(p2[0], p2[1]),
|
||||
]);
|
||||
}
|
||||
|
||||
return pointSets;
|
||||
}
|
||||
|
||||
export function curveCatmullRomCubicApproxPoints<
|
||||
Point extends GlobalPoint | LocalPoint,
|
||||
>(points: Point[], tension = 0.5) {
|
||||
if (points.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pointSets: Curve<Point>[] = [];
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
const p0 = points[i - 1 < 0 ? 0 : i - 1];
|
||||
const p1 = points[i];
|
||||
const p2 = points[i + 1 >= points.length ? points.length - 1 : i + 1];
|
||||
const p3 = points[i + 2 >= points.length ? points.length - 1 : i + 2];
|
||||
const tangent1 = [(p2[0] - p0[0]) * tension, (p2[1] - p0[1]) * tension];
|
||||
const tangent2 = [(p3[0] - p1[0]) * tension, (p3[1] - p1[1]) * tension];
|
||||
const cp1x = p1[0] + tangent1[0] / 3;
|
||||
const cp1y = p1[1] + tangent1[1] / 3;
|
||||
const cp2x = p2[0] - tangent2[0] / 3;
|
||||
const cp2y = p2[1] - tangent2[1] / 3;
|
||||
|
||||
pointSets.push(
|
||||
curve(
|
||||
pointFrom(p1[0], p1[1]),
|
||||
pointFrom(cp1x, cp1y),
|
||||
pointFrom(cp2x, cp2y),
|
||||
pointFrom(p2[0], p2[1]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return pointSets;
|
||||
}
|
||||
|
||||
export function curveOffsetPoints(
|
||||
[p0, p1, p2, p3]: Curve<GlobalPoint>,
|
||||
offset: number,
|
||||
steps = 50,
|
||||
) {
|
||||
const offsetPoints = [];
|
||||
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const t = i / steps;
|
||||
const c = curve(p0, p1, p2, p3);
|
||||
const point = bezierEquation(c, t);
|
||||
const tangent = vectorNormalize(curveTangent(c, t));
|
||||
const normal = vectorNormal(tangent);
|
||||
|
||||
offsetPoints.push(pointFromVector(vectorScale(normal, offset), point));
|
||||
}
|
||||
|
||||
return offsetPoints;
|
||||
}
|
||||
|
||||
export function offsetPointsForQuadraticBezier(
|
||||
p0: GlobalPoint,
|
||||
p1: GlobalPoint,
|
||||
p2: GlobalPoint,
|
||||
offsetDist: number,
|
||||
steps = 50,
|
||||
) {
|
||||
const offsetPoints = [];
|
||||
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const t = i / steps;
|
||||
const t1 = 1 - t;
|
||||
const point = pointFrom<GlobalPoint>(
|
||||
t1 * t1 * p0[0] + 2 * t1 * t * p1[0] + t * t * p2[0],
|
||||
t1 * t1 * p0[1] + 2 * t1 * t * p1[1] + t * t * p2[1],
|
||||
);
|
||||
const tangentX = 2 * (1 - t) * (p1[0] - p0[0]) + 2 * t * (p2[0] - p1[0]);
|
||||
const tangentY = 2 * (1 - t) * (p1[1] - p0[1]) + 2 * t * (p2[1] - p1[1]);
|
||||
const tangent = vectorNormalize(vector(tangentX, tangentY));
|
||||
const normal = vectorNormal(tangent);
|
||||
|
||||
offsetPoints.push(pointFromVector(vectorScale(normal, offsetDist), point));
|
||||
}
|
||||
|
||||
return offsetPoints;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
export * from "./angle";
|
||||
export * from "./curve";
|
||||
export * from "./ellipse";
|
||||
export * from "./line";
|
||||
export * from "./point";
|
||||
export * from "./polygon";
|
||||
|
@ -21,13 +21,23 @@ export function vector(
|
||||
*
|
||||
* @param p The point to turn into a vector
|
||||
* @param origin The origin point in a given coordiante system
|
||||
* @returns The created vector from the point and the origin
|
||||
* @param threshold The threshold to consider the vector as 'undefined'
|
||||
* @param defaultValue The default value to return if the vector is 'undefined'
|
||||
* @returns The created vector from the point and the origin or default
|
||||
*/
|
||||
export function vectorFromPoint<Point extends GlobalPoint | LocalPoint>(
|
||||
p: Point,
|
||||
origin: Point = [0, 0] as Point,
|
||||
threshold?: number,
|
||||
defaultValue: Vector = [0, 1] as Vector,
|
||||
): Vector {
|
||||
return vector(p[0] - origin[0], p[1] - origin[1]);
|
||||
const vec = vector(p[0] - origin[0], p[1] - origin[1]);
|
||||
|
||||
if (threshold && vectorMagnitudeSq(vec) < threshold * threshold) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return vec;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,135 +0,0 @@
|
||||
import {
|
||||
lineSegment,
|
||||
pointFrom,
|
||||
polygonIncludesPoint,
|
||||
pointOnLineSegment,
|
||||
pointOnPolygon,
|
||||
polygonFromPoints,
|
||||
type GlobalPoint,
|
||||
type LocalPoint,
|
||||
type Polygon,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import type { Curve } from "@excalidraw/math";
|
||||
|
||||
import { pointInEllipse, pointOnEllipse } from "./shape";
|
||||
|
||||
import type { Polycurve, Polyline, GeometricShape } from "./shape";
|
||||
|
||||
// check if the given point is considered on the given shape's border
|
||||
export const isPointOnShape = <Point extends GlobalPoint | LocalPoint>(
|
||||
point: Point,
|
||||
shape: GeometricShape<Point>,
|
||||
tolerance = 0,
|
||||
) => {
|
||||
// get the distance from the given point to the given element
|
||||
// check if the distance is within the given epsilon range
|
||||
switch (shape.type) {
|
||||
case "polygon":
|
||||
return pointOnPolygon(point, shape.data, tolerance);
|
||||
case "ellipse":
|
||||
return pointOnEllipse(point, shape.data, tolerance);
|
||||
case "line":
|
||||
return pointOnLineSegment(point, shape.data, tolerance);
|
||||
case "polyline":
|
||||
return pointOnPolyline(point, shape.data, tolerance);
|
||||
case "curve":
|
||||
return pointOnCurve(point, shape.data, tolerance);
|
||||
case "polycurve":
|
||||
return pointOnPolycurve(point, shape.data, tolerance);
|
||||
default:
|
||||
throw Error(`shape ${shape} is not implemented`);
|
||||
}
|
||||
};
|
||||
|
||||
// check if the given point is considered inside the element's border
|
||||
export const isPointInShape = <Point extends GlobalPoint | LocalPoint>(
|
||||
point: Point,
|
||||
shape: GeometricShape<Point>,
|
||||
) => {
|
||||
switch (shape.type) {
|
||||
case "polygon":
|
||||
return polygonIncludesPoint(point, shape.data);
|
||||
case "line":
|
||||
return false;
|
||||
case "curve":
|
||||
return false;
|
||||
case "ellipse":
|
||||
return pointInEllipse(point, shape.data);
|
||||
case "polyline": {
|
||||
const polygon = polygonFromPoints(shape.data.flat());
|
||||
return polygonIncludesPoint(point, polygon);
|
||||
}
|
||||
case "polycurve": {
|
||||
return false;
|
||||
}
|
||||
default:
|
||||
throw Error(`shape ${shape} is not implemented`);
|
||||
}
|
||||
};
|
||||
|
||||
// check if the given element is in the given bounds
|
||||
export const isPointInBounds = <Point extends GlobalPoint | LocalPoint>(
|
||||
point: Point,
|
||||
bounds: Polygon<Point>,
|
||||
) => {
|
||||
return polygonIncludesPoint(point, bounds);
|
||||
};
|
||||
|
||||
const pointOnPolycurve = <Point extends LocalPoint | GlobalPoint>(
|
||||
point: Point,
|
||||
polycurve: Polycurve<Point>,
|
||||
tolerance: number,
|
||||
) => {
|
||||
return polycurve.some((curve) => pointOnCurve(point, curve, tolerance));
|
||||
};
|
||||
|
||||
const cubicBezierEquation = <Point extends LocalPoint | GlobalPoint>(
|
||||
curve: Curve<Point>,
|
||||
) => {
|
||||
const [p0, p1, p2, p3] = curve;
|
||||
// B(t) = p0 * (1-t)^3 + 3p1 * t * (1-t)^2 + 3p2 * t^2 * (1-t) + p3 * t^3
|
||||
return (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 polyLineFromCurve = <Point extends LocalPoint | GlobalPoint>(
|
||||
curve: Curve<Point>,
|
||||
segments = 10,
|
||||
): Polyline<Point> => {
|
||||
const equation = cubicBezierEquation(curve);
|
||||
let startingPoint = [equation(0, 0), equation(0, 1)] as Point;
|
||||
const lineSegments: Polyline<Point> = [];
|
||||
let t = 0;
|
||||
const increment = 1 / segments;
|
||||
|
||||
for (let i = 0; i < segments; i++) {
|
||||
t += increment;
|
||||
if (t <= 1) {
|
||||
const nextPoint: Point = pointFrom(equation(t, 0), equation(t, 1));
|
||||
lineSegments.push(lineSegment(startingPoint, nextPoint));
|
||||
startingPoint = nextPoint;
|
||||
}
|
||||
}
|
||||
|
||||
return lineSegments;
|
||||
};
|
||||
|
||||
export const pointOnCurve = <Point extends LocalPoint | GlobalPoint>(
|
||||
point: Point,
|
||||
curve: Curve<Point>,
|
||||
threshold: number,
|
||||
) => {
|
||||
return pointOnPolyline(point, polyLineFromCurve(curve), threshold);
|
||||
};
|
||||
|
||||
export const pointOnPolyline = <Point extends LocalPoint | GlobalPoint>(
|
||||
point: Point,
|
||||
polyline: Polyline<Point>,
|
||||
threshold = 10e-5,
|
||||
) => {
|
||||
return polyline.some((line) => pointOnLineSegment(point, line, threshold));
|
||||
};
|
@ -1,90 +0,0 @@
|
||||
import {
|
||||
curve,
|
||||
degreesToRadians,
|
||||
lineSegment,
|
||||
lineSegmentRotate,
|
||||
pointFrom,
|
||||
pointRotateDegs,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import type { Curve, Degrees, GlobalPoint } from "@excalidraw/math";
|
||||
|
||||
import { pointOnCurve, pointOnPolyline } from "../src/collision";
|
||||
|
||||
import type { Polyline } from "../src/shape";
|
||||
|
||||
describe("point and curve", () => {
|
||||
const c: Curve<GlobalPoint> = curve(
|
||||
pointFrom(1.4, 1.65),
|
||||
pointFrom(1.9, 7.9),
|
||||
pointFrom(5.9, 1.65),
|
||||
pointFrom(6.44, 4.84),
|
||||
);
|
||||
|
||||
it("point on curve", () => {
|
||||
expect(pointOnCurve(c[0], c, 10e-5)).toBe(true);
|
||||
expect(pointOnCurve(c[3], c, 10e-5)).toBe(true);
|
||||
|
||||
expect(pointOnCurve(pointFrom(2, 4), c, 0.1)).toBe(true);
|
||||
expect(pointOnCurve(pointFrom(4, 4.4), c, 0.1)).toBe(true);
|
||||
expect(pointOnCurve(pointFrom(5.6, 3.85), c, 0.1)).toBe(true);
|
||||
|
||||
expect(pointOnCurve(pointFrom(5.6, 4), c, 0.1)).toBe(false);
|
||||
expect(pointOnCurve(c[1], c, 0.1)).toBe(false);
|
||||
expect(pointOnCurve(c[2], c, 0.1)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("point and polylines", () => {
|
||||
const polyline: Polyline<GlobalPoint> = [
|
||||
lineSegment(pointFrom(1, 0), pointFrom(1, 2)),
|
||||
lineSegment(pointFrom(1, 2), pointFrom(2, 2)),
|
||||
lineSegment(pointFrom(2, 2), pointFrom(2, 1)),
|
||||
lineSegment(pointFrom(2, 1), pointFrom(3, 1)),
|
||||
];
|
||||
|
||||
it("point on the line", () => {
|
||||
expect(pointOnPolyline(pointFrom(1, 0), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(pointFrom(1, 2), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(pointFrom(2, 2), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(pointFrom(2, 1), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(pointFrom(3, 1), polyline)).toBe(true);
|
||||
|
||||
expect(pointOnPolyline(pointFrom(1, 1), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(pointFrom(2, 1.5), polyline)).toBe(true);
|
||||
expect(pointOnPolyline(pointFrom(2.5, 1), polyline)).toBe(true);
|
||||
|
||||
expect(pointOnPolyline(pointFrom(0, 1), polyline)).toBe(false);
|
||||
expect(pointOnPolyline(pointFrom(2.1, 1.5), polyline)).toBe(false);
|
||||
});
|
||||
|
||||
it("point on the line with rotation", () => {
|
||||
const truePoints = [
|
||||
pointFrom(1, 0),
|
||||
pointFrom(1, 2),
|
||||
pointFrom(2, 2),
|
||||
pointFrom(2, 1),
|
||||
pointFrom(3, 1),
|
||||
];
|
||||
|
||||
truePoints.forEach((p) => {
|
||||
const rotation = (Math.random() * 360) as Degrees;
|
||||
const rotatedPoint = pointRotateDegs(p, pointFrom(0, 0), rotation);
|
||||
const rotatedPolyline = polyline.map((line) =>
|
||||
lineSegmentRotate(line, degreesToRadians(rotation), pointFrom(0, 0)),
|
||||
);
|
||||
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(true);
|
||||
});
|
||||
|
||||
const falsePoints = [pointFrom(0, 1), pointFrom(2.1, 1.5)];
|
||||
|
||||
falsePoints.forEach((p) => {
|
||||
const rotation = (Math.random() * 360) as Degrees;
|
||||
const rotatedPoint = pointRotateDegs(p, pointFrom(0, 0), rotation);
|
||||
const rotatedPolyline = polyline.map((line) =>
|
||||
lineSegmentRotate(line, degreesToRadians(rotation), pointFrom(0, 0)),
|
||||
);
|
||||
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user