feat: Precise hit testing (#9488)
This commit is contained in:
parent
56c05b3099
commit
ca1a4f25e7
@ -1,10 +1,12 @@
|
|||||||
import { average, pointFrom, type GlobalPoint } from "@excalidraw/math";
|
import { average, pointFrom, type GlobalPoint } from "@excalidraw/math";
|
||||||
|
import { getCenterForBounds, getElementBounds } from "@excalidraw/element";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
FontFamilyValues,
|
FontFamilyValues,
|
||||||
FontString,
|
FontString,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
|
ElementsMap,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@ -1240,16 +1242,13 @@ export const castArray = <T>(value: T | T[]): T[] =>
|
|||||||
|
|
||||||
export const elementCenterPoint = (
|
export const elementCenterPoint = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
xOffset: number = 0,
|
xOffset: number = 0,
|
||||||
yOffset: number = 0,
|
yOffset: number = 0,
|
||||||
) => {
|
) => {
|
||||||
const { x, y, width, height } = element;
|
const [x, y] = getCenterForBounds(getElementBounds(element, elementsMap));
|
||||||
|
|
||||||
const centerXPoint = x + width / 2 + xOffset;
|
return pointFrom<GlobalPoint>(x + xOffset, y + yOffset);
|
||||||
|
|
||||||
const centerYPoint = y + height / 2 + yOffset;
|
|
||||||
|
|
||||||
return pointFrom<GlobalPoint>(centerXPoint, centerYPoint);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** hack for Array.isArray type guard not working with readonly value[] */
|
/** hack for Array.isArray type guard not working with readonly value[] */
|
||||||
|
@ -1,8 +1,17 @@
|
|||||||
import { simplify } from "points-on-curve";
|
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 { ROUGHNESS, isTransparent, assertNever } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import { RoughGenerator } from "roughjs/bin/generator";
|
||||||
|
|
||||||
|
import type { GlobalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import type { EmbedsValidationStatus } from "@excalidraw/excalidraw/types";
|
import type { EmbedsValidationStatus } from "@excalidraw/excalidraw/types";
|
||||||
@ -20,7 +29,12 @@ import { headingForPointIsHorizontal } from "./heading";
|
|||||||
|
|
||||||
import { canChangeRoundness } from "./comparisons";
|
import { canChangeRoundness } from "./comparisons";
|
||||||
import { generateFreeDrawShape } from "./renderElement";
|
import { generateFreeDrawShape } from "./renderElement";
|
||||||
import { getArrowheadPoints, getDiamondPoints } from "./bounds";
|
import {
|
||||||
|
getArrowheadPoints,
|
||||||
|
getCenterForBounds,
|
||||||
|
getDiamondPoints,
|
||||||
|
getElementBounds,
|
||||||
|
} from "./bounds";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
@ -28,10 +42,11 @@ import type {
|
|||||||
ExcalidrawSelectionElement,
|
ExcalidrawSelectionElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
Arrowhead,
|
Arrowhead,
|
||||||
|
ExcalidrawFreeDrawElement,
|
||||||
|
ElementsMap,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import type { Drawable, Options } from "roughjs/bin/core";
|
import type { Drawable, Options } from "roughjs/bin/core";
|
||||||
import type { RoughGenerator } from "roughjs/bin/generator";
|
|
||||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||||
|
|
||||||
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
||||||
@ -303,6 +318,172 @@ const getArrowheadShapes = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const generateLinearCollisionShape = (
|
||||||
|
element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
) => {
|
||||||
|
const generator = new RoughGenerator();
|
||||||
|
const options: Options = {
|
||||||
|
seed: element.seed,
|
||||||
|
disableMultiStroke: true,
|
||||||
|
disableMultiStrokeFill: true,
|
||||||
|
roughness: 0,
|
||||||
|
preserveVertices: true,
|
||||||
|
};
|
||||||
|
const center = getCenterForBounds(
|
||||||
|
getElementBounds(element, elementsMap, 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)];
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
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)
|
||||||
|
.map((op, i) => {
|
||||||
|
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(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the roughjs shape for given element.
|
* Generates the roughjs shape for given element.
|
||||||
*
|
*
|
||||||
|
@ -27,8 +27,6 @@ import {
|
|||||||
PRECISION,
|
PRECISION,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import { isPointOnShape } from "@excalidraw/utils/collision";
|
|
||||||
|
|
||||||
import type { LocalPoint, Radians } from "@excalidraw/math";
|
import type { LocalPoint, Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||||
@ -41,7 +39,7 @@ import {
|
|||||||
doBoundsIntersect,
|
doBoundsIntersect,
|
||||||
} from "./bounds";
|
} from "./bounds";
|
||||||
import { intersectElementWithLineSegment } from "./collision";
|
import { intersectElementWithLineSegment } from "./collision";
|
||||||
import { distanceToBindableElement } from "./distance";
|
import { distanceToElement } from "./distance";
|
||||||
import {
|
import {
|
||||||
headingForPointFromElement,
|
headingForPointFromElement,
|
||||||
headingIsHorizontal,
|
headingIsHorizontal,
|
||||||
@ -63,7 +61,7 @@ import {
|
|||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
|
|
||||||
import { aabbForElement, getElementShape, pointInsideBounds } from "./shapes";
|
import { aabbForElement } from "./shapes";
|
||||||
import { updateElbowArrowPoints } from "./elbowArrow";
|
import { updateElbowArrowPoints } from "./elbowArrow";
|
||||||
|
|
||||||
import type { Scene } from "./Scene";
|
import type { Scene } from "./Scene";
|
||||||
@ -109,7 +107,6 @@ export const isBindingEnabled = (appState: AppState): boolean => {
|
|||||||
|
|
||||||
export const FIXED_BINDING_DISTANCE = 5;
|
export const FIXED_BINDING_DISTANCE = 5;
|
||||||
export const BINDING_HIGHLIGHT_THICKNESS = 10;
|
export const BINDING_HIGHLIGHT_THICKNESS = 10;
|
||||||
export const BINDING_HIGHLIGHT_OFFSET = 4;
|
|
||||||
|
|
||||||
const getNonDeletedElements = (
|
const getNonDeletedElements = (
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
@ -131,6 +128,7 @@ export const bindOrUnbindLinearElement = (
|
|||||||
endBindingElement: ExcalidrawBindableElement | null | "keep",
|
endBindingElement: ExcalidrawBindableElement | null | "keep",
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
): void => {
|
): void => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const boundToElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
|
const boundToElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
|
||||||
const unboundFromElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
|
const unboundFromElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
|
||||||
bindOrUnbindLinearElementEdge(
|
bindOrUnbindLinearElementEdge(
|
||||||
@ -141,6 +139,7 @@ export const bindOrUnbindLinearElement = (
|
|||||||
boundToElementIds,
|
boundToElementIds,
|
||||||
unboundFromElementIds,
|
unboundFromElementIds,
|
||||||
scene,
|
scene,
|
||||||
|
elementsMap,
|
||||||
);
|
);
|
||||||
bindOrUnbindLinearElementEdge(
|
bindOrUnbindLinearElementEdge(
|
||||||
linearElement,
|
linearElement,
|
||||||
@ -150,6 +149,7 @@ export const bindOrUnbindLinearElement = (
|
|||||||
boundToElementIds,
|
boundToElementIds,
|
||||||
unboundFromElementIds,
|
unboundFromElementIds,
|
||||||
scene,
|
scene,
|
||||||
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
const onlyUnbound = Array.from(unboundFromElementIds).filter(
|
const onlyUnbound = Array.from(unboundFromElementIds).filter(
|
||||||
@ -176,6 +176,7 @@ const bindOrUnbindLinearElementEdge = (
|
|||||||
// Is mutated
|
// Is mutated
|
||||||
unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>,
|
unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>,
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
): void => {
|
): void => {
|
||||||
// "keep" is for method chaining convenience, a "no-op", so just bail out
|
// "keep" is for method chaining convenience, a "no-op", so just bail out
|
||||||
if (bindableElement === "keep") {
|
if (bindableElement === "keep") {
|
||||||
@ -216,43 +217,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 = (
|
const getOriginalBindingsIfStillCloseToArrowEnds = (
|
||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
zoom?: AppState["zoom"],
|
zoom?: AppState["zoom"],
|
||||||
): (NonDeleted<ExcalidrawElement> | null)[] =>
|
): (NonDeleted<ExcalidrawElement> | null)[] =>
|
||||||
["start", "end"].map((edge) =>
|
(["start", "end"] as const).map((edge) => {
|
||||||
getOriginalBindingIfStillCloseOfLinearElementEdge(
|
const coors = getLinearElementEdgeCoors(linearElement, edge, elementsMap);
|
||||||
linearElement,
|
const elementId =
|
||||||
edge as "start" | "end",
|
edge === "start"
|
||||||
elementsMap,
|
? linearElement.startBinding?.elementId
|
||||||
zoom,
|
: linearElement.endBinding?.elementId;
|
||||||
),
|
if (elementId) {
|
||||||
);
|
const element = elementsMap.get(elementId);
|
||||||
|
if (
|
||||||
|
isBindableElement(element) &&
|
||||||
|
bindingBorderTest(element, coors, elementsMap, zoom)
|
||||||
|
) {
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
const getBindingStrategyForDraggingArrowEndpoints = (
|
const getBindingStrategyForDraggingArrowEndpoints = (
|
||||||
selectedElement: NonDeleted<ExcalidrawLinearElement>,
|
selectedElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
@ -268,7 +255,7 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
|||||||
const endDragged = draggingPoints.findIndex((i) => i === endIdx) > -1;
|
const endDragged = draggingPoints.findIndex((i) => i === endIdx) > -1;
|
||||||
const start = startDragged
|
const start = startDragged
|
||||||
? isBindingEnabled
|
? isBindingEnabled
|
||||||
? getElligibleElementForBindingElement(
|
? getEligibleElementForBindingElement(
|
||||||
selectedElement,
|
selectedElement,
|
||||||
"start",
|
"start",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@ -279,7 +266,7 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
|||||||
: "keep";
|
: "keep";
|
||||||
const end = endDragged
|
const end = endDragged
|
||||||
? isBindingEnabled
|
? isBindingEnabled
|
||||||
? getElligibleElementForBindingElement(
|
? getEligibleElementForBindingElement(
|
||||||
selectedElement,
|
selectedElement,
|
||||||
"end",
|
"end",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@ -311,7 +298,7 @@ const getBindingStrategyForDraggingArrowOrJoints = (
|
|||||||
);
|
);
|
||||||
const start = startIsClose
|
const start = startIsClose
|
||||||
? isBindingEnabled
|
? isBindingEnabled
|
||||||
? getElligibleElementForBindingElement(
|
? getEligibleElementForBindingElement(
|
||||||
selectedElement,
|
selectedElement,
|
||||||
"start",
|
"start",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@ -322,7 +309,7 @@ const getBindingStrategyForDraggingArrowOrJoints = (
|
|||||||
: null;
|
: null;
|
||||||
const end = endIsClose
|
const end = endIsClose
|
||||||
? isBindingEnabled
|
? isBindingEnabled
|
||||||
? getElligibleElementForBindingElement(
|
? getEligibleElementForBindingElement(
|
||||||
selectedElement,
|
selectedElement,
|
||||||
"end",
|
"end",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@ -441,22 +428,13 @@ export const maybeBindLinearElement = (
|
|||||||
const normalizePointBinding = (
|
const normalizePointBinding = (
|
||||||
binding: { focus: number; gap: number },
|
binding: { focus: number; gap: number },
|
||||||
hoveredElement: ExcalidrawBindableElement,
|
hoveredElement: ExcalidrawBindableElement,
|
||||||
) => {
|
) => ({
|
||||||
let gap = binding.gap;
|
...binding,
|
||||||
const maxGap = maxBindingGap(
|
gap: Math.min(
|
||||||
hoveredElement,
|
binding.gap,
|
||||||
hoveredElement.width,
|
maxBindingGap(hoveredElement, hoveredElement.width, hoveredElement.height),
|
||||||
hoveredElement.height,
|
),
|
||||||
);
|
});
|
||||||
|
|
||||||
if (gap > maxGap) {
|
|
||||||
gap = BINDING_HIGHLIGHT_THICKNESS + BINDING_HIGHLIGHT_OFFSET;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...binding,
|
|
||||||
gap,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const bindLinearElement = (
|
export const bindLinearElement = (
|
||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
@ -488,6 +466,7 @@ export const bindLinearElement = (
|
|||||||
linearElement,
|
linearElement,
|
||||||
hoveredElement,
|
hoveredElement,
|
||||||
startOrEnd,
|
startOrEnd,
|
||||||
|
scene.getNonDeletedElementsMap(),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -703,8 +682,13 @@ const calculateFocusAndGap = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
focus: determineFocusDistance(hoveredElement, adjacentPoint, edgePoint),
|
focus: determineFocusDistance(
|
||||||
gap: Math.max(1, distanceToBindableElement(hoveredElement, edgePoint)),
|
hoveredElement,
|
||||||
|
elementsMap,
|
||||||
|
adjacentPoint,
|
||||||
|
edgePoint,
|
||||||
|
),
|
||||||
|
gap: Math.max(1, distanceToElement(hoveredElement, elementsMap, edgePoint)),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -874,6 +858,7 @@ export const getHeadingForElbowArrowSnap = (
|
|||||||
bindableElement: ExcalidrawBindableElement | undefined | null,
|
bindableElement: ExcalidrawBindableElement | undefined | null,
|
||||||
aabb: Bounds | undefined | null,
|
aabb: Bounds | undefined | null,
|
||||||
origPoint: GlobalPoint,
|
origPoint: GlobalPoint,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
zoom?: AppState["zoom"],
|
zoom?: AppState["zoom"],
|
||||||
): Heading => {
|
): Heading => {
|
||||||
const otherPointHeading = vectorToHeading(vectorFromPoint(otherPoint, p));
|
const otherPointHeading = vectorToHeading(vectorFromPoint(otherPoint, p));
|
||||||
@ -882,11 +867,16 @@ export const getHeadingForElbowArrowSnap = (
|
|||||||
return otherPointHeading;
|
return otherPointHeading;
|
||||||
}
|
}
|
||||||
|
|
||||||
const distance = getDistanceForBinding(origPoint, bindableElement, zoom);
|
const distance = getDistanceForBinding(
|
||||||
|
origPoint,
|
||||||
|
bindableElement,
|
||||||
|
elementsMap,
|
||||||
|
zoom,
|
||||||
|
);
|
||||||
|
|
||||||
if (!distance) {
|
if (!distance) {
|
||||||
return vectorToHeading(
|
return vectorToHeading(
|
||||||
vectorFromPoint(p, elementCenterPoint(bindableElement)),
|
vectorFromPoint(p, elementCenterPoint(bindableElement, elementsMap)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -896,9 +886,10 @@ export const getHeadingForElbowArrowSnap = (
|
|||||||
const getDistanceForBinding = (
|
const getDistanceForBinding = (
|
||||||
point: Readonly<GlobalPoint>,
|
point: Readonly<GlobalPoint>,
|
||||||
bindableElement: ExcalidrawBindableElement,
|
bindableElement: ExcalidrawBindableElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
zoom?: AppState["zoom"],
|
zoom?: AppState["zoom"],
|
||||||
) => {
|
) => {
|
||||||
const distance = distanceToBindableElement(bindableElement, point);
|
const distance = distanceToElement(bindableElement, elementsMap, point);
|
||||||
const bindDistance = maxBindingGap(
|
const bindDistance = maxBindingGap(
|
||||||
bindableElement,
|
bindableElement,
|
||||||
bindableElement.width,
|
bindableElement.width,
|
||||||
@ -913,12 +904,13 @@ export const bindPointToSnapToElementOutline = (
|
|||||||
arrow: ExcalidrawElbowArrowElement,
|
arrow: ExcalidrawElbowArrowElement,
|
||||||
bindableElement: ExcalidrawBindableElement,
|
bindableElement: ExcalidrawBindableElement,
|
||||||
startOrEnd: "start" | "end",
|
startOrEnd: "start" | "end",
|
||||||
|
elementsMap: ElementsMap,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
if (isDevEnv() || isTestEnv()) {
|
if (isDevEnv() || isTestEnv()) {
|
||||||
invariant(arrow.points.length > 1, "Arrow should have at least 2 points");
|
invariant(arrow.points.length > 1, "Arrow should have at least 2 points");
|
||||||
}
|
}
|
||||||
|
|
||||||
const aabb = aabbForElement(bindableElement);
|
const aabb = aabbForElement(bindableElement, elementsMap);
|
||||||
const localP =
|
const localP =
|
||||||
arrow.points[startOrEnd === "start" ? 0 : arrow.points.length - 1];
|
arrow.points[startOrEnd === "start" ? 0 : arrow.points.length - 1];
|
||||||
const globalP = pointFrom<GlobalPoint>(
|
const globalP = pointFrom<GlobalPoint>(
|
||||||
@ -926,7 +918,7 @@ export const bindPointToSnapToElementOutline = (
|
|||||||
arrow.y + localP[1],
|
arrow.y + localP[1],
|
||||||
);
|
);
|
||||||
const edgePoint = isRectanguloidElement(bindableElement)
|
const edgePoint = isRectanguloidElement(bindableElement)
|
||||||
? avoidRectangularCorner(bindableElement, globalP)
|
? avoidRectangularCorner(bindableElement, elementsMap, globalP)
|
||||||
: globalP;
|
: globalP;
|
||||||
const elbowed = isElbowArrow(arrow);
|
const elbowed = isElbowArrow(arrow);
|
||||||
const center = getCenterForBounds(aabb);
|
const center = getCenterForBounds(aabb);
|
||||||
@ -945,26 +937,31 @@ export const bindPointToSnapToElementOutline = (
|
|||||||
const isHorizontal = headingIsHorizontal(
|
const isHorizontal = headingIsHorizontal(
|
||||||
headingForPointFromElement(bindableElement, aabb, globalP),
|
headingForPointFromElement(bindableElement, aabb, globalP),
|
||||||
);
|
);
|
||||||
|
const snapPoint = snapToMid(bindableElement, elementsMap, edgePoint);
|
||||||
const otherPoint = pointFrom<GlobalPoint>(
|
const otherPoint = pointFrom<GlobalPoint>(
|
||||||
isHorizontal ? center[0] : edgePoint[0],
|
isHorizontal ? center[0] : snapPoint[0],
|
||||||
!isHorizontal ? center[1] : edgePoint[1],
|
!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(
|
intersection = intersectElementWithLineSegment(
|
||||||
bindableElement,
|
bindableElement,
|
||||||
lineSegment(
|
elementsMap,
|
||||||
otherPoint,
|
intersector,
|
||||||
pointFromVector(
|
FIXED_BINDING_DISTANCE,
|
||||||
vectorScale(
|
).sort(pointDistanceSq)[0];
|
||||||
vectorNormalize(vectorFromPoint(edgePoint, otherPoint)),
|
|
||||||
Math.max(bindableElement.width, bindableElement.height) * 2,
|
|
||||||
),
|
|
||||||
otherPoint,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)[0];
|
|
||||||
} else {
|
} else {
|
||||||
intersection = intersectElementWithLineSegment(
|
intersection = intersectElementWithLineSegment(
|
||||||
bindableElement,
|
bindableElement,
|
||||||
|
elementsMap,
|
||||||
lineSegment(
|
lineSegment(
|
||||||
adjacentPoint,
|
adjacentPoint,
|
||||||
pointFromVector(
|
pointFromVector(
|
||||||
@ -991,31 +988,15 @@ export const bindPointToSnapToElementOutline = (
|
|||||||
return edgePoint;
|
return edgePoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (elbowed) {
|
return elbowed ? intersection : edgePoint;
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const avoidRectangularCorner = (
|
export const avoidRectangularCorner = (
|
||||||
element: ExcalidrawBindableElement,
|
element: ExcalidrawBindableElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
const center = elementCenterPoint(element);
|
const center = elementCenterPoint(element, elementsMap);
|
||||||
const nonRotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
const nonRotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
||||||
|
|
||||||
if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) {
|
if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) {
|
||||||
@ -1108,35 +1089,34 @@ export const avoidRectangularCorner = (
|
|||||||
|
|
||||||
export const snapToMid = (
|
export const snapToMid = (
|
||||||
element: ExcalidrawBindableElement,
|
element: ExcalidrawBindableElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
tolerance: number = 0.05,
|
tolerance: number = 0.05,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
const { x, y, width, height, angle } = element;
|
const { x, y, width, height, angle } = element;
|
||||||
|
const center = elementCenterPoint(element, elementsMap, -0.1, -0.1);
|
||||||
const center = elementCenterPoint(element, -0.1, -0.1);
|
|
||||||
|
|
||||||
const nonRotated = pointRotateRads(p, center, -angle as Radians);
|
const nonRotated = pointRotateRads(p, center, -angle as Radians);
|
||||||
|
|
||||||
// snap-to-center point is adaptive to element size, but we don't want to go
|
// snap-to-center point is adaptive to element size, but we don't want to go
|
||||||
// above and below certain px distance
|
// above and below certain px distance
|
||||||
const verticalThrehsold = clamp(tolerance * height, 5, 80);
|
const verticalThreshold = clamp(tolerance * height, 5, 80);
|
||||||
const horizontalThrehsold = clamp(tolerance * width, 5, 80);
|
const horizontalThreshold = clamp(tolerance * width, 5, 80);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
nonRotated[0] <= x + width / 2 &&
|
nonRotated[0] <= x + width / 2 &&
|
||||||
nonRotated[1] > center[1] - verticalThrehsold &&
|
nonRotated[1] > center[1] - verticalThreshold &&
|
||||||
nonRotated[1] < center[1] + verticalThrehsold
|
nonRotated[1] < center[1] + verticalThreshold
|
||||||
) {
|
) {
|
||||||
// LEFT
|
// LEFT
|
||||||
return pointRotateRads(
|
return pointRotateRads<GlobalPoint>(
|
||||||
pointFrom(x - FIXED_BINDING_DISTANCE, center[1]),
|
pointFrom(x - FIXED_BINDING_DISTANCE, center[1]),
|
||||||
center,
|
center,
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
} else if (
|
} else if (
|
||||||
nonRotated[1] <= y + height / 2 &&
|
nonRotated[1] <= y + height / 2 &&
|
||||||
nonRotated[0] > center[0] - horizontalThrehsold &&
|
nonRotated[0] > center[0] - horizontalThreshold &&
|
||||||
nonRotated[0] < center[0] + horizontalThrehsold
|
nonRotated[0] < center[0] + horizontalThreshold
|
||||||
) {
|
) {
|
||||||
// TOP
|
// TOP
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
@ -1146,8 +1126,8 @@ export const snapToMid = (
|
|||||||
);
|
);
|
||||||
} else if (
|
} else if (
|
||||||
nonRotated[0] >= x + width / 2 &&
|
nonRotated[0] >= x + width / 2 &&
|
||||||
nonRotated[1] > center[1] - verticalThrehsold &&
|
nonRotated[1] > center[1] - verticalThreshold &&
|
||||||
nonRotated[1] < center[1] + verticalThrehsold
|
nonRotated[1] < center[1] + verticalThreshold
|
||||||
) {
|
) {
|
||||||
// RIGHT
|
// RIGHT
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
@ -1157,8 +1137,8 @@ export const snapToMid = (
|
|||||||
);
|
);
|
||||||
} else if (
|
} else if (
|
||||||
nonRotated[1] >= y + height / 2 &&
|
nonRotated[1] >= y + height / 2 &&
|
||||||
nonRotated[0] > center[0] - horizontalThrehsold &&
|
nonRotated[0] > center[0] - horizontalThreshold &&
|
||||||
nonRotated[0] < center[0] + horizontalThrehsold
|
nonRotated[0] < center[0] + horizontalThreshold
|
||||||
) {
|
) {
|
||||||
// DOWN
|
// DOWN
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
@ -1167,7 +1147,7 @@ export const snapToMid = (
|
|||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
} else if (element.type === "diamond") {
|
} else if (element.type === "diamond") {
|
||||||
const distance = FIXED_BINDING_DISTANCE - 1;
|
const distance = FIXED_BINDING_DISTANCE;
|
||||||
const topLeft = pointFrom<GlobalPoint>(
|
const topLeft = pointFrom<GlobalPoint>(
|
||||||
x + width / 4 - distance,
|
x + width / 4 - distance,
|
||||||
y + height / 4 - distance,
|
y + height / 4 - distance,
|
||||||
@ -1184,27 +1164,28 @@ export const snapToMid = (
|
|||||||
x + (3 * width) / 4 + distance,
|
x + (3 * width) / 4 + distance,
|
||||||
y + (3 * height) / 4 + distance,
|
y + (3 * height) / 4 + distance,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
pointDistance(topLeft, nonRotated) <
|
pointDistance(topLeft, nonRotated) <
|
||||||
Math.max(horizontalThrehsold, verticalThrehsold)
|
Math.max(horizontalThreshold, verticalThreshold)
|
||||||
) {
|
) {
|
||||||
return pointRotateRads(topLeft, center, angle);
|
return pointRotateRads(topLeft, center, angle);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
pointDistance(topRight, nonRotated) <
|
pointDistance(topRight, nonRotated) <
|
||||||
Math.max(horizontalThrehsold, verticalThrehsold)
|
Math.max(horizontalThreshold, verticalThreshold)
|
||||||
) {
|
) {
|
||||||
return pointRotateRads(topRight, center, angle);
|
return pointRotateRads(topRight, center, angle);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
pointDistance(bottomLeft, nonRotated) <
|
pointDistance(bottomLeft, nonRotated) <
|
||||||
Math.max(horizontalThrehsold, verticalThrehsold)
|
Math.max(horizontalThreshold, verticalThreshold)
|
||||||
) {
|
) {
|
||||||
return pointRotateRads(bottomLeft, center, angle);
|
return pointRotateRads(bottomLeft, center, angle);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
pointDistance(bottomRight, nonRotated) <
|
pointDistance(bottomRight, nonRotated) <
|
||||||
Math.max(horizontalThrehsold, verticalThrehsold)
|
Math.max(horizontalThreshold, verticalThreshold)
|
||||||
) {
|
) {
|
||||||
return pointRotateRads(bottomRight, center, angle);
|
return pointRotateRads(bottomRight, center, angle);
|
||||||
}
|
}
|
||||||
@ -1239,8 +1220,9 @@ const updateBoundPoint = (
|
|||||||
linearElement,
|
linearElement,
|
||||||
bindableElement,
|
bindableElement,
|
||||||
startOrEnd === "startBinding" ? "start" : "end",
|
startOrEnd === "startBinding" ? "start" : "end",
|
||||||
|
elementsMap,
|
||||||
).fixedPoint;
|
).fixedPoint;
|
||||||
const globalMidPoint = elementCenterPoint(bindableElement);
|
const globalMidPoint = elementCenterPoint(bindableElement, elementsMap);
|
||||||
const global = pointFrom<GlobalPoint>(
|
const global = pointFrom<GlobalPoint>(
|
||||||
bindableElement.x + fixedPoint[0] * bindableElement.width,
|
bindableElement.x + fixedPoint[0] * bindableElement.width,
|
||||||
bindableElement.y + fixedPoint[1] * bindableElement.height,
|
bindableElement.y + fixedPoint[1] * bindableElement.height,
|
||||||
@ -1266,6 +1248,7 @@ const updateBoundPoint = (
|
|||||||
);
|
);
|
||||||
const focusPointAbsolute = determineFocusPoint(
|
const focusPointAbsolute = determineFocusPoint(
|
||||||
bindableElement,
|
bindableElement,
|
||||||
|
elementsMap,
|
||||||
binding.focus,
|
binding.focus,
|
||||||
adjacentPoint,
|
adjacentPoint,
|
||||||
);
|
);
|
||||||
@ -1284,7 +1267,7 @@ const updateBoundPoint = (
|
|||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
const center = elementCenterPoint(bindableElement);
|
const center = elementCenterPoint(bindableElement, elementsMap);
|
||||||
const interceptorLength =
|
const interceptorLength =
|
||||||
pointDistance(adjacentPoint, edgePointAbsolute) +
|
pointDistance(adjacentPoint, edgePointAbsolute) +
|
||||||
pointDistance(adjacentPoint, center) +
|
pointDistance(adjacentPoint, center) +
|
||||||
@ -1292,6 +1275,7 @@ const updateBoundPoint = (
|
|||||||
const intersections = [
|
const intersections = [
|
||||||
...intersectElementWithLineSegment(
|
...intersectElementWithLineSegment(
|
||||||
bindableElement,
|
bindableElement,
|
||||||
|
elementsMap,
|
||||||
lineSegment<GlobalPoint>(
|
lineSegment<GlobalPoint>(
|
||||||
adjacentPoint,
|
adjacentPoint,
|
||||||
pointFromVector(
|
pointFromVector(
|
||||||
@ -1342,6 +1326,7 @@ export const calculateFixedPointForElbowArrowBinding = (
|
|||||||
linearElement: NonDeleted<ExcalidrawElbowArrowElement>,
|
linearElement: NonDeleted<ExcalidrawElbowArrowElement>,
|
||||||
hoveredElement: ExcalidrawBindableElement,
|
hoveredElement: ExcalidrawBindableElement,
|
||||||
startOrEnd: "start" | "end",
|
startOrEnd: "start" | "end",
|
||||||
|
elementsMap: ElementsMap,
|
||||||
): { fixedPoint: FixedPoint } => {
|
): { fixedPoint: FixedPoint } => {
|
||||||
const bounds = [
|
const bounds = [
|
||||||
hoveredElement.x,
|
hoveredElement.x,
|
||||||
@ -1353,6 +1338,7 @@ export const calculateFixedPointForElbowArrowBinding = (
|
|||||||
linearElement,
|
linearElement,
|
||||||
hoveredElement,
|
hoveredElement,
|
||||||
startOrEnd,
|
startOrEnd,
|
||||||
|
elementsMap,
|
||||||
);
|
);
|
||||||
const globalMidPoint = pointFrom(
|
const globalMidPoint = pointFrom(
|
||||||
bounds[0] + (bounds[2] - bounds[0]) / 2,
|
bounds[0] + (bounds[2] - bounds[0]) / 2,
|
||||||
@ -1396,7 +1382,7 @@ const maybeCalculateNewGapWhenScaling = (
|
|||||||
return { ...currentBinding, gap: newGap };
|
return { ...currentBinding, gap: newGap };
|
||||||
};
|
};
|
||||||
|
|
||||||
const getElligibleElementForBindingElement = (
|
const getEligibleElementForBindingElement = (
|
||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
startOrEnd: "start" | "end",
|
startOrEnd: "start" | "end",
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
@ -1548,14 +1534,38 @@ export const bindingBorderTest = (
|
|||||||
zoom?: AppState["zoom"],
|
zoom?: AppState["zoom"],
|
||||||
fullShape?: boolean,
|
fullShape?: boolean,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
|
const p = pointFrom<GlobalPoint>(x, y);
|
||||||
const threshold = maxBindingGap(element, element.width, element.height, zoom);
|
const threshold = maxBindingGap(element, element.width, element.height, zoom);
|
||||||
|
const shouldTestInside =
|
||||||
|
// disable fullshape snapping for frame elements so we
|
||||||
|
// can bind to frame children
|
||||||
|
(fullShape || !isBindingFallthroughEnabled(element)) &&
|
||||||
|
!isFrameLikeElement(element);
|
||||||
|
|
||||||
const shape = getElementShape(element, elementsMap);
|
// PERF: Run a cheap test to see if the binding element
|
||||||
return (
|
// is even close to the element
|
||||||
isPointOnShape(pointFrom(x, y), shape, threshold) ||
|
const bounds = [
|
||||||
(fullShape === true &&
|
x - threshold,
|
||||||
pointInsideBounds(pointFrom(x, y), aabbForElement(element)))
|
y - threshold,
|
||||||
|
x + threshold,
|
||||||
|
y + threshold,
|
||||||
|
] as Bounds;
|
||||||
|
const elementBounds = getElementBounds(element, elementsMap);
|
||||||
|
if (!doBoundsIntersect(bounds, elementBounds)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do the intersection test against the element since it's close enough
|
||||||
|
const intersections = intersectElementWithLineSegment(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
lineSegment(elementCenterPoint(element, elementsMap), p),
|
||||||
);
|
);
|
||||||
|
const distance = distanceToElement(element, elementsMap, p);
|
||||||
|
|
||||||
|
return shouldTestInside
|
||||||
|
? intersections.length === 0 || distance <= threshold
|
||||||
|
: intersections.length > 0 && distance <= threshold;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const maxBindingGap = (
|
export const maxBindingGap = (
|
||||||
@ -1575,7 +1585,7 @@ export const maxBindingGap = (
|
|||||||
// bigger bindable boundary for bigger elements
|
// bigger bindable boundary for bigger elements
|
||||||
Math.min(0.25 * smallerDimension, 32),
|
Math.min(0.25 * smallerDimension, 32),
|
||||||
// keep in sync with the zoomed highlight
|
// keep in sync with the zoomed highlight
|
||||||
BINDING_HIGHLIGHT_THICKNESS / zoomValue + BINDING_HIGHLIGHT_OFFSET,
|
BINDING_HIGHLIGHT_THICKNESS / zoomValue + FIXED_BINDING_DISTANCE,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1586,12 +1596,13 @@ export const maxBindingGap = (
|
|||||||
// of the element.
|
// of the element.
|
||||||
const determineFocusDistance = (
|
const determineFocusDistance = (
|
||||||
element: ExcalidrawBindableElement,
|
element: ExcalidrawBindableElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
// Point on the line, in absolute coordinates
|
// Point on the line, in absolute coordinates
|
||||||
a: GlobalPoint,
|
a: GlobalPoint,
|
||||||
// Another point on the line, in absolute coordinates (closer to element)
|
// Another point on the line, in absolute coordinates (closer to element)
|
||||||
b: GlobalPoint,
|
b: GlobalPoint,
|
||||||
): number => {
|
): number => {
|
||||||
const center = elementCenterPoint(element);
|
const center = elementCenterPoint(element, elementsMap);
|
||||||
|
|
||||||
if (pointsEqual(a, b)) {
|
if (pointsEqual(a, b)) {
|
||||||
return 0;
|
return 0;
|
||||||
@ -1716,12 +1727,13 @@ const determineFocusDistance = (
|
|||||||
|
|
||||||
const determineFocusPoint = (
|
const determineFocusPoint = (
|
||||||
element: ExcalidrawBindableElement,
|
element: ExcalidrawBindableElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
// The oriented, relative distance from the center of `element` of the
|
// The oriented, relative distance from the center of `element` of the
|
||||||
// returned focusPoint
|
// returned focusPoint
|
||||||
focus: number,
|
focus: number,
|
||||||
adjacentPoint: GlobalPoint,
|
adjacentPoint: GlobalPoint,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
const center = elementCenterPoint(element);
|
const center = elementCenterPoint(element, elementsMap);
|
||||||
|
|
||||||
if (focus === 0) {
|
if (focus === 0) {
|
||||||
return center;
|
return center;
|
||||||
@ -2144,6 +2156,7 @@ export class BindableElement {
|
|||||||
export const getGlobalFixedPointForBindableElement = (
|
export const getGlobalFixedPointForBindableElement = (
|
||||||
fixedPointRatio: [number, number],
|
fixedPointRatio: [number, number],
|
||||||
element: ExcalidrawBindableElement,
|
element: ExcalidrawBindableElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
const [fixedX, fixedY] = normalizeFixedPoint(fixedPointRatio);
|
const [fixedX, fixedY] = normalizeFixedPoint(fixedPointRatio);
|
||||||
|
|
||||||
@ -2152,7 +2165,7 @@ export const getGlobalFixedPointForBindableElement = (
|
|||||||
element.x + element.width * fixedX,
|
element.x + element.width * fixedX,
|
||||||
element.y + element.height * fixedY,
|
element.y + element.height * fixedY,
|
||||||
),
|
),
|
||||||
elementCenterPoint(element),
|
elementCenterPoint(element, elementsMap),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -2176,6 +2189,7 @@ export const getGlobalFixedPoints = (
|
|||||||
? getGlobalFixedPointForBindableElement(
|
? getGlobalFixedPointForBindableElement(
|
||||||
arrow.startBinding.fixedPoint,
|
arrow.startBinding.fixedPoint,
|
||||||
startElement as ExcalidrawBindableElement,
|
startElement as ExcalidrawBindableElement,
|
||||||
|
elementsMap,
|
||||||
)
|
)
|
||||||
: pointFrom<GlobalPoint>(
|
: pointFrom<GlobalPoint>(
|
||||||
arrow.x + arrow.points[0][0],
|
arrow.x + arrow.points[0][0],
|
||||||
@ -2186,6 +2200,7 @@ export const getGlobalFixedPoints = (
|
|||||||
? getGlobalFixedPointForBindableElement(
|
? getGlobalFixedPointForBindableElement(
|
||||||
arrow.endBinding.fixedPoint,
|
arrow.endBinding.fixedPoint,
|
||||||
endElement as ExcalidrawBindableElement,
|
endElement as ExcalidrawBindableElement,
|
||||||
|
elementsMap,
|
||||||
)
|
)
|
||||||
: pointFrom<GlobalPoint>(
|
: pointFrom<GlobalPoint>(
|
||||||
arrow.x + arrow.points[arrow.points.length - 1][0],
|
arrow.x + arrow.points[arrow.points.length - 1][0],
|
||||||
|
@ -102,9 +102,23 @@ export class ElementBounds {
|
|||||||
version: ExcalidrawElement["version"];
|
version: ExcalidrawElement["version"];
|
||||||
}
|
}
|
||||||
>();
|
>();
|
||||||
|
private static nonRotatedBoundsCache = new WeakMap<
|
||||||
|
ExcalidrawElement,
|
||||||
|
{
|
||||||
|
bounds: Bounds;
|
||||||
|
version: ExcalidrawElement["version"];
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
|
||||||
static getBounds(element: ExcalidrawElement, elementsMap: ElementsMap) {
|
static getBounds(
|
||||||
const cachedBounds = ElementBounds.boundsCache.get(element);
|
element: ExcalidrawElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
nonRotated: boolean = false,
|
||||||
|
) {
|
||||||
|
const cachedBounds =
|
||||||
|
nonRotated && element.angle !== 0
|
||||||
|
? ElementBounds.nonRotatedBoundsCache.get(element)
|
||||||
|
: ElementBounds.boundsCache.get(element);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
cachedBounds?.version &&
|
cachedBounds?.version &&
|
||||||
@ -115,6 +129,23 @@ export class ElementBounds {
|
|||||||
) {
|
) {
|
||||||
return cachedBounds.bounds;
|
return cachedBounds.bounds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (nonRotated && element.angle !== 0) {
|
||||||
|
const nonRotatedBounds = ElementBounds.calculateBounds(
|
||||||
|
{
|
||||||
|
...element,
|
||||||
|
angle: 0 as Radians,
|
||||||
|
},
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
ElementBounds.nonRotatedBoundsCache.set(element, {
|
||||||
|
version: element.version,
|
||||||
|
bounds: nonRotatedBounds,
|
||||||
|
});
|
||||||
|
|
||||||
|
return nonRotatedBounds;
|
||||||
|
}
|
||||||
|
|
||||||
const bounds = ElementBounds.calculateBounds(element, elementsMap);
|
const bounds = ElementBounds.calculateBounds(element, elementsMap);
|
||||||
|
|
||||||
ElementBounds.boundsCache.set(element, {
|
ElementBounds.boundsCache.set(element, {
|
||||||
@ -939,8 +970,9 @@ const getLinearElementRotatedBounds = (
|
|||||||
export const getElementBounds = (
|
export const getElementBounds = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
|
nonRotated: boolean = false,
|
||||||
): Bounds => {
|
): Bounds => {
|
||||||
return ElementBounds.getBounds(element, elementsMap);
|
return ElementBounds.getBounds(element, elementsMap, nonRotated);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCommonBounds = (
|
export const getCommonBounds = (
|
||||||
|
@ -2,51 +2,60 @@ import { isTransparent, elementCenterPoint } from "@excalidraw/common";
|
|||||||
import {
|
import {
|
||||||
curveIntersectLineSegment,
|
curveIntersectLineSegment,
|
||||||
isPointWithinBounds,
|
isPointWithinBounds,
|
||||||
line,
|
|
||||||
lineSegment,
|
lineSegment,
|
||||||
lineSegmentIntersectionPoints,
|
lineSegmentIntersectionPoints,
|
||||||
pointFrom,
|
pointFrom,
|
||||||
|
pointFromVector,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
pointsEqual,
|
pointsEqual,
|
||||||
|
vectorFromPoint,
|
||||||
|
vectorNormalize,
|
||||||
|
vectorScale,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ellipse,
|
ellipse,
|
||||||
ellipseLineIntersectionPoints,
|
ellipseSegmentInterceptPoints,
|
||||||
} from "@excalidraw/math/ellipse";
|
} from "@excalidraw/math/ellipse";
|
||||||
|
|
||||||
import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision";
|
import type { GlobalPoint, LineSegment, Radians } from "@excalidraw/math";
|
||||||
import { type GeometricShape, getPolygonShape } from "@excalidraw/utils/shape";
|
|
||||||
|
|
||||||
import type {
|
|
||||||
GlobalPoint,
|
|
||||||
LineSegment,
|
|
||||||
LocalPoint,
|
|
||||||
Polygon,
|
|
||||||
Radians,
|
|
||||||
} from "@excalidraw/math";
|
|
||||||
|
|
||||||
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
|
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import { getBoundTextShape, isPathALoop } from "./shapes";
|
import { isPathALoop } from "./shapes";
|
||||||
import { getElementBounds } from "./bounds";
|
import {
|
||||||
|
type Bounds,
|
||||||
|
doBoundsIntersect,
|
||||||
|
getCenterForBounds,
|
||||||
|
getElementBounds,
|
||||||
|
} from "./bounds";
|
||||||
import {
|
import {
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
|
isFreeDrawElement,
|
||||||
isIframeLikeElement,
|
isIframeLikeElement,
|
||||||
isImageElement,
|
isImageElement,
|
||||||
|
isLinearElement,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
import {
|
import {
|
||||||
deconstructDiamondElement,
|
deconstructDiamondElement,
|
||||||
|
deconstructLinearOrFreeDrawElement,
|
||||||
deconstructRectanguloidElement,
|
deconstructRectanguloidElement,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
|
|
||||||
|
import { getBoundTextElement } from "./textElement";
|
||||||
|
|
||||||
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
|
|
||||||
|
import { distanceToElement } from "./distance";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawDiamondElement,
|
ExcalidrawDiamondElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawEllipseElement,
|
ExcalidrawEllipseElement,
|
||||||
ExcalidrawRectangleElement,
|
ExcalidrawFreeDrawElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawRectanguloidElement,
|
ExcalidrawRectanguloidElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@ -72,45 +81,64 @@ export const shouldTestInside = (element: ExcalidrawElement) => {
|
|||||||
return isDraggableFromInside || isImageElement(element);
|
return isDraggableFromInside || isImageElement(element);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HitTestArgs<Point extends GlobalPoint | LocalPoint> = {
|
export type HitTestArgs = {
|
||||||
x: number;
|
point: GlobalPoint;
|
||||||
y: number;
|
|
||||||
element: ExcalidrawElement;
|
element: ExcalidrawElement;
|
||||||
shape: GeometricShape<Point>;
|
threshold: number;
|
||||||
threshold?: number;
|
elementsMap: ElementsMap;
|
||||||
frameNameBound?: FrameNameBounds | null;
|
frameNameBound?: FrameNameBounds | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hitElementItself = <Point extends GlobalPoint | LocalPoint>({
|
export const hitElementItself = ({
|
||||||
x,
|
point,
|
||||||
y,
|
|
||||||
element,
|
element,
|
||||||
shape,
|
threshold,
|
||||||
threshold = 10,
|
elementsMap,
|
||||||
frameNameBound = null,
|
frameNameBound = null,
|
||||||
}: HitTestArgs<Point>) => {
|
}: HitTestArgs) => {
|
||||||
let hit = shouldTestInside(element)
|
// Hit test against a frame's name
|
||||||
? // Since `inShape` tests STRICTLY againt the insides of a shape
|
const hitFrameName = frameNameBound
|
||||||
// we would need `onShape` as well to include the "borders"
|
? isPointWithinBounds(
|
||||||
isPointInShape(pointFrom(x, y), shape) ||
|
pointFrom(frameNameBound.x - threshold, frameNameBound.y - threshold),
|
||||||
isPointOnShape(pointFrom(x, y), shape, threshold)
|
point,
|
||||||
: isPointOnShape(pointFrom(x, y), shape, threshold);
|
pointFrom(
|
||||||
|
frameNameBound.x + frameNameBound.width + threshold,
|
||||||
|
frameNameBound.y + frameNameBound.height + threshold,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: false;
|
||||||
|
|
||||||
// hit test against a frame's name
|
// Hit test against the extended, rotated bounding box of the element first
|
||||||
if (!hit && frameNameBound) {
|
const bounds = getElementBounds(element, elementsMap, true);
|
||||||
hit = isPointInShape(pointFrom(x, y), {
|
const hitBounds = isPointWithinBounds(
|
||||||
type: "polygon",
|
pointFrom(bounds[0] - threshold, bounds[1] - threshold),
|
||||||
data: getPolygonShape(frameNameBound as ExcalidrawRectangleElement)
|
pointRotateRads(
|
||||||
.data as Polygon<Point>,
|
point,
|
||||||
});
|
getCenterForBounds(bounds),
|
||||||
|
-element.angle as Radians,
|
||||||
|
),
|
||||||
|
pointFrom(bounds[2] + threshold, bounds[3] + threshold),
|
||||||
|
);
|
||||||
|
|
||||||
|
// PERF: Bail out early if the point is not even in the
|
||||||
|
// rotated bounding box or not hitting the frame name (saves 99%)
|
||||||
|
if (!hitBounds && !hitFrameName) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return hit;
|
// Do the precise (and relatively costly) hit test
|
||||||
|
const hitElement = 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, elementsMap) ||
|
||||||
|
isPointOnElementOutline(point, element, elementsMap, threshold)
|
||||||
|
: isPointOnElementOutline(point, element, elementsMap, threshold);
|
||||||
|
|
||||||
|
return hitElement || hitFrameName;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hitElementBoundingBox = (
|
export const hitElementBoundingBox = (
|
||||||
x: number,
|
point: GlobalPoint,
|
||||||
y: number,
|
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
tolerance = 0,
|
tolerance = 0,
|
||||||
@ -120,37 +148,42 @@ export const hitElementBoundingBox = (
|
|||||||
y1 -= tolerance;
|
y1 -= tolerance;
|
||||||
x2 += tolerance;
|
x2 += tolerance;
|
||||||
y2 += tolerance;
|
y2 += tolerance;
|
||||||
return isPointWithinBounds(
|
return isPointWithinBounds(pointFrom(x1, y1), point, pointFrom(x2, y2));
|
||||||
pointFrom(x1, y1),
|
|
||||||
pointFrom(x, y),
|
|
||||||
pointFrom(x2, y2),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hitElementBoundingBoxOnly = <
|
export const hitElementBoundingBoxOnly = (
|
||||||
Point extends GlobalPoint | LocalPoint,
|
hitArgs: HitTestArgs,
|
||||||
>(
|
|
||||||
hitArgs: HitTestArgs<Point>,
|
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
) => {
|
) =>
|
||||||
return (
|
!hitElementItself(hitArgs) &&
|
||||||
!hitElementItself(hitArgs) &&
|
// bound text is considered part of the element (even if it's outside the bounding box)
|
||||||
// bound text is considered part of the element (even if it's outside the bounding box)
|
!hitElementBoundText(hitArgs.point, hitArgs.element, elementsMap) &&
|
||||||
!hitElementBoundText(
|
hitElementBoundingBox(hitArgs.point, hitArgs.element, elementsMap);
|
||||||
hitArgs.x,
|
|
||||||
hitArgs.y,
|
|
||||||
getBoundTextShape(hitArgs.element, elementsMap),
|
|
||||||
) &&
|
|
||||||
hitElementBoundingBox(hitArgs.x, hitArgs.y, hitArgs.element, elementsMap)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hitElementBoundText = <Point extends GlobalPoint | LocalPoint>(
|
export const hitElementBoundText = (
|
||||||
x: number,
|
point: GlobalPoint,
|
||||||
y: number,
|
element: ExcalidrawElement,
|
||||||
textShape: GeometricShape<Point> | null,
|
elementsMap: ElementsMap,
|
||||||
): boolean => {
|
): 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, elementsMap);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -163,9 +196,26 @@ export const hitElementBoundText = <Point extends GlobalPoint | LocalPoint>(
|
|||||||
*/
|
*/
|
||||||
export const intersectElementWithLineSegment = (
|
export const intersectElementWithLineSegment = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
line: LineSegment<GlobalPoint>,
|
line: LineSegment<GlobalPoint>,
|
||||||
offset: number = 0,
|
offset: number = 0,
|
||||||
|
onlyFirst = false,
|
||||||
): GlobalPoint[] => {
|
): GlobalPoint[] => {
|
||||||
|
// First check if the line intersects the element's axis-aligned bounding box
|
||||||
|
// as it is much faster than checking intersection against the element's shape
|
||||||
|
const intersectorBounds = [
|
||||||
|
Math.min(line[0][0] - offset, line[1][0] - offset),
|
||||||
|
Math.min(line[0][1] - offset, line[1][1] - offset),
|
||||||
|
Math.max(line[0][0] + offset, line[1][0] + offset),
|
||||||
|
Math.max(line[0][1] + offset, line[1][1] + offset),
|
||||||
|
] as Bounds;
|
||||||
|
const elementBounds = getElementBounds(element, elementsMap);
|
||||||
|
|
||||||
|
if (!doBoundsIntersect(intersectorBounds, elementBounds)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do the actual intersection test against the element's shape
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "image":
|
case "image":
|
||||||
@ -173,23 +223,88 @@ export const intersectElementWithLineSegment = (
|
|||||||
case "iframe":
|
case "iframe":
|
||||||
case "embeddable":
|
case "embeddable":
|
||||||
case "frame":
|
case "frame":
|
||||||
|
case "selection":
|
||||||
case "magicframe":
|
case "magicframe":
|
||||||
return intersectRectanguloidWithLineSegment(element, line, offset);
|
return intersectRectanguloidWithLineSegment(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
line,
|
||||||
|
offset,
|
||||||
|
onlyFirst,
|
||||||
|
);
|
||||||
case "diamond":
|
case "diamond":
|
||||||
return intersectDiamondWithLineSegment(element, line, offset);
|
return intersectDiamondWithLineSegment(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
line,
|
||||||
|
offset,
|
||||||
|
onlyFirst,
|
||||||
|
);
|
||||||
case "ellipse":
|
case "ellipse":
|
||||||
return intersectEllipseWithLineSegment(element, line, offset);
|
return intersectEllipseWithLineSegment(
|
||||||
default:
|
element,
|
||||||
throw new Error(`Unimplemented element type '${element.type}'`);
|
elementsMap,
|
||||||
|
line,
|
||||||
|
offset,
|
||||||
|
);
|
||||||
|
case "line":
|
||||||
|
case "freedraw":
|
||||||
|
case "arrow":
|
||||||
|
return intersectLinearOrFreeDrawWithLineSegment(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
line,
|
||||||
|
onlyFirst,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const intersectLinearOrFreeDrawWithLineSegment = (
|
||||||
|
element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
segment: LineSegment<GlobalPoint>,
|
||||||
|
onlyFirst = false,
|
||||||
|
): GlobalPoint[] => {
|
||||||
|
const [lines, curves] = deconstructLinearOrFreeDrawElement(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
const intersections = [];
|
||||||
|
|
||||||
|
for (const l of lines) {
|
||||||
|
const intersection = lineSegmentIntersectionPoints(l, segment);
|
||||||
|
if (intersection) {
|
||||||
|
intersections.push(intersection);
|
||||||
|
|
||||||
|
if (onlyFirst) {
|
||||||
|
return intersections;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const c of curves) {
|
||||||
|
const hits = curveIntersectLineSegment(c, segment);
|
||||||
|
|
||||||
|
if (hits.length > 0) {
|
||||||
|
intersections.push(...hits);
|
||||||
|
|
||||||
|
if (onlyFirst) {
|
||||||
|
return intersections;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return intersections;
|
||||||
|
};
|
||||||
|
|
||||||
const intersectRectanguloidWithLineSegment = (
|
const intersectRectanguloidWithLineSegment = (
|
||||||
element: ExcalidrawRectanguloidElement,
|
element: ExcalidrawRectanguloidElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
l: LineSegment<GlobalPoint>,
|
l: LineSegment<GlobalPoint>,
|
||||||
offset: number = 0,
|
offset: number = 0,
|
||||||
|
onlyFirst = false,
|
||||||
): GlobalPoint[] => {
|
): GlobalPoint[] => {
|
||||||
const center = elementCenterPoint(element);
|
const center = elementCenterPoint(element, elementsMap);
|
||||||
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
||||||
// instead. It's all the same distance-wise.
|
// instead. It's all the same distance-wise.
|
||||||
const rotatedA = pointRotateRads<GlobalPoint>(
|
const rotatedA = pointRotateRads<GlobalPoint>(
|
||||||
@ -206,34 +321,37 @@ const intersectRectanguloidWithLineSegment = (
|
|||||||
// Get the element's building components we can test against
|
// Get the element's building components we can test against
|
||||||
const [sides, corners] = deconstructRectanguloidElement(element, offset);
|
const [sides, corners] = deconstructRectanguloidElement(element, offset);
|
||||||
|
|
||||||
return (
|
const intersections: GlobalPoint[] = [];
|
||||||
// Test intersection against the sides, keep only the valid
|
|
||||||
// intersection points and rotate them back to scene space
|
for (const s of sides) {
|
||||||
sides
|
const intersection = lineSegmentIntersectionPoints(
|
||||||
.map((s) =>
|
lineSegment(rotatedA, rotatedB),
|
||||||
lineSegmentIntersectionPoints(
|
s,
|
||||||
lineSegment<GlobalPoint>(rotatedA, rotatedB),
|
);
|
||||||
s,
|
if (intersection) {
|
||||||
),
|
intersections.push(pointRotateRads(intersection, center, element.angle));
|
||||||
)
|
|
||||||
.filter((x) => x != null)
|
if (onlyFirst) {
|
||||||
.map((j) => pointRotateRads<GlobalPoint>(j!, center, element.angle))
|
return intersections;
|
||||||
// Test intersection against the corners which are cubic bezier curves,
|
}
|
||||||
// keep only the valid intersection points and rotate them back to scene
|
}
|
||||||
// space
|
}
|
||||||
.concat(
|
|
||||||
corners
|
for (const t of corners) {
|
||||||
.flatMap((t) =>
|
const hits = curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB));
|
||||||
curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB)),
|
|
||||||
)
|
if (hits.length > 0) {
|
||||||
.filter((i) => i != null)
|
for (const j of hits) {
|
||||||
.map((j) => pointRotateRads(j, center, element.angle)),
|
intersections.push(pointRotateRads(j, center, element.angle));
|
||||||
)
|
}
|
||||||
// Remove duplicates
|
|
||||||
.filter(
|
if (onlyFirst) {
|
||||||
(p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx,
|
return intersections;
|
||||||
)
|
}
|
||||||
);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return intersections;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -245,43 +363,51 @@ const intersectRectanguloidWithLineSegment = (
|
|||||||
*/
|
*/
|
||||||
const intersectDiamondWithLineSegment = (
|
const intersectDiamondWithLineSegment = (
|
||||||
element: ExcalidrawDiamondElement,
|
element: ExcalidrawDiamondElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
l: LineSegment<GlobalPoint>,
|
l: LineSegment<GlobalPoint>,
|
||||||
offset: number = 0,
|
offset: number = 0,
|
||||||
|
onlyFirst = false,
|
||||||
): GlobalPoint[] => {
|
): GlobalPoint[] => {
|
||||||
const center = elementCenterPoint(element);
|
const center = elementCenterPoint(element, elementsMap);
|
||||||
|
|
||||||
// Rotate the point to the inverse direction to simulate the rotated diamond
|
// Rotate the point to the inverse direction to simulate the rotated diamond
|
||||||
// points. It's all the same distance-wise.
|
// points. It's all the same distance-wise.
|
||||||
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
|
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
|
||||||
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
|
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
|
||||||
|
|
||||||
const [sides, curves] = deconstructDiamondElement(element, offset);
|
const [sides, corners] = deconstructDiamondElement(element, offset);
|
||||||
|
|
||||||
return (
|
const intersections: GlobalPoint[] = [];
|
||||||
sides
|
|
||||||
.map((s) =>
|
for (const s of sides) {
|
||||||
lineSegmentIntersectionPoints(
|
const intersection = lineSegmentIntersectionPoints(
|
||||||
lineSegment<GlobalPoint>(rotatedA, rotatedB),
|
lineSegment(rotatedA, rotatedB),
|
||||||
s,
|
s,
|
||||||
),
|
);
|
||||||
)
|
if (intersection) {
|
||||||
.filter((p): p is GlobalPoint => p != null)
|
intersections.push(pointRotateRads(intersection, center, element.angle));
|
||||||
// Rotate back intersection points
|
|
||||||
.map((p) => pointRotateRads<GlobalPoint>(p!, center, element.angle))
|
if (onlyFirst) {
|
||||||
.concat(
|
return intersections;
|
||||||
curves
|
}
|
||||||
.flatMap((p) =>
|
}
|
||||||
curveIntersectLineSegment(p, lineSegment(rotatedA, rotatedB)),
|
}
|
||||||
)
|
|
||||||
.filter((p) => p != null)
|
for (const t of corners) {
|
||||||
// Rotate back intersection points
|
const hits = curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB));
|
||||||
.map((p) => pointRotateRads(p, center, element.angle)),
|
|
||||||
)
|
if (hits.length > 0) {
|
||||||
// Remove duplicates
|
for (const j of hits) {
|
||||||
.filter(
|
intersections.push(pointRotateRads(j, center, element.angle));
|
||||||
(p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx,
|
}
|
||||||
)
|
|
||||||
);
|
if (onlyFirst) {
|
||||||
|
return intersections;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return intersections;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -293,16 +419,76 @@ const intersectDiamondWithLineSegment = (
|
|||||||
*/
|
*/
|
||||||
const intersectEllipseWithLineSegment = (
|
const intersectEllipseWithLineSegment = (
|
||||||
element: ExcalidrawEllipseElement,
|
element: ExcalidrawEllipseElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
l: LineSegment<GlobalPoint>,
|
l: LineSegment<GlobalPoint>,
|
||||||
offset: number = 0,
|
offset: number = 0,
|
||||||
): GlobalPoint[] => {
|
): GlobalPoint[] => {
|
||||||
const center = elementCenterPoint(element);
|
const center = elementCenterPoint(element, elementsMap);
|
||||||
|
|
||||||
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
|
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
|
||||||
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
|
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
|
||||||
|
|
||||||
return ellipseLineIntersectionPoints(
|
return ellipseSegmentInterceptPoints(
|
||||||
ellipse(center, element.width / 2 + offset, element.height / 2 + offset),
|
ellipse(center, element.width / 2 + offset, element.height / 2 + offset),
|
||||||
line(rotatedA, rotatedB),
|
lineSegment(rotatedA, rotatedB),
|
||||||
).map((p) => pointRotateRads(p, center, element.angle));
|
).map((p) => pointRotateRads(p, center, element.angle));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given point is considered on the given shape's border
|
||||||
|
*
|
||||||
|
* @param point
|
||||||
|
* @param element
|
||||||
|
* @param tolerance
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const isPointOnElementOutline = (
|
||||||
|
point: GlobalPoint,
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
tolerance = 1,
|
||||||
|
) => distanceToElement(element, elementsMap, point) <= tolerance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given point is considered inside the element's border
|
||||||
|
*
|
||||||
|
* @param point
|
||||||
|
* @param element
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const isPointInElement = (
|
||||||
|
point: GlobalPoint,
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
) => {
|
||||||
|
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, elementsMap);
|
||||||
|
|
||||||
|
if (!isPointWithinBounds(pointFrom(x1, y1), point, pointFrom(x2, y2))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
elementsMap,
|
||||||
|
intersector,
|
||||||
|
).filter((p, pos, arr) => arr.findIndex((q) => pointsEqual(q, p)) === pos);
|
||||||
|
|
||||||
|
return intersections.length % 2 === 1;
|
||||||
|
};
|
||||||
|
@ -34,6 +34,7 @@ export const MINIMAL_CROP_SIZE = 10;
|
|||||||
|
|
||||||
export const cropElement = (
|
export const cropElement = (
|
||||||
element: ExcalidrawImageElement,
|
element: ExcalidrawImageElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
transformHandle: TransformHandleType,
|
transformHandle: TransformHandleType,
|
||||||
naturalWidth: number,
|
naturalWidth: number,
|
||||||
naturalHeight: number,
|
naturalHeight: number,
|
||||||
@ -63,7 +64,7 @@ export const cropElement = (
|
|||||||
|
|
||||||
const rotatedPointer = pointRotateRads(
|
const rotatedPointer = pointRotateRads(
|
||||||
pointFrom(pointerX, pointerY),
|
pointFrom(pointerX, pointerY),
|
||||||
elementCenterPoint(element),
|
elementCenterPoint(element, elementsMap),
|
||||||
-element.angle as Radians,
|
-element.angle as Radians,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -12,21 +12,27 @@ import type { GlobalPoint, Radians } from "@excalidraw/math";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
deconstructDiamondElement,
|
deconstructDiamondElement,
|
||||||
|
deconstructLinearOrFreeDrawElement,
|
||||||
deconstructRectanguloidElement,
|
deconstructRectanguloidElement,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawBindableElement,
|
ElementsMap,
|
||||||
ExcalidrawDiamondElement,
|
ExcalidrawDiamondElement,
|
||||||
|
ExcalidrawElement,
|
||||||
ExcalidrawEllipseElement,
|
ExcalidrawEllipseElement,
|
||||||
|
ExcalidrawFreeDrawElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawRectanguloidElement,
|
ExcalidrawRectanguloidElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export const distanceToBindableElement = (
|
export const distanceToElement = (
|
||||||
element: ExcalidrawBindableElement,
|
element: ExcalidrawElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
): number => {
|
): number => {
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
|
case "selection":
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "image":
|
case "image":
|
||||||
case "text":
|
case "text":
|
||||||
@ -34,11 +40,15 @@ export const distanceToBindableElement = (
|
|||||||
case "embeddable":
|
case "embeddable":
|
||||||
case "frame":
|
case "frame":
|
||||||
case "magicframe":
|
case "magicframe":
|
||||||
return distanceToRectanguloidElement(element, p);
|
return distanceToRectanguloidElement(element, elementsMap, p);
|
||||||
case "diamond":
|
case "diamond":
|
||||||
return distanceToDiamondElement(element, p);
|
return distanceToDiamondElement(element, elementsMap, p);
|
||||||
case "ellipse":
|
case "ellipse":
|
||||||
return distanceToEllipseElement(element, p);
|
return distanceToEllipseElement(element, elementsMap, p);
|
||||||
|
case "line":
|
||||||
|
case "arrow":
|
||||||
|
case "freedraw":
|
||||||
|
return distanceToLinearOrFreeDraElement(element, elementsMap, p);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -52,9 +62,10 @@ export const distanceToBindableElement = (
|
|||||||
*/
|
*/
|
||||||
const distanceToRectanguloidElement = (
|
const distanceToRectanguloidElement = (
|
||||||
element: ExcalidrawRectanguloidElement,
|
element: ExcalidrawRectanguloidElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
) => {
|
) => {
|
||||||
const center = elementCenterPoint(element);
|
const center = elementCenterPoint(element, elementsMap);
|
||||||
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
||||||
// instead. It's all the same distance-wise.
|
// instead. It's all the same distance-wise.
|
||||||
const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
||||||
@ -80,9 +91,10 @@ const distanceToRectanguloidElement = (
|
|||||||
*/
|
*/
|
||||||
const distanceToDiamondElement = (
|
const distanceToDiamondElement = (
|
||||||
element: ExcalidrawDiamondElement,
|
element: ExcalidrawDiamondElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
): number => {
|
): number => {
|
||||||
const center = elementCenterPoint(element);
|
const center = elementCenterPoint(element, elementsMap);
|
||||||
|
|
||||||
// Rotate the point to the inverse direction to simulate the rotated diamond
|
// Rotate the point to the inverse direction to simulate the rotated diamond
|
||||||
// points. It's all the same distance-wise.
|
// points. It's all the same distance-wise.
|
||||||
@ -108,12 +120,28 @@ const distanceToDiamondElement = (
|
|||||||
*/
|
*/
|
||||||
const distanceToEllipseElement = (
|
const distanceToEllipseElement = (
|
||||||
element: ExcalidrawEllipseElement,
|
element: ExcalidrawEllipseElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
): number => {
|
): number => {
|
||||||
const center = elementCenterPoint(element);
|
const center = elementCenterPoint(element, elementsMap);
|
||||||
return ellipseDistanceFromPoint(
|
return ellipseDistanceFromPoint(
|
||||||
// Instead of rotating the ellipse, rotate the point to the inverse angle
|
// Instead of rotating the ellipse, rotate the point to the inverse angle
|
||||||
pointRotateRads(p, center, -element.angle as Radians),
|
pointRotateRads(p, center, -element.angle as Radians),
|
||||||
ellipse(center, element.width / 2, element.height / 2),
|
ellipse(center, element.width / 2, element.height / 2),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const distanceToLinearOrFreeDraElement = (
|
||||||
|
element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
p: GlobalPoint,
|
||||||
|
) => {
|
||||||
|
const [lines, curves] = deconstructLinearOrFreeDrawElement(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
return Math.min(
|
||||||
|
...lines.map((s) => distanceToLineSegment(p, s)),
|
||||||
|
...curves.map((a) => curvePointDistance(a, p)),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -29,10 +29,9 @@ import {
|
|||||||
FIXED_BINDING_DISTANCE,
|
FIXED_BINDING_DISTANCE,
|
||||||
getHeadingForElbowArrowSnap,
|
getHeadingForElbowArrowSnap,
|
||||||
getGlobalFixedPointForBindableElement,
|
getGlobalFixedPointForBindableElement,
|
||||||
snapToMid,
|
|
||||||
getHoveredElementForBinding,
|
getHoveredElementForBinding,
|
||||||
} from "./binding";
|
} from "./binding";
|
||||||
import { distanceToBindableElement } from "./distance";
|
import { distanceToElement } from "./distance";
|
||||||
import {
|
import {
|
||||||
compareHeading,
|
compareHeading,
|
||||||
flipHeading,
|
flipHeading,
|
||||||
@ -898,50 +897,6 @@ export const updateElbowArrowPoints = (
|
|||||||
return { points: updates.points ?? arrow.points };
|
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) {
|
if (!import.meta.env.PROD) {
|
||||||
invariant(
|
invariant(
|
||||||
!updates.points || updates.points.length >= 2,
|
!updates.points || updates.points.length >= 2,
|
||||||
@ -1273,6 +1228,7 @@ const getElbowArrowData = (
|
|||||||
arrow.startBinding?.fixedPoint,
|
arrow.startBinding?.fixedPoint,
|
||||||
origStartGlobalPoint,
|
origStartGlobalPoint,
|
||||||
hoveredStartElement,
|
hoveredStartElement,
|
||||||
|
elementsMap,
|
||||||
options?.isDragging,
|
options?.isDragging,
|
||||||
);
|
);
|
||||||
const endGlobalPoint = getGlobalPoint(
|
const endGlobalPoint = getGlobalPoint(
|
||||||
@ -1286,6 +1242,7 @@ const getElbowArrowData = (
|
|||||||
arrow.endBinding?.fixedPoint,
|
arrow.endBinding?.fixedPoint,
|
||||||
origEndGlobalPoint,
|
origEndGlobalPoint,
|
||||||
hoveredEndElement,
|
hoveredEndElement,
|
||||||
|
elementsMap,
|
||||||
options?.isDragging,
|
options?.isDragging,
|
||||||
);
|
);
|
||||||
const startHeading = getBindPointHeading(
|
const startHeading = getBindPointHeading(
|
||||||
@ -1293,12 +1250,14 @@ const getElbowArrowData = (
|
|||||||
endGlobalPoint,
|
endGlobalPoint,
|
||||||
hoveredStartElement,
|
hoveredStartElement,
|
||||||
origStartGlobalPoint,
|
origStartGlobalPoint,
|
||||||
|
elementsMap,
|
||||||
);
|
);
|
||||||
const endHeading = getBindPointHeading(
|
const endHeading = getBindPointHeading(
|
||||||
endGlobalPoint,
|
endGlobalPoint,
|
||||||
startGlobalPoint,
|
startGlobalPoint,
|
||||||
hoveredEndElement,
|
hoveredEndElement,
|
||||||
origEndGlobalPoint,
|
origEndGlobalPoint,
|
||||||
|
elementsMap,
|
||||||
);
|
);
|
||||||
const startPointBounds = [
|
const startPointBounds = [
|
||||||
startGlobalPoint[0] - 2,
|
startGlobalPoint[0] - 2,
|
||||||
@ -1315,6 +1274,7 @@ const getElbowArrowData = (
|
|||||||
const startElementBounds = hoveredStartElement
|
const startElementBounds = hoveredStartElement
|
||||||
? aabbForElement(
|
? aabbForElement(
|
||||||
hoveredStartElement,
|
hoveredStartElement,
|
||||||
|
elementsMap,
|
||||||
offsetFromHeading(
|
offsetFromHeading(
|
||||||
startHeading,
|
startHeading,
|
||||||
arrow.startArrowhead
|
arrow.startArrowhead
|
||||||
@ -1327,6 +1287,7 @@ const getElbowArrowData = (
|
|||||||
const endElementBounds = hoveredEndElement
|
const endElementBounds = hoveredEndElement
|
||||||
? aabbForElement(
|
? aabbForElement(
|
||||||
hoveredEndElement,
|
hoveredEndElement,
|
||||||
|
elementsMap,
|
||||||
offsetFromHeading(
|
offsetFromHeading(
|
||||||
endHeading,
|
endHeading,
|
||||||
arrow.endArrowhead
|
arrow.endArrowhead
|
||||||
@ -1342,6 +1303,7 @@ const getElbowArrowData = (
|
|||||||
hoveredEndElement
|
hoveredEndElement
|
||||||
? aabbForElement(
|
? aabbForElement(
|
||||||
hoveredEndElement,
|
hoveredEndElement,
|
||||||
|
elementsMap,
|
||||||
offsetFromHeading(endHeading, BASE_PADDING, BASE_PADDING),
|
offsetFromHeading(endHeading, BASE_PADDING, BASE_PADDING),
|
||||||
)
|
)
|
||||||
: endPointBounds,
|
: endPointBounds,
|
||||||
@ -1351,6 +1313,7 @@ const getElbowArrowData = (
|
|||||||
hoveredStartElement
|
hoveredStartElement
|
||||||
? aabbForElement(
|
? aabbForElement(
|
||||||
hoveredStartElement,
|
hoveredStartElement,
|
||||||
|
elementsMap,
|
||||||
offsetFromHeading(startHeading, BASE_PADDING, BASE_PADDING),
|
offsetFromHeading(startHeading, BASE_PADDING, BASE_PADDING),
|
||||||
)
|
)
|
||||||
: startPointBounds,
|
: startPointBounds,
|
||||||
@ -1397,8 +1360,8 @@ const getElbowArrowData = (
|
|||||||
BASE_PADDING,
|
BASE_PADDING,
|
||||||
),
|
),
|
||||||
boundsOverlap,
|
boundsOverlap,
|
||||||
hoveredStartElement && aabbForElement(hoveredStartElement),
|
hoveredStartElement && aabbForElement(hoveredStartElement, elementsMap),
|
||||||
hoveredEndElement && aabbForElement(hoveredEndElement),
|
hoveredEndElement && aabbForElement(hoveredEndElement, elementsMap),
|
||||||
);
|
);
|
||||||
const startDonglePosition = getDonglePosition(
|
const startDonglePosition = getDonglePosition(
|
||||||
dynamicAABBs[0],
|
dynamicAABBs[0],
|
||||||
@ -2229,34 +2192,35 @@ const getGlobalPoint = (
|
|||||||
fixedPointRatio: [number, number] | undefined | null,
|
fixedPointRatio: [number, number] | undefined | null,
|
||||||
initialPoint: GlobalPoint,
|
initialPoint: GlobalPoint,
|
||||||
element?: ExcalidrawBindableElement | null,
|
element?: ExcalidrawBindableElement | null,
|
||||||
|
elementsMap?: ElementsMap,
|
||||||
isDragging?: boolean,
|
isDragging?: boolean,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
if (element) {
|
if (element && elementsMap) {
|
||||||
const snapPoint = bindPointToSnapToElementOutline(
|
return bindPointToSnapToElementOutline(
|
||||||
arrow,
|
arrow,
|
||||||
element,
|
element,
|
||||||
startOrEnd,
|
startOrEnd,
|
||||||
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
return snapToMid(element, snapPoint);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return initialPoint;
|
return initialPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (element) {
|
if (element && elementsMap) {
|
||||||
const fixedGlobalPoint = getGlobalFixedPointForBindableElement(
|
const fixedGlobalPoint = getGlobalFixedPointForBindableElement(
|
||||||
fixedPointRatio || [0, 0],
|
fixedPointRatio || [0, 0],
|
||||||
element,
|
element,
|
||||||
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
// NOTE: Resize scales the binding position point too, so we need to update it
|
// NOTE: Resize scales the binding position point too, so we need to update it
|
||||||
return Math.abs(
|
return Math.abs(
|
||||||
distanceToBindableElement(element, fixedGlobalPoint) -
|
distanceToElement(element, elementsMap, fixedGlobalPoint) -
|
||||||
FIXED_BINDING_DISTANCE,
|
FIXED_BINDING_DISTANCE,
|
||||||
) > 0.01
|
) > 0.01
|
||||||
? bindPointToSnapToElementOutline(arrow, element, startOrEnd)
|
? bindPointToSnapToElementOutline(arrow, element, startOrEnd, elementsMap)
|
||||||
: fixedGlobalPoint;
|
: fixedGlobalPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2268,6 +2232,7 @@ const getBindPointHeading = (
|
|||||||
otherPoint: GlobalPoint,
|
otherPoint: GlobalPoint,
|
||||||
hoveredElement: ExcalidrawBindableElement | null | undefined,
|
hoveredElement: ExcalidrawBindableElement | null | undefined,
|
||||||
origPoint: GlobalPoint,
|
origPoint: GlobalPoint,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
): Heading =>
|
): Heading =>
|
||||||
getHeadingForElbowArrowSnap(
|
getHeadingForElbowArrowSnap(
|
||||||
p,
|
p,
|
||||||
@ -2276,7 +2241,8 @@ const getBindPointHeading = (
|
|||||||
hoveredElement &&
|
hoveredElement &&
|
||||||
aabbForElement(
|
aabbForElement(
|
||||||
hoveredElement,
|
hoveredElement,
|
||||||
Array(4).fill(distanceToBindableElement(hoveredElement, p)) as [
|
elementsMap,
|
||||||
|
Array(4).fill(distanceToElement(hoveredElement, elementsMap, p)) as [
|
||||||
number,
|
number,
|
||||||
number,
|
number,
|
||||||
number,
|
number,
|
||||||
@ -2284,6 +2250,7 @@ const getBindPointHeading = (
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
origPoint,
|
origPoint,
|
||||||
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
const getHoveredElement = (
|
const getHoveredElement = (
|
||||||
|
@ -95,10 +95,11 @@ const getNodeRelatives = (
|
|||||||
type === "predecessors" ? el.points[el.points.length - 1] : [0, 0]
|
type === "predecessors" ? el.points[el.points.length - 1] : [0, 0]
|
||||||
) as Readonly<LocalPoint>;
|
) as Readonly<LocalPoint>;
|
||||||
|
|
||||||
const heading = headingForPointFromElement(node, aabbForElement(node), [
|
const heading = headingForPointFromElement(
|
||||||
edgePoint[0] + el.x,
|
node,
|
||||||
edgePoint[1] + el.y,
|
aabbForElement(node, elementsMap),
|
||||||
] as Readonly<GlobalPoint>);
|
[edgePoint[0] + el.x, edgePoint[1] + el.y] as Readonly<GlobalPoint>,
|
||||||
|
);
|
||||||
|
|
||||||
acc.push({
|
acc.push({
|
||||||
relative,
|
relative,
|
||||||
|
@ -291,6 +291,7 @@ export const mapIntervalToBezierT = <P extends GlobalPoint | LocalPoint>(
|
|||||||
*/
|
*/
|
||||||
export const aabbForElement = (
|
export const aabbForElement = (
|
||||||
element: Readonly<ExcalidrawElement>,
|
element: Readonly<ExcalidrawElement>,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
offset?: [number, number, number, number],
|
offset?: [number, number, number, number],
|
||||||
) => {
|
) => {
|
||||||
const bbox = {
|
const bbox = {
|
||||||
@ -302,7 +303,7 @@ export const aabbForElement = (
|
|||||||
midY: element.y + element.height / 2,
|
midY: element.y + element.height / 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
const center = elementCenterPoint(element);
|
const center = elementCenterPoint(element, elementsMap);
|
||||||
const [topLeftX, topLeftY] = pointRotateRads(
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
pointFrom(bbox.minX, bbox.minY),
|
pointFrom(bbox.minX, bbox.minY),
|
||||||
center,
|
center,
|
||||||
|
@ -195,7 +195,8 @@ export type ExcalidrawRectanguloidElement =
|
|||||||
| ExcalidrawFreeDrawElement
|
| ExcalidrawFreeDrawElement
|
||||||
| ExcalidrawIframeLikeElement
|
| ExcalidrawIframeLikeElement
|
||||||
| ExcalidrawFrameLikeElement
|
| ExcalidrawFrameLikeElement
|
||||||
| ExcalidrawEmbeddableElement;
|
| ExcalidrawEmbeddableElement
|
||||||
|
| ExcalidrawSelectionElement;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ExcalidrawElement should be JSON serializable and (eventually) contain
|
* ExcalidrawElement should be JSON serializable and (eventually) contain
|
||||||
|
@ -1,28 +1,168 @@
|
|||||||
import {
|
import {
|
||||||
curve,
|
curve,
|
||||||
|
curveCatmullRomCubicApproxPoints,
|
||||||
|
curveOffsetPoints,
|
||||||
lineSegment,
|
lineSegment,
|
||||||
pointFrom,
|
pointFrom,
|
||||||
pointFromVector,
|
pointFromArray,
|
||||||
rectangle,
|
rectangle,
|
||||||
vectorFromPoint,
|
|
||||||
vectorNormalize,
|
|
||||||
vectorScale,
|
|
||||||
type GlobalPoint,
|
type GlobalPoint,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import { elementCenterPoint } from "@excalidraw/common";
|
import type { Curve, LineSegment, LocalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
import type { Curve, LineSegment } from "@excalidraw/math";
|
|
||||||
|
|
||||||
import { getCornerRadius } from "./shapes";
|
import { getCornerRadius } from "./shapes";
|
||||||
|
|
||||||
import { getDiamondPoints } from "./bounds";
|
import { getDiamondPoints } from "./bounds";
|
||||||
|
|
||||||
|
import { generateLinearCollisionShape } from "./Shape";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
ElementsMap,
|
||||||
ExcalidrawDiamondElement,
|
ExcalidrawDiamondElement,
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawFreeDrawElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawRectanguloidElement,
|
ExcalidrawRectanguloidElement,
|
||||||
} from "./types";
|
} 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,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
): [LineSegment<GlobalPoint>[], Curve<GlobalPoint>[]] {
|
||||||
|
const cachedShape = getElementShapesCacheEntry(element, 0);
|
||||||
|
|
||||||
|
if (cachedShape) {
|
||||||
|
return cachedShape;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ops = generateLinearCollisionShape(element, elementsMap) 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
|
* Get the building components of a rectanguloid element in the form of
|
||||||
* line segments and curves.
|
* line segments and curves.
|
||||||
@ -35,175 +175,132 @@ export function deconstructRectanguloidElement(
|
|||||||
element: ExcalidrawRectanguloidElement,
|
element: ExcalidrawRectanguloidElement,
|
||||||
offset: number = 0,
|
offset: number = 0,
|
||||||
): [LineSegment<GlobalPoint>[], Curve<GlobalPoint>[]] {
|
): [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),
|
Math.min(element.width, element.height),
|
||||||
element,
|
element,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (roundness <= 0) {
|
if (radius === 0) {
|
||||||
const r = rectangle(
|
radius = 0.01;
|
||||||
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, []];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const center = elementCenterPoint(element);
|
|
||||||
|
|
||||||
const r = rectangle(
|
const r = rectangle(
|
||||||
pointFrom(element.x, element.y),
|
pointFrom(element.x, element.y),
|
||||||
pointFrom(element.x + element.width, element.y + element.height),
|
pointFrom(element.x + element.width, element.y + element.height),
|
||||||
);
|
);
|
||||||
|
|
||||||
const top = lineSegment<GlobalPoint>(
|
const top = lineSegment<GlobalPoint>(
|
||||||
pointFrom<GlobalPoint>(r[0][0] + roundness, r[0][1]),
|
pointFrom<GlobalPoint>(r[0][0] + radius, r[0][1]),
|
||||||
pointFrom<GlobalPoint>(r[1][0] - roundness, r[0][1]),
|
pointFrom<GlobalPoint>(r[1][0] - radius, r[0][1]),
|
||||||
);
|
);
|
||||||
const right = lineSegment<GlobalPoint>(
|
const right = lineSegment<GlobalPoint>(
|
||||||
pointFrom<GlobalPoint>(r[1][0], r[0][1] + roundness),
|
pointFrom<GlobalPoint>(r[1][0], r[0][1] + radius),
|
||||||
pointFrom<GlobalPoint>(r[1][0], r[1][1] - roundness),
|
pointFrom<GlobalPoint>(r[1][0], r[1][1] - radius),
|
||||||
);
|
);
|
||||||
const bottom = lineSegment<GlobalPoint>(
|
const bottom = lineSegment<GlobalPoint>(
|
||||||
pointFrom<GlobalPoint>(r[0][0] + roundness, r[1][1]),
|
pointFrom<GlobalPoint>(r[0][0] + radius, r[1][1]),
|
||||||
pointFrom<GlobalPoint>(r[1][0] - roundness, r[1][1]),
|
pointFrom<GlobalPoint>(r[1][0] - radius, r[1][1]),
|
||||||
);
|
);
|
||||||
const left = lineSegment<GlobalPoint>(
|
const left = lineSegment<GlobalPoint>(
|
||||||
pointFrom<GlobalPoint>(r[0][0], r[1][1] - roundness),
|
pointFrom<GlobalPoint>(r[0][0], r[1][1] - radius),
|
||||||
pointFrom<GlobalPoint>(r[0][0], r[0][1] + roundness),
|
pointFrom<GlobalPoint>(r[0][0], r[0][1] + radius),
|
||||||
);
|
);
|
||||||
|
|
||||||
const offsets = [
|
const baseCorners = [
|
||||||
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 = [
|
|
||||||
curve(
|
curve(
|
||||||
pointFromVector(offsets[0], left[1]),
|
left[1],
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[0],
|
left[1][0] + (2 / 3) * (r[0][0] - left[1][0]),
|
||||||
pointFrom<GlobalPoint>(
|
left[1][1] + (2 / 3) * (r[0][1] - left[1][1]),
|
||||||
left[1][0] + (2 / 3) * (r[0][0] - left[1][0]),
|
|
||||||
left[1][1] + (2 / 3) * (r[0][1] - left[1][1]),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[0],
|
top[0][0] + (2 / 3) * (r[0][0] - top[0][0]),
|
||||||
pointFrom<GlobalPoint>(
|
top[0][1] + (2 / 3) * (r[0][1] - top[0][1]),
|
||||||
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
|
), // TOP LEFT
|
||||||
curve(
|
curve(
|
||||||
pointFromVector(offsets[1], top[1]),
|
top[1],
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[1],
|
top[1][0] + (2 / 3) * (r[1][0] - top[1][0]),
|
||||||
pointFrom<GlobalPoint>(
|
top[1][1] + (2 / 3) * (r[0][1] - top[1][1]),
|
||||||
top[1][0] + (2 / 3) * (r[1][0] - top[1][0]),
|
|
||||||
top[1][1] + (2 / 3) * (r[0][1] - top[1][1]),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[1],
|
right[0][0] + (2 / 3) * (r[1][0] - right[0][0]),
|
||||||
pointFrom<GlobalPoint>(
|
right[0][1] + (2 / 3) * (r[0][1] - right[0][1]),
|
||||||
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
|
), // TOP RIGHT
|
||||||
curve(
|
curve(
|
||||||
pointFromVector(offsets[2], right[1]),
|
right[1],
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[2],
|
right[1][0] + (2 / 3) * (r[1][0] - right[1][0]),
|
||||||
pointFrom<GlobalPoint>(
|
right[1][1] + (2 / 3) * (r[1][1] - right[1][1]),
|
||||||
right[1][0] + (2 / 3) * (r[1][0] - right[1][0]),
|
|
||||||
right[1][1] + (2 / 3) * (r[1][1] - right[1][1]),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[2],
|
bottom[1][0] + (2 / 3) * (r[1][0] - bottom[1][0]),
|
||||||
pointFrom<GlobalPoint>(
|
bottom[1][1] + (2 / 3) * (r[1][1] - bottom[1][1]),
|
||||||
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
|
), // BOTTOM RIGHT
|
||||||
curve(
|
curve(
|
||||||
pointFromVector(offsets[3], bottom[0]),
|
bottom[0],
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[3],
|
bottom[0][0] + (2 / 3) * (r[0][0] - bottom[0][0]),
|
||||||
pointFrom<GlobalPoint>(
|
bottom[0][1] + (2 / 3) * (r[1][1] - bottom[0][1]),
|
||||||
bottom[0][0] + (2 / 3) * (r[0][0] - bottom[0][0]),
|
|
||||||
bottom[0][1] + (2 / 3) * (r[1][1] - bottom[0][1]),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[3],
|
left[0][0] + (2 / 3) * (r[0][0] - left[0][0]),
|
||||||
pointFrom<GlobalPoint>(
|
left[0][1] + (2 / 3) * (r[1][1] - left[0][1]),
|
||||||
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
|
), // BOTTOM LEFT
|
||||||
];
|
];
|
||||||
|
|
||||||
const sides = [
|
const corners =
|
||||||
lineSegment<GlobalPoint>(corners[0][3], corners[1][0]),
|
offset > 0
|
||||||
lineSegment<GlobalPoint>(corners[1][3], corners[2][0]),
|
? baseCorners.map(
|
||||||
lineSegment<GlobalPoint>(corners[2][3], corners[3][0]),
|
(corner) =>
|
||||||
lineSegment<GlobalPoint>(corners[3][3], corners[0][0]),
|
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 +315,20 @@ export function deconstructDiamondElement(
|
|||||||
element: ExcalidrawDiamondElement,
|
element: ExcalidrawDiamondElement,
|
||||||
offset: number = 0,
|
offset: number = 0,
|
||||||
): [LineSegment<GlobalPoint>[], Curve<GlobalPoint>[]] {
|
): [LineSegment<GlobalPoint>[], Curve<GlobalPoint>[]] {
|
||||||
const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
|
const cachedShape = getElementShapesCacheEntry(element, offset);
|
||||||
getDiamondPoints(element);
|
|
||||||
const verticalRadius = getCornerRadius(Math.abs(topX - leftX), element);
|
|
||||||
const horizontalRadius = getCornerRadius(Math.abs(rightY - topY), element);
|
|
||||||
|
|
||||||
if (element.roundness?.type == null) {
|
if (cachedShape) {
|
||||||
const [top, right, bottom, left]: GlobalPoint[] = [
|
return cachedShape;
|
||||||
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], []];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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[] = [
|
const [top, right, bottom, left]: GlobalPoint[] = [
|
||||||
pointFrom(element.x + topX, element.y + topY),
|
pointFrom(element.x + topX, element.y + topY),
|
||||||
@ -262,94 +337,94 @@ export function deconstructDiamondElement(
|
|||||||
pointFrom(element.x + leftX, element.y + leftY),
|
pointFrom(element.x + leftX, element.y + leftY),
|
||||||
];
|
];
|
||||||
|
|
||||||
const offsets = [
|
const baseCorners = [
|
||||||
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 = [
|
|
||||||
curve(
|
curve(
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[0],
|
right[0] - verticalRadius,
|
||||||
pointFrom<GlobalPoint>(
|
right[1] - horizontalRadius,
|
||||||
right[0] - verticalRadius,
|
|
||||||
right[1] - horizontalRadius,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
pointFromVector(offsets[0], right),
|
right,
|
||||||
pointFromVector(offsets[0], right),
|
right,
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[0],
|
right[0] - verticalRadius,
|
||||||
pointFrom<GlobalPoint>(
|
right[1] + horizontalRadius,
|
||||||
right[0] - verticalRadius,
|
|
||||||
right[1] + horizontalRadius,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
), // RIGHT
|
), // RIGHT
|
||||||
curve(
|
curve(
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[1],
|
bottom[0] + verticalRadius,
|
||||||
pointFrom<GlobalPoint>(
|
bottom[1] - horizontalRadius,
|
||||||
bottom[0] + verticalRadius,
|
|
||||||
bottom[1] - horizontalRadius,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
pointFromVector(offsets[1], bottom),
|
bottom,
|
||||||
pointFromVector(offsets[1], bottom),
|
bottom,
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[1],
|
bottom[0] - verticalRadius,
|
||||||
pointFrom<GlobalPoint>(
|
bottom[1] - horizontalRadius,
|
||||||
bottom[0] - verticalRadius,
|
|
||||||
bottom[1] - horizontalRadius,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
), // BOTTOM
|
), // BOTTOM
|
||||||
curve(
|
curve(
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[2],
|
left[0] + verticalRadius,
|
||||||
pointFrom<GlobalPoint>(
|
left[1] + horizontalRadius,
|
||||||
left[0] + verticalRadius,
|
|
||||||
left[1] + horizontalRadius,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
pointFromVector(offsets[2], left),
|
left,
|
||||||
pointFromVector(offsets[2], left),
|
left,
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[2],
|
left[0] + verticalRadius,
|
||||||
pointFrom<GlobalPoint>(
|
left[1] - horizontalRadius,
|
||||||
left[0] + verticalRadius,
|
|
||||||
left[1] - horizontalRadius,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
), // LEFT
|
), // LEFT
|
||||||
curve(
|
curve(
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[3],
|
top[0] - verticalRadius,
|
||||||
pointFrom<GlobalPoint>(
|
top[1] + horizontalRadius,
|
||||||
top[0] - verticalRadius,
|
|
||||||
top[1] + horizontalRadius,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
pointFromVector(offsets[3], top),
|
top,
|
||||||
pointFromVector(offsets[3], top),
|
top,
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[3],
|
top[0] + verticalRadius,
|
||||||
pointFrom<GlobalPoint>(
|
top[1] + horizontalRadius,
|
||||||
top[0] + verticalRadius,
|
|
||||||
top[1] + horizontalRadius,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
), // TOP
|
), // TOP
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const corners =
|
||||||
|
offset > 0
|
||||||
|
? baseCorners.map(
|
||||||
|
(corner) =>
|
||||||
|
curveCatmullRomCubicApproxPoints(
|
||||||
|
curveOffsetPoints(corner, offset),
|
||||||
|
)!,
|
||||||
|
)
|
||||||
|
: [
|
||||||
|
[baseCorners[0]],
|
||||||
|
[baseCorners[1]],
|
||||||
|
[baseCorners[2]],
|
||||||
|
[baseCorners[3]],
|
||||||
|
];
|
||||||
|
|
||||||
const sides = [
|
const sides = [
|
||||||
lineSegment<GlobalPoint>(corners[0][3], corners[1][0]),
|
lineSegment<GlobalPoint>(
|
||||||
lineSegment<GlobalPoint>(corners[1][3], corners[2][0]),
|
corners[0][corners[0].length - 1][3],
|
||||||
lineSegment<GlobalPoint>(corners[2][3], corners[3][0]),
|
corners[1][0][0],
|
||||||
lineSegment<GlobalPoint>(corners[3][3], corners[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
|
// The second rectangle is already reselected because it was the last element created
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.moveTo(10, 0);
|
||||||
mouse.click();
|
mouse.click();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -52,6 +53,7 @@ const createAndSelectTwoRectanglesWithDifferentSizes = () => {
|
|||||||
// The second rectangle is already reselected because it was the last element created
|
// The second rectangle is already reselected because it was the last element created
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.moveTo(10, 0);
|
||||||
mouse.click();
|
mouse.click();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -202,6 +204,7 @@ describe("aligning", () => {
|
|||||||
// The second rectangle is already reselected because it was the last element created
|
// The second rectangle is already reselected because it was the last element created
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.moveTo(10, 0);
|
||||||
mouse.click();
|
mouse.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -215,6 +218,7 @@ describe("aligning", () => {
|
|||||||
// Add the created group to the current selection
|
// Add the created group to the current selection
|
||||||
mouse.restorePosition(0, 0);
|
mouse.restorePosition(0, 0);
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.moveTo(10, 0);
|
||||||
mouse.click();
|
mouse.click();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -316,6 +320,7 @@ describe("aligning", () => {
|
|||||||
// The second rectangle is already selected because it was the last element created
|
// The second rectangle is already selected because it was the last element created
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.moveTo(10, 0);
|
||||||
mouse.click();
|
mouse.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -330,7 +335,7 @@ describe("aligning", () => {
|
|||||||
mouse.down();
|
mouse.down();
|
||||||
mouse.up(100, 100);
|
mouse.up(100, 100);
|
||||||
|
|
||||||
mouse.restorePosition(200, 200);
|
mouse.restorePosition(210, 200);
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
mouse.click();
|
mouse.click();
|
||||||
});
|
});
|
||||||
@ -341,6 +346,7 @@ describe("aligning", () => {
|
|||||||
// The second group is already selected because it was the last group created
|
// The second group is already selected because it was the last group created
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.moveTo(10, 0);
|
||||||
mouse.click();
|
mouse.click();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -454,6 +460,7 @@ describe("aligning", () => {
|
|||||||
// The second rectangle is already reselected because it was the last element created
|
// The second rectangle is already reselected because it was the last element created
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.moveTo(10, 0);
|
||||||
mouse.click();
|
mouse.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -466,7 +473,7 @@ describe("aligning", () => {
|
|||||||
mouse.up(100, 100);
|
mouse.up(100, 100);
|
||||||
|
|
||||||
// Add group to current selection
|
// Add group to current selection
|
||||||
mouse.restorePosition(0, 0);
|
mouse.restorePosition(10, 0);
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
mouse.click();
|
mouse.click();
|
||||||
});
|
});
|
||||||
@ -482,6 +489,7 @@ describe("aligning", () => {
|
|||||||
// Select the nested group, the rectangle is already selected
|
// Select the nested group, the rectangle is already selected
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.moveTo(10, 0);
|
||||||
mouse.click();
|
mouse.click();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -172,12 +172,12 @@ describe("element binding", () => {
|
|||||||
const arrow = UI.createElement("arrow", {
|
const arrow = UI.createElement("arrow", {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
size: 50,
|
size: 49,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(arrow.endBinding).toBe(null);
|
expect(arrow.endBinding).toBe(null);
|
||||||
|
|
||||||
mouse.downAt(50, 50);
|
mouse.downAt(49, 49);
|
||||||
mouse.moveTo(51, 0);
|
mouse.moveTo(51, 0);
|
||||||
mouse.up(0, 0);
|
mouse.up(0, 0);
|
||||||
|
|
||||||
|
38
packages/element/tests/collision.test.tsx
Normal file
38
packages/element/tests/collision.test.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { type GlobalPoint, type LocalPoint, pointFrom } from "@excalidraw/math";
|
||||||
|
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||||
|
import { UI } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||||
|
import "@excalidraw/utils/test-utils";
|
||||||
|
import { render } from "@excalidraw/excalidraw/tests/test-utils";
|
||||||
|
|
||||||
|
import { hitElementItself } from "../src/collision";
|
||||||
|
|
||||||
|
describe("check rotated elements can be hit:", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
localStorage.clear();
|
||||||
|
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("arrow", () => {
|
||||||
|
UI.createElement("arrow", {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 124,
|
||||||
|
height: 302,
|
||||||
|
angle: 1.8700426423973724,
|
||||||
|
points: [
|
||||||
|
[0, 0],
|
||||||
|
[120, -198],
|
||||||
|
[-4, -302],
|
||||||
|
] as LocalPoint[],
|
||||||
|
});
|
||||||
|
//const p = [120, -211];
|
||||||
|
//const p = [0, 13];
|
||||||
|
const hit = hitElementItself({
|
||||||
|
point: pointFrom<GlobalPoint>(87, -68),
|
||||||
|
element: window.h.elements[0],
|
||||||
|
threshold: 10,
|
||||||
|
elementsMap: window.h.scene.getNonDeletedElementsMap(),
|
||||||
|
});
|
||||||
|
expect(hit).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
@ -1262,7 +1262,7 @@ describe("Test Linear Elements", () => {
|
|||||||
mouse.downAt(rect.x, rect.y);
|
mouse.downAt(rect.x, rect.y);
|
||||||
mouse.moveTo(200, 0);
|
mouse.moveTo(200, 0);
|
||||||
mouse.upAt(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.x).toBe(200);
|
||||||
expect(rect.y).toBe(0);
|
expect(rect.y).toBe(0);
|
||||||
expect(handleBindTextResizeSpy).toHaveBeenCalledWith(
|
expect(handleBindTextResizeSpy).toHaveBeenCalledWith(
|
||||||
|
@ -510,12 +510,12 @@ describe("arrow element", () => {
|
|||||||
h.state,
|
h.state,
|
||||||
)[0] as ExcalidrawElbowArrowElement;
|
)[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);
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||||
|
|
||||||
UI.resize(rectangle, "se", [-200, -150]);
|
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);
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -538,11 +538,11 @@ describe("arrow element", () => {
|
|||||||
h.state,
|
h.state,
|
||||||
)[0] as ExcalidrawElbowArrowElement;
|
)[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);
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||||
|
|
||||||
UI.resize([rectangle, arrow], "nw", [300, 350]);
|
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);
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.25);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -819,7 +819,7 @@ describe("image element", () => {
|
|||||||
|
|
||||||
UI.resize(image, "ne", [40, 0]);
|
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 imageWidth = image.width;
|
||||||
const scale = 20 / image.height;
|
const scale = 20 / image.height;
|
||||||
@ -1033,7 +1033,7 @@ describe("multiple selection", () => {
|
|||||||
|
|
||||||
expect(leftBoundArrow.x).toBeCloseTo(-110);
|
expect(leftBoundArrow.x).toBeCloseTo(-110);
|
||||||
expect(leftBoundArrow.y).toBeCloseTo(50);
|
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.height).toBeCloseTo(7, 0);
|
||||||
expect(leftBoundArrow.angle).toEqual(0);
|
expect(leftBoundArrow.angle).toEqual(0);
|
||||||
expect(leftBoundArrow.startBinding).toBeNull();
|
expect(leftBoundArrow.startBinding).toBeNull();
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
|
|
||||||
import * as constants from "@excalidraw/common";
|
|
||||||
|
|
||||||
import { getPerfectElementSize } from "../src/sizeHelpers";
|
import { getPerfectElementSize } from "../src/sizeHelpers";
|
||||||
|
|
||||||
const EPSILON_DIGITS = 3;
|
const EPSILON_DIGITS = 3;
|
||||||
@ -57,13 +55,4 @@ describe("getPerfectElementSize", () => {
|
|||||||
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
|
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
|
||||||
expect(height).toBeCloseTo(0, EPSILON_DIGITS);
|
expect(height).toBeCloseTo(0, EPSILON_DIGITS);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("should respond to SHIFT_LOCKING_ANGLE constant", () => {
|
|
||||||
it("should have only 2 locking angles per section if SHIFT_LOCKING_ANGLE = 45 deg (Math.PI/4)", () => {
|
|
||||||
(constants as any).SHIFT_LOCKING_ANGLE = Math.PI / 4;
|
|
||||||
const { height, width } = getPerfectElementSize("arrow", 120, 185);
|
|
||||||
expect(width).toBeCloseTo(120, EPSILON_DIGITS);
|
|
||||||
expect(height).toBeCloseTo(120, EPSILON_DIGITS);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -18,7 +18,6 @@ import {
|
|||||||
arrayToMap,
|
arrayToMap,
|
||||||
getFontFamilyString,
|
getFontFamilyString,
|
||||||
getShortcutKey,
|
getShortcutKey,
|
||||||
tupleToCoors,
|
|
||||||
getLineHeight,
|
getLineHeight,
|
||||||
isTransparent,
|
isTransparent,
|
||||||
reduceToCommonValue,
|
reduceToCommonValue,
|
||||||
@ -28,9 +27,7 @@ import { canBecomePolygon, getNonDeletedElements } from "@excalidraw/element";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
bindLinearElement,
|
bindLinearElement,
|
||||||
bindPointToSnapToElementOutline,
|
|
||||||
calculateFixedPointForElbowArrowBinding,
|
calculateFixedPointForElbowArrowBinding,
|
||||||
getHoveredElementForBinding,
|
|
||||||
updateBoundElements,
|
updateBoundElements,
|
||||||
} from "@excalidraw/element";
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
@ -1661,63 +1658,16 @@ export const actionChangeArrowType = register({
|
|||||||
-1,
|
-1,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
const startHoveredElement =
|
const startElement =
|
||||||
!newElement.startBinding &&
|
newElement.startBinding &&
|
||||||
getHoveredElementForBinding(
|
(elementsMap.get(
|
||||||
tupleToCoors(startGlobalPoint),
|
newElement.startBinding.elementId,
|
||||||
elements,
|
) as ExcalidrawBindableElement);
|
||||||
elementsMap,
|
const endElement =
|
||||||
appState.zoom,
|
newElement.endBinding &&
|
||||||
false,
|
(elementsMap.get(
|
||||||
true,
|
newElement.endBinding.elementId,
|
||||||
);
|
) as ExcalidrawBindableElement);
|
||||||
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 startBinding =
|
const startBinding =
|
||||||
startElement && newElement.startBinding
|
startElement && newElement.startBinding
|
||||||
@ -1728,6 +1678,7 @@ export const actionChangeArrowType = register({
|
|||||||
newElement,
|
newElement,
|
||||||
startElement,
|
startElement,
|
||||||
"start",
|
"start",
|
||||||
|
elementsMap,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
@ -1740,6 +1691,7 @@ export const actionChangeArrowType = register({
|
|||||||
newElement,
|
newElement,
|
||||||
endElement,
|
endElement,
|
||||||
"end",
|
"end",
|
||||||
|
elementsMap,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
@ -1749,7 +1701,7 @@ export const actionChangeArrowType = register({
|
|||||||
startBinding,
|
startBinding,
|
||||||
endBinding,
|
endBinding,
|
||||||
...updateElbowArrowPoints(newElement, elementsMap, {
|
...updateElbowArrowPoints(newElement, elementsMap, {
|
||||||
points: [finalStartPoint, finalEndPoint].map(
|
points: [startGlobalPoint, endGlobalPoint].map(
|
||||||
(p): LocalPoint =>
|
(p): LocalPoint =>
|
||||||
pointFrom(p[0] - newElement.x, p[1] - newElement.y),
|
pointFrom(p[0] - newElement.x, p[1] - newElement.y),
|
||||||
),
|
),
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
STATS_PANELS,
|
STATS_PANELS,
|
||||||
THEME,
|
THEME,
|
||||||
DEFAULT_GRID_STEP,
|
DEFAULT_GRID_STEP,
|
||||||
|
isTestEnv,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import type { AppState, NormalizedZoomValue } from "./types";
|
import type { AppState, NormalizedZoomValue } from "./types";
|
||||||
@ -36,7 +37,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
currentItemRoughness: DEFAULT_ELEMENT_PROPS.roughness,
|
currentItemRoughness: DEFAULT_ELEMENT_PROPS.roughness,
|
||||||
currentItemStartArrowhead: null,
|
currentItemStartArrowhead: null,
|
||||||
currentItemStrokeColor: DEFAULT_ELEMENT_PROPS.strokeColor,
|
currentItemStrokeColor: DEFAULT_ELEMENT_PROPS.strokeColor,
|
||||||
currentItemRoundness: "round",
|
currentItemRoundness: isTestEnv() ? "sharp" : "round",
|
||||||
currentItemArrowType: ARROW_TYPE.round,
|
currentItemArrowType: ARROW_TYPE.round,
|
||||||
currentItemStrokeStyle: DEFAULT_ELEMENT_PROPS.strokeStyle,
|
currentItemStrokeStyle: DEFAULT_ELEMENT_PROPS.strokeStyle,
|
||||||
currentItemStrokeWidth: DEFAULT_ELEMENT_PROPS.strokeWidth,
|
currentItemStrokeWidth: DEFAULT_ELEMENT_PROPS.strokeWidth,
|
||||||
|
@ -17,8 +17,6 @@ import {
|
|||||||
vectorDot,
|
vectorDot,
|
||||||
vectorNormalize,
|
vectorNormalize,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
import { isPointInShape } from "@excalidraw/utils/collision";
|
|
||||||
import { getSelectionBoxShape } from "@excalidraw/utils/shape";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
COLOR_PALETTE,
|
COLOR_PALETTE,
|
||||||
@ -104,9 +102,9 @@ import {
|
|||||||
Emitter,
|
Emitter,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import { getCommonBounds, getElementAbsoluteCoords } from "@excalidraw/element";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
getCommonBounds,
|
||||||
|
getElementAbsoluteCoords,
|
||||||
bindOrUnbindLinearElements,
|
bindOrUnbindLinearElements,
|
||||||
fixBindingsAfterDeletion,
|
fixBindingsAfterDeletion,
|
||||||
getHoveredElementForBinding,
|
getHoveredElementForBinding,
|
||||||
@ -115,13 +113,8 @@ import {
|
|||||||
shouldEnableBindingForPointerEvent,
|
shouldEnableBindingForPointerEvent,
|
||||||
updateBoundElements,
|
updateBoundElements,
|
||||||
getSuggestedBindingsForArrows,
|
getSuggestedBindingsForArrows,
|
||||||
} from "@excalidraw/element";
|
LinearElementEditor,
|
||||||
|
newElementWith,
|
||||||
import { LinearElementEditor } from "@excalidraw/element";
|
|
||||||
|
|
||||||
import { newElementWith } from "@excalidraw/element";
|
|
||||||
|
|
||||||
import {
|
|
||||||
newFrameElement,
|
newFrameElement,
|
||||||
newFreeDrawElement,
|
newFreeDrawElement,
|
||||||
newEmbeddableElement,
|
newEmbeddableElement,
|
||||||
@ -133,11 +126,8 @@ import {
|
|||||||
newLinearElement,
|
newLinearElement,
|
||||||
newTextElement,
|
newTextElement,
|
||||||
refreshTextDimensions,
|
refreshTextDimensions,
|
||||||
} from "@excalidraw/element";
|
deepCopyElement,
|
||||||
|
duplicateElements,
|
||||||
import { deepCopyElement, duplicateElements } from "@excalidraw/element";
|
|
||||||
|
|
||||||
import {
|
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
isArrowElement,
|
isArrowElement,
|
||||||
isBindingElement,
|
isBindingElement,
|
||||||
@ -158,48 +148,27 @@ import {
|
|||||||
isFlowchartNodeElement,
|
isFlowchartNodeElement,
|
||||||
isBindableElement,
|
isBindableElement,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "@excalidraw/element";
|
|
||||||
|
|
||||||
import {
|
|
||||||
getLockedLinearCursorAlignSize,
|
getLockedLinearCursorAlignSize,
|
||||||
getNormalizedDimensions,
|
getNormalizedDimensions,
|
||||||
isElementCompletelyInViewport,
|
isElementCompletelyInViewport,
|
||||||
isElementInViewport,
|
isElementInViewport,
|
||||||
isInvisiblySmallElement,
|
isInvisiblySmallElement,
|
||||||
} from "@excalidraw/element";
|
|
||||||
|
|
||||||
import {
|
|
||||||
getBoundTextShape,
|
|
||||||
getCornerRadius,
|
getCornerRadius,
|
||||||
getElementShape,
|
|
||||||
isPathALoop,
|
isPathALoop,
|
||||||
} from "@excalidraw/element";
|
|
||||||
|
|
||||||
import {
|
|
||||||
createSrcDoc,
|
createSrcDoc,
|
||||||
embeddableURLValidator,
|
embeddableURLValidator,
|
||||||
maybeParseEmbedSrc,
|
maybeParseEmbedSrc,
|
||||||
getEmbedLink,
|
getEmbedLink,
|
||||||
} from "@excalidraw/element";
|
|
||||||
|
|
||||||
import {
|
|
||||||
getInitializedImageElements,
|
getInitializedImageElements,
|
||||||
loadHTMLImageElement,
|
loadHTMLImageElement,
|
||||||
normalizeSVG,
|
normalizeSVG,
|
||||||
updateImageCache as _updateImageCache,
|
updateImageCache as _updateImageCache,
|
||||||
} from "@excalidraw/element";
|
|
||||||
|
|
||||||
import {
|
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getContainerCenter,
|
getContainerCenter,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
isValidTextContainer,
|
isValidTextContainer,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
} from "@excalidraw/element";
|
shouldShowBoundingBox,
|
||||||
|
|
||||||
import { shouldShowBoundingBox } from "@excalidraw/element";
|
|
||||||
|
|
||||||
import {
|
|
||||||
getFrameChildren,
|
getFrameChildren,
|
||||||
isCursorInFrame,
|
isCursorInFrame,
|
||||||
addElementsToFrame,
|
addElementsToFrame,
|
||||||
@ -214,29 +183,17 @@ import {
|
|||||||
getFrameLikeTitle,
|
getFrameLikeTitle,
|
||||||
getElementsOverlappingFrame,
|
getElementsOverlappingFrame,
|
||||||
filterElementsEligibleAsFrameChildren,
|
filterElementsEligibleAsFrameChildren,
|
||||||
} from "@excalidraw/element";
|
|
||||||
|
|
||||||
import {
|
|
||||||
hitElementBoundText,
|
hitElementBoundText,
|
||||||
hitElementBoundingBoxOnly,
|
hitElementBoundingBoxOnly,
|
||||||
hitElementItself,
|
hitElementItself,
|
||||||
} from "@excalidraw/element";
|
getVisibleSceneBounds,
|
||||||
|
|
||||||
import { getVisibleSceneBounds } from "@excalidraw/element";
|
|
||||||
|
|
||||||
import {
|
|
||||||
FlowChartCreator,
|
FlowChartCreator,
|
||||||
FlowChartNavigator,
|
FlowChartNavigator,
|
||||||
getLinkDirectionFromKey,
|
getLinkDirectionFromKey,
|
||||||
} from "@excalidraw/element";
|
cropElement,
|
||||||
|
wrapText,
|
||||||
import { cropElement } from "@excalidraw/element";
|
isElementLink,
|
||||||
|
parseElementLinkFromURL,
|
||||||
import { wrapText } from "@excalidraw/element";
|
|
||||||
|
|
||||||
import { isElementLink, parseElementLinkFromURL } from "@excalidraw/element";
|
|
||||||
|
|
||||||
import {
|
|
||||||
isMeasureTextSupported,
|
isMeasureTextSupported,
|
||||||
normalizeText,
|
normalizeText,
|
||||||
measureText,
|
measureText,
|
||||||
@ -244,13 +201,8 @@ import {
|
|||||||
getApproxMinLineWidth,
|
getApproxMinLineWidth,
|
||||||
getApproxMinLineHeight,
|
getApproxMinLineHeight,
|
||||||
getMinTextElementWidth,
|
getMinTextElementWidth,
|
||||||
} from "@excalidraw/element";
|
ShapeCache,
|
||||||
|
getRenderOpacity,
|
||||||
import { ShapeCache } from "@excalidraw/element";
|
|
||||||
|
|
||||||
import { getRenderOpacity } from "@excalidraw/element";
|
|
||||||
|
|
||||||
import {
|
|
||||||
editGroupForSelectedElement,
|
editGroupForSelectedElement,
|
||||||
getElementsInGroup,
|
getElementsInGroup,
|
||||||
getSelectedGroupIdForElement,
|
getSelectedGroupIdForElement,
|
||||||
@ -258,42 +210,28 @@ import {
|
|||||||
isElementInGroup,
|
isElementInGroup,
|
||||||
isSelectedViaGroup,
|
isSelectedViaGroup,
|
||||||
selectGroupsForSelectedElements,
|
selectGroupsForSelectedElements,
|
||||||
} from "@excalidraw/element";
|
syncInvalidIndices,
|
||||||
|
syncMovedIndices,
|
||||||
import { syncInvalidIndices, syncMovedIndices } from "@excalidraw/element";
|
|
||||||
|
|
||||||
import {
|
|
||||||
excludeElementsInFramesFromSelection,
|
excludeElementsInFramesFromSelection,
|
||||||
getSelectionStateForElements,
|
getSelectionStateForElements,
|
||||||
makeNextSelectedElementIds,
|
makeNextSelectedElementIds,
|
||||||
} from "@excalidraw/element";
|
|
||||||
|
|
||||||
import {
|
|
||||||
getResizeOffsetXY,
|
getResizeOffsetXY,
|
||||||
getResizeArrowDirection,
|
getResizeArrowDirection,
|
||||||
transformElements,
|
transformElements,
|
||||||
} from "@excalidraw/element";
|
|
||||||
|
|
||||||
import {
|
|
||||||
getCursorForResizingElement,
|
getCursorForResizingElement,
|
||||||
getElementWithTransformHandleType,
|
getElementWithTransformHandleType,
|
||||||
getTransformHandleTypeFromCoords,
|
getTransformHandleTypeFromCoords,
|
||||||
} from "@excalidraw/element";
|
|
||||||
|
|
||||||
import {
|
|
||||||
dragNewElement,
|
dragNewElement,
|
||||||
dragSelectedElements,
|
dragSelectedElements,
|
||||||
getDragOffsetXY,
|
getDragOffsetXY,
|
||||||
|
isNonDeletedElement,
|
||||||
|
Scene,
|
||||||
|
Store,
|
||||||
|
CaptureUpdateAction,
|
||||||
|
type ElementUpdate,
|
||||||
|
hitElementBoundingBox,
|
||||||
} from "@excalidraw/element";
|
} 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 { LocalPoint, Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@ -5095,6 +5033,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: Hot path for hit testing, so avoid unnecessary computations
|
||||||
private getElementAtPosition(
|
private getElementAtPosition(
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
@ -5134,16 +5073,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// If we're hitting element with highest z-index only on its bounding box
|
// 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.
|
// while also hitting other element figure, the latter should be considered.
|
||||||
return hitElementItself({
|
return hitElementItself({
|
||||||
x,
|
point: pointFrom(x, y),
|
||||||
y,
|
|
||||||
element: elementWithHighestZIndex,
|
element: elementWithHighestZIndex,
|
||||||
shape: getElementShape(
|
|
||||||
elementWithHighestZIndex,
|
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
),
|
|
||||||
// when overlapping, we would like to be more precise
|
// when overlapping, we would like to be more precise
|
||||||
// this also avoids the need to update past tests
|
// this also avoids the need to update past tests
|
||||||
threshold: this.getElementHitThreshold() / 2,
|
threshold: this.getElementHitThreshold(elementWithHighestZIndex) / 2,
|
||||||
|
elementsMap: this.scene.getNonDeletedElementsMap(),
|
||||||
frameNameBound: isFrameLikeElement(elementWithHighestZIndex)
|
frameNameBound: isFrameLikeElement(elementWithHighestZIndex)
|
||||||
? this.frameNameBoundsCache.get(elementWithHighestZIndex)
|
? this.frameNameBoundsCache.get(elementWithHighestZIndex)
|
||||||
: null,
|
: null,
|
||||||
@ -5158,6 +5093,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: Hot path for hit testing, so avoid unnecessary computations
|
||||||
private getElementsAtPosition(
|
private getElementsAtPosition(
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
@ -5208,8 +5144,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return elements;
|
return elements;
|
||||||
}
|
}
|
||||||
|
|
||||||
getElementHitThreshold() {
|
getElementHitThreshold(element: ExcalidrawElement) {
|
||||||
return DEFAULT_COLLISION_THRESHOLD / this.state.zoom.value;
|
return Math.max(
|
||||||
|
element.strokeWidth / 2 + 0.1,
|
||||||
|
// NOTE: Here be dragons. Do not go under the 0.63 multiplier unless you're
|
||||||
|
// willing to test extensively. The hit testing starts to become unreliable
|
||||||
|
// due to FP imprecision under 0.63 in high zoom levels.
|
||||||
|
0.85 * (DEFAULT_COLLISION_THRESHOLD / this.state.zoom.value),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private hitElement(
|
private hitElement(
|
||||||
@ -5224,35 +5166,35 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.state.selectedElementIds[element.id] &&
|
this.state.selectedElementIds[element.id] &&
|
||||||
shouldShowBoundingBox([element], this.state)
|
shouldShowBoundingBox([element], this.state)
|
||||||
) {
|
) {
|
||||||
const selectionShape = getSelectionBoxShape(
|
|
||||||
element,
|
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
isImageElement(element) ? 0 : this.getElementHitThreshold(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// if hitting the bounding box, return early
|
// if hitting the bounding box, return early
|
||||||
// but if not, we should check for other cases as well (e.g. frame name)
|
// but if not, we should check for other cases as well (e.g. frame name)
|
||||||
if (isPointInShape(pointFrom(x, y), selectionShape)) {
|
if (
|
||||||
|
hitElementBoundingBox(
|
||||||
|
pointFrom(x, y),
|
||||||
|
element,
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
this.getElementHitThreshold(element),
|
||||||
|
)
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// take bound text element into consideration for hit collision as well
|
// take bound text element into consideration for hit collision as well
|
||||||
const hitBoundTextOfElement = hitElementBoundText(
|
const hitBoundTextOfElement = hitElementBoundText(
|
||||||
x,
|
pointFrom(x, y),
|
||||||
y,
|
element,
|
||||||
getBoundTextShape(element, this.scene.getNonDeletedElementsMap()),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
);
|
);
|
||||||
if (hitBoundTextOfElement) {
|
if (hitBoundTextOfElement) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return hitElementItself({
|
return hitElementItself({
|
||||||
x,
|
point: pointFrom(x, y),
|
||||||
y,
|
|
||||||
element,
|
element,
|
||||||
shape: getElementShape(element, this.scene.getNonDeletedElementsMap()),
|
threshold: this.getElementHitThreshold(element),
|
||||||
threshold: this.getElementHitThreshold(),
|
elementsMap: this.scene.getNonDeletedElementsMap(),
|
||||||
frameNameBound: isFrameLikeElement(element)
|
frameNameBound: isFrameLikeElement(element)
|
||||||
? this.frameNameBoundsCache.get(element)
|
? this.frameNameBoundsCache.get(element)
|
||||||
: null,
|
: null,
|
||||||
@ -5280,14 +5222,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (
|
if (
|
||||||
isArrowElement(elements[index]) &&
|
isArrowElement(elements[index]) &&
|
||||||
hitElementItself({
|
hitElementItself({
|
||||||
x,
|
point: pointFrom(x, y),
|
||||||
y,
|
|
||||||
element: elements[index],
|
element: elements[index],
|
||||||
shape: getElementShape(
|
elementsMap: this.scene.getNonDeletedElementsMap(),
|
||||||
elements[index],
|
threshold: this.getElementHitThreshold(elements[index]),
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
),
|
|
||||||
threshold: this.getElementHitThreshold(),
|
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
hitElement = elements[index];
|
hitElement = elements[index];
|
||||||
@ -5632,14 +5570,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
hasBoundTextElement(container) ||
|
hasBoundTextElement(container) ||
|
||||||
!isTransparent(container.backgroundColor) ||
|
!isTransparent(container.backgroundColor) ||
|
||||||
hitElementItself({
|
hitElementItself({
|
||||||
x: sceneX,
|
point: pointFrom(sceneX, sceneY),
|
||||||
y: sceneY,
|
|
||||||
element: container,
|
element: container,
|
||||||
shape: getElementShape(
|
elementsMap: this.scene.getNonDeletedElementsMap(),
|
||||||
container,
|
threshold: this.getElementHitThreshold(container),
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
),
|
|
||||||
threshold: this.getElementHitThreshold(),
|
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
const midPoint = getContainerCenter(
|
const midPoint = getContainerCenter(
|
||||||
@ -6329,13 +6263,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
let segmentMidPointHoveredCoords = null;
|
let segmentMidPointHoveredCoords = null;
|
||||||
if (
|
if (
|
||||||
hitElementItself({
|
hitElementItself({
|
||||||
x: scenePointerX,
|
point: pointFrom(scenePointerX, scenePointerY),
|
||||||
y: scenePointerY,
|
|
||||||
element,
|
element,
|
||||||
shape: getElementShape(
|
elementsMap,
|
||||||
element,
|
threshold: this.getElementHitThreshold(element),
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
hoverPointIndex = LinearElementEditor.getPointIndexUnderCursor(
|
hoverPointIndex = LinearElementEditor.getPointIndexUnderCursor(
|
||||||
@ -7505,7 +7436,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// How many pixels off the shape boundary we still consider a hit
|
// How many pixels off the shape boundary we still consider a hit
|
||||||
const threshold = this.getElementHitThreshold();
|
const threshold = Math.max(
|
||||||
|
DEFAULT_COLLISION_THRESHOLD / this.state.zoom.value,
|
||||||
|
1,
|
||||||
|
);
|
||||||
const [x1, y1, x2, y2] = getCommonBounds(selectedElements);
|
const [x1, y1, x2, y2] = getCommonBounds(selectedElements);
|
||||||
return (
|
return (
|
||||||
point.x > x1 - threshold &&
|
point.x > x1 - threshold &&
|
||||||
@ -9768,14 +9702,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
((hitElement &&
|
((hitElement &&
|
||||||
hitElementBoundingBoxOnly(
|
hitElementBoundingBoxOnly(
|
||||||
{
|
{
|
||||||
x: pointerDownState.origin.x,
|
point: pointFrom(
|
||||||
y: pointerDownState.origin.y,
|
pointerDownState.origin.x,
|
||||||
element: hitElement,
|
pointerDownState.origin.y,
|
||||||
shape: getElementShape(
|
|
||||||
hitElement,
|
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
),
|
),
|
||||||
threshold: this.getElementHitThreshold(),
|
element: hitElement,
|
||||||
|
elementsMap,
|
||||||
|
threshold: this.getElementHitThreshold(hitElement),
|
||||||
frameNameBound: isFrameLikeElement(hitElement)
|
frameNameBound: isFrameLikeElement(hitElement)
|
||||||
? this.frameNameBoundsCache.get(hitElement)
|
? this.frameNameBoundsCache.get(hitElement)
|
||||||
: null,
|
: null,
|
||||||
@ -10882,6 +10815,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
croppingElement,
|
croppingElement,
|
||||||
cropElement(
|
cropElement(
|
||||||
croppingElement,
|
croppingElement,
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
transformHandleType,
|
transformHandleType,
|
||||||
image.naturalWidth,
|
image.naturalWidth,
|
||||||
image.naturalHeight,
|
image.naturalHeight,
|
||||||
|
@ -133,7 +133,6 @@ describe("binding with linear elements", () => {
|
|||||||
const inputX = UI.queryStatsProperty("X")?.querySelector(
|
const inputX = UI.queryStatsProperty("X")?.querySelector(
|
||||||
".drag-input",
|
".drag-input",
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
|
|
||||||
expect(linear.startBinding).not.toBe(null);
|
expect(linear.startBinding).not.toBe(null);
|
||||||
expect(inputX).not.toBeNull();
|
expect(inputX).not.toBeNull();
|
||||||
UI.updateInput(inputX, String("204"));
|
UI.updateInput(inputX, String("204"));
|
||||||
@ -657,6 +656,7 @@ describe("stats for multiple elements", () => {
|
|||||||
|
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.moveTo(10, 0);
|
||||||
mouse.click();
|
mouse.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -463,7 +463,7 @@ const shouldHideLinkPopup = (
|
|||||||
|
|
||||||
const threshold = 15 / appState.zoom.value;
|
const threshold = 15 / appState.zoom.value;
|
||||||
// hitbox to prevent hiding when hovered in element bounding box
|
// 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;
|
return false;
|
||||||
}
|
}
|
||||||
const [x1, y1, x2] = getElementAbsoluteCoords(element, elementsMap);
|
const [x1, y1, x2] = getElementAbsoluteCoords(element, elementsMap);
|
||||||
|
@ -92,7 +92,7 @@ export const isPointHittingLink = (
|
|||||||
if (
|
if (
|
||||||
!isMobile &&
|
!isMobile &&
|
||||||
appState.viewModeEnabled &&
|
appState.viewModeEnabled &&
|
||||||
hitElementBoundingBox(x, y, element, elementsMap)
|
hitElementBoundingBox(pointFrom(x, y), element, elementsMap)
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -175,7 +175,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "diamond-1",
|
"elementId": "diamond-1",
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 4.545343408287929,
|
"gap": 4.535423522449215,
|
||||||
},
|
},
|
||||||
"strokeColor": "#e67700",
|
"strokeColor": "#e67700",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -335,7 +335,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "text-2",
|
"elementId": "text-2",
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 14,
|
"gap": 16,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
@ -1540,7 +1540,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "B",
|
"elementId": "B",
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 14,
|
"gap": 32,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
|
@ -781,7 +781,7 @@ describe("Test Transform", () => {
|
|||||||
expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
|
expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
|
||||||
elementId: "rect-1",
|
elementId: "rect-1",
|
||||||
focus: -0,
|
focus: -0,
|
||||||
gap: 14,
|
gap: 25,
|
||||||
});
|
});
|
||||||
expect(rect.boundElements).toStrictEqual([
|
expect(rect.boundElements).toStrictEqual([
|
||||||
{
|
{
|
||||||
|
@ -1,25 +1,19 @@
|
|||||||
import { arrayToMap, easeOut, THEME } from "@excalidraw/common";
|
import { arrayToMap, easeOut, THEME } from "@excalidraw/common";
|
||||||
import { getElementLineSegments } from "@excalidraw/element";
|
|
||||||
import {
|
import {
|
||||||
lineSegment,
|
computeBoundTextPosition,
|
||||||
lineSegmentIntersectionPoints,
|
getBoundTextElement,
|
||||||
pointFrom,
|
intersectElementWithLineSegment,
|
||||||
} from "@excalidraw/math";
|
isPointInElement,
|
||||||
|
} from "@excalidraw/element";
|
||||||
|
import { lineSegment, pointFrom } from "@excalidraw/math";
|
||||||
|
|
||||||
import { getElementsInGroup } from "@excalidraw/element";
|
import { getElementsInGroup } from "@excalidraw/element";
|
||||||
|
|
||||||
import { getElementShape } from "@excalidraw/element";
|
|
||||||
import { shouldTestInside } from "@excalidraw/element";
|
import { shouldTestInside } from "@excalidraw/element";
|
||||||
import { isPointInShape } from "@excalidraw/utils/collision";
|
|
||||||
import { hasBoundTextElement, isBoundToContainer } from "@excalidraw/element";
|
import { hasBoundTextElement, isBoundToContainer } from "@excalidraw/element";
|
||||||
import { getBoundTextElementId } from "@excalidraw/element";
|
import { getBoundTextElementId } from "@excalidraw/element";
|
||||||
|
|
||||||
import type { GeometricShape } from "@excalidraw/utils/shape";
|
import type { GlobalPoint, LineSegment } from "@excalidraw/math/types";
|
||||||
import type {
|
|
||||||
ElementsSegmentsMap,
|
|
||||||
GlobalPoint,
|
|
||||||
LineSegment,
|
|
||||||
} from "@excalidraw/math/types";
|
|
||||||
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { AnimatedTrail } from "../animated-trail";
|
import { AnimatedTrail } from "../animated-trail";
|
||||||
@ -28,15 +22,9 @@ import type { AnimationFrameHandler } from "../animation-frame-handler";
|
|||||||
|
|
||||||
import type App from "../components/App";
|
import type App from "../components/App";
|
||||||
|
|
||||||
// just enough to form a segment; this is sufficient for eraser
|
|
||||||
const POINTS_ON_TRAIL = 2;
|
|
||||||
|
|
||||||
export class EraserTrail extends AnimatedTrail {
|
export class EraserTrail extends AnimatedTrail {
|
||||||
private elementsToErase: Set<ExcalidrawElement["id"]> = new Set();
|
private elementsToErase: Set<ExcalidrawElement["id"]> = new Set();
|
||||||
private groupsToErase: 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) {
|
constructor(animationFrameHandler: AnimationFrameHandler, app: App) {
|
||||||
super(animationFrameHandler, app, {
|
super(animationFrameHandler, app, {
|
||||||
@ -79,14 +67,21 @@ export class EraserTrail extends AnimatedTrail {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private updateElementsToBeErased(restoreToErase?: boolean) {
|
private updateElementsToBeErased(restoreToErase?: boolean) {
|
||||||
let eraserPath: GlobalPoint[] =
|
const eraserPath: GlobalPoint[] =
|
||||||
super
|
super
|
||||||
.getCurrentTrail()
|
.getCurrentTrail()
|
||||||
?.originalPoints?.map((p) => pointFrom<GlobalPoint>(p[0], p[1])) || [];
|
?.originalPoints?.map((p) => pointFrom<GlobalPoint>(p[0], p[1])) || [];
|
||||||
|
|
||||||
|
if (eraserPath.length < 2) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
// for efficiency and avoid unnecessary calculations,
|
// for efficiency and avoid unnecessary calculations,
|
||||||
// take only POINTS_ON_TRAIL points to form some number of segments
|
// take only POINTS_ON_TRAIL points to form some number of segments
|
||||||
eraserPath = eraserPath?.slice(eraserPath.length - POINTS_ON_TRAIL);
|
const pathSegment = lineSegment<GlobalPoint>(
|
||||||
|
eraserPath[eraserPath.length - 1],
|
||||||
|
eraserPath[eraserPath.length - 2],
|
||||||
|
);
|
||||||
|
|
||||||
const candidateElements = this.app.visibleElements.filter(
|
const candidateElements = this.app.visibleElements.filter(
|
||||||
(el) => !el.locked,
|
(el) => !el.locked,
|
||||||
@ -94,28 +89,13 @@ export class EraserTrail extends AnimatedTrail {
|
|||||||
|
|
||||||
const candidateElementsMap = arrayToMap(candidateElements);
|
const candidateElementsMap = arrayToMap(candidateElements);
|
||||||
|
|
||||||
const pathSegments = eraserPath.reduce((acc, point, index) => {
|
|
||||||
if (index === 0) {
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
acc.push(lineSegment(eraserPath[index - 1], point));
|
|
||||||
return acc;
|
|
||||||
}, [] as LineSegment<GlobalPoint>[]);
|
|
||||||
|
|
||||||
if (pathSegments.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const element of candidateElements) {
|
for (const element of candidateElements) {
|
||||||
// restore only if already added to the to-be-erased set
|
// restore only if already added to the to-be-erased set
|
||||||
if (restoreToErase && this.elementsToErase.has(element.id)) {
|
if (restoreToErase && this.elementsToErase.has(element.id)) {
|
||||||
const intersects = eraserTest(
|
const intersects = eraserTest(
|
||||||
pathSegments,
|
pathSegment,
|
||||||
element,
|
element,
|
||||||
this.segmentsCache,
|
|
||||||
this.geometricShapesCache,
|
|
||||||
candidateElementsMap,
|
candidateElementsMap,
|
||||||
this.app,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (intersects) {
|
if (intersects) {
|
||||||
@ -148,12 +128,9 @@ export class EraserTrail extends AnimatedTrail {
|
|||||||
}
|
}
|
||||||
} else if (!restoreToErase && !this.elementsToErase.has(element.id)) {
|
} else if (!restoreToErase && !this.elementsToErase.has(element.id)) {
|
||||||
const intersects = eraserTest(
|
const intersects = eraserTest(
|
||||||
pathSegments,
|
pathSegment,
|
||||||
element,
|
element,
|
||||||
this.segmentsCache,
|
|
||||||
this.geometricShapesCache,
|
|
||||||
candidateElementsMap,
|
candidateElementsMap,
|
||||||
this.app,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (intersects) {
|
if (intersects) {
|
||||||
@ -196,45 +173,37 @@ export class EraserTrail extends AnimatedTrail {
|
|||||||
super.clearTrails();
|
super.clearTrails();
|
||||||
this.elementsToErase.clear();
|
this.elementsToErase.clear();
|
||||||
this.groupsToErase.clear();
|
this.groupsToErase.clear();
|
||||||
this.segmentsCache.clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const eraserTest = (
|
const eraserTest = (
|
||||||
pathSegments: LineSegment<GlobalPoint>[],
|
pathSegment: LineSegment<GlobalPoint>,
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
elementsSegments: ElementsSegmentsMap,
|
|
||||||
shapesCache: Map<string, GeometricShape<GlobalPoint>>,
|
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
app: App,
|
|
||||||
): boolean => {
|
): boolean => {
|
||||||
let shape = shapesCache.get(element.id);
|
const lastPoint = pathSegment[1];
|
||||||
|
if (
|
||||||
if (!shape) {
|
shouldTestInside(element) &&
|
||||||
shape = getElementShape<GlobalPoint>(element, elementsMap);
|
isPointInElement(lastPoint, element, elementsMap)
|
||||||
shapesCache.set(element.id, shape);
|
) {
|
||||||
}
|
|
||||||
|
|
||||||
const lastPoint = pathSegments[pathSegments.length - 1][1];
|
|
||||||
if (shouldTestInside(element) && isPointInShape(lastPoint, shape)) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let elementSegments = elementsSegments.get(element.id);
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
|
|
||||||
if (!elementSegments) {
|
return (
|
||||||
elementSegments = getElementLineSegments(element, elementsMap);
|
intersectElementWithLineSegment(element, elementsMap, pathSegment, 0, true)
|
||||||
elementsSegments.set(element.id, elementSegments);
|
.length > 0 ||
|
||||||
}
|
(!!boundTextElement &&
|
||||||
|
intersectElementWithLineSegment(
|
||||||
return pathSegments.some((pathSegment) =>
|
{
|
||||||
elementSegments?.some(
|
...boundTextElement,
|
||||||
(elementSegment) =>
|
...computeBoundTextPosition(element, boundTextElement, elementsMap),
|
||||||
lineSegmentIntersectionPoints(
|
},
|
||||||
pathSegment,
|
elementsMap,
|
||||||
elementSegment,
|
pathSegment,
|
||||||
app.getElementHitThreshold(),
|
0,
|
||||||
) !== null,
|
true,
|
||||||
),
|
).length > 0)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -199,6 +199,7 @@ export class LassoTrail extends AnimatedTrail {
|
|||||||
const { selectedElementIds } = getLassoSelectedElementIds({
|
const { selectedElementIds } = getLassoSelectedElementIds({
|
||||||
lassoPath,
|
lassoPath,
|
||||||
elements: this.app.visibleElements,
|
elements: this.app.visibleElements,
|
||||||
|
elementsMap: this.app.scene.getNonDeletedElementsMap(),
|
||||||
elementsSegments: this.elementsSegments,
|
elementsSegments: this.elementsSegments,
|
||||||
intersectedElements: this.intersectedElements,
|
intersectedElements: this.intersectedElements,
|
||||||
enclosedElements: this.enclosedElements,
|
enclosedElements: this.enclosedElements,
|
||||||
|
@ -3,20 +3,25 @@ import { simplify } from "points-on-curve";
|
|||||||
import {
|
import {
|
||||||
polygonFromPoints,
|
polygonFromPoints,
|
||||||
lineSegment,
|
lineSegment,
|
||||||
lineSegmentIntersectionPoints,
|
|
||||||
polygonIncludesPointNonZero,
|
polygonIncludesPointNonZero,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import type {
|
import {
|
||||||
ElementsSegmentsMap,
|
type Bounds,
|
||||||
GlobalPoint,
|
computeBoundTextPosition,
|
||||||
LineSegment,
|
doBoundsIntersect,
|
||||||
} from "@excalidraw/math/types";
|
getBoundTextElement,
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
getElementBounds,
|
||||||
|
intersectElementWithLineSegment,
|
||||||
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
|
import type { ElementsSegmentsMap, GlobalPoint } from "@excalidraw/math/types";
|
||||||
|
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
export const getLassoSelectedElementIds = (input: {
|
export const getLassoSelectedElementIds = (input: {
|
||||||
lassoPath: GlobalPoint[];
|
lassoPath: GlobalPoint[];
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
|
elementsMap: ElementsMap;
|
||||||
elementsSegments: ElementsSegmentsMap;
|
elementsSegments: ElementsSegmentsMap;
|
||||||
intersectedElements: Set<ExcalidrawElement["id"]>;
|
intersectedElements: Set<ExcalidrawElement["id"]>;
|
||||||
enclosedElements: Set<ExcalidrawElement["id"]>;
|
enclosedElements: Set<ExcalidrawElement["id"]>;
|
||||||
@ -27,6 +32,7 @@ export const getLassoSelectedElementIds = (input: {
|
|||||||
const {
|
const {
|
||||||
lassoPath,
|
lassoPath,
|
||||||
elements,
|
elements,
|
||||||
|
elementsMap,
|
||||||
elementsSegments,
|
elementsSegments,
|
||||||
intersectedElements,
|
intersectedElements,
|
||||||
enclosedElements,
|
enclosedElements,
|
||||||
@ -40,8 +46,26 @@ export const getLassoSelectedElementIds = (input: {
|
|||||||
const unlockedElements = elements.filter((el) => !el.locked);
|
const unlockedElements = elements.filter((el) => !el.locked);
|
||||||
// as the path might not enclose a shape anymore, clear before checking
|
// as the path might not enclose a shape anymore, clear before checking
|
||||||
enclosedElements.clear();
|
enclosedElements.clear();
|
||||||
|
intersectedElements.clear();
|
||||||
|
const lassoBounds = lassoPath.reduce(
|
||||||
|
(acc, item) => {
|
||||||
|
return [
|
||||||
|
Math.min(acc[0], item[0]),
|
||||||
|
Math.min(acc[1], item[1]),
|
||||||
|
Math.max(acc[2], item[0]),
|
||||||
|
Math.max(acc[3], item[1]),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
[Infinity, Infinity, -Infinity, -Infinity],
|
||||||
|
) as Bounds;
|
||||||
for (const element of unlockedElements) {
|
for (const element of unlockedElements) {
|
||||||
|
// First check if the lasso segment intersects the element's axis-aligned
|
||||||
|
// bounding box as it is much faster than checking intersection against
|
||||||
|
// the element's shape
|
||||||
|
const elementBounds = getElementBounds(element, elementsMap);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
doBoundsIntersect(lassoBounds, elementBounds) &&
|
||||||
!intersectedElements.has(element.id) &&
|
!intersectedElements.has(element.id) &&
|
||||||
!enclosedElements.has(element.id)
|
!enclosedElements.has(element.id)
|
||||||
) {
|
) {
|
||||||
@ -49,7 +73,7 @@ export const getLassoSelectedElementIds = (input: {
|
|||||||
if (enclosed) {
|
if (enclosed) {
|
||||||
enclosedElements.add(element.id);
|
enclosedElements.add(element.id);
|
||||||
} else {
|
} else {
|
||||||
const intersects = intersectionTest(path, element, elementsSegments);
|
const intersects = intersectionTest(path, element, elementsMap);
|
||||||
if (intersects) {
|
if (intersects) {
|
||||||
intersectedElements.add(element.id);
|
intersectedElements.add(element.id);
|
||||||
}
|
}
|
||||||
@ -85,26 +109,34 @@ const enclosureTest = (
|
|||||||
const intersectionTest = (
|
const intersectionTest = (
|
||||||
lassoPath: GlobalPoint[],
|
lassoPath: GlobalPoint[],
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
elementsSegments: ElementsSegmentsMap,
|
elementsMap: ElementsMap,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const elementSegments = elementsSegments.get(element.id);
|
const lassoSegments = lassoPath
|
||||||
if (!elementSegments) {
|
.slice(1)
|
||||||
return false;
|
.map((point: GlobalPoint, index) => lineSegment(lassoPath[index], point))
|
||||||
}
|
.concat([lineSegment(lassoPath[lassoPath.length - 1], lassoPath[0])]);
|
||||||
|
|
||||||
const lassoSegments = lassoPath.reduce((acc, point, index) => {
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
if (index === 0) {
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
acc.push(lineSegment(lassoPath[index - 1], point));
|
|
||||||
return acc;
|
|
||||||
}, [] as LineSegment<GlobalPoint>[]);
|
|
||||||
|
|
||||||
return lassoSegments.some((lassoSegment) =>
|
return lassoSegments.some(
|
||||||
elementSegments.some(
|
(lassoSegment) =>
|
||||||
(elementSegment) =>
|
intersectElementWithLineSegment(
|
||||||
// introduce a bit of tolerance to account for roughness and simplification of paths
|
element,
|
||||||
lineSegmentIntersectionPoints(lassoSegment, elementSegment, 1) !== null,
|
elementsMap,
|
||||||
),
|
lassoSegment,
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
).length > 0 ||
|
||||||
|
(!!boundTextElement &&
|
||||||
|
intersectElementWithLineSegment(
|
||||||
|
{
|
||||||
|
...boundTextElement,
|
||||||
|
...computeBoundTextPosition(element, boundTextElement, elementsMap),
|
||||||
|
},
|
||||||
|
elementsMap,
|
||||||
|
lassoSegment,
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
).length > 0),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -5,17 +5,14 @@ import { getDiamondPoints } from "@excalidraw/element";
|
|||||||
import { getCornerRadius } from "@excalidraw/element";
|
import { getCornerRadius } from "@excalidraw/element";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
bezierEquation,
|
|
||||||
curve,
|
curve,
|
||||||
curveTangent,
|
curveCatmullRomCubicApproxPoints,
|
||||||
|
curveCatmullRomQuadraticApproxPoints,
|
||||||
|
curveOffsetPoints,
|
||||||
type GlobalPoint,
|
type GlobalPoint,
|
||||||
|
offsetPointsForQuadraticBezier,
|
||||||
pointFrom,
|
pointFrom,
|
||||||
pointFromVector,
|
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
vector,
|
|
||||||
vectorNormal,
|
|
||||||
vectorNormalize,
|
|
||||||
vectorScale,
|
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@ -102,25 +99,14 @@ export const bootstrapCanvas = ({
|
|||||||
function drawCatmullRomQuadraticApprox(
|
function drawCatmullRomQuadraticApprox(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
points: GlobalPoint[],
|
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++) {
|
ctx.quadraticCurveTo(cpX, cpY, p2X, p2Y);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -128,35 +114,13 @@ function drawCatmullRomQuadraticApprox(
|
|||||||
function drawCatmullRomCubicApprox(
|
function drawCatmullRomCubicApprox(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
points: GlobalPoint[],
|
points: GlobalPoint[],
|
||||||
segments = 20,
|
tension = 0.5,
|
||||||
) {
|
) {
|
||||||
ctx.lineTo(points[0][0], points[0][1]);
|
const pointSets = curveCatmullRomCubicApproxPoints(points, tension);
|
||||||
|
if (pointSets) {
|
||||||
for (let i = 0; i < points.length - 1; i++) {
|
for (let i = 0; i < pointSets.length; i++) {
|
||||||
const p0 = points[i - 1 < 0 ? 0 : i - 1];
|
const [[cp1x, cp1y], [cp2x, cp2y], [x, y]] = pointSets[i];
|
||||||
const p1 = points[i];
|
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -168,7 +132,10 @@ export const drawHighlightForRectWithRotation = (
|
|||||||
) => {
|
) => {
|
||||||
const [x, y] = pointRotateRads(
|
const [x, y] = pointRotateRads(
|
||||||
pointFrom<GlobalPoint>(element.x, element.y),
|
pointFrom<GlobalPoint>(element.x, element.y),
|
||||||
elementCenterPoint(element),
|
elementCenterPoint(
|
||||||
|
element,
|
||||||
|
window.h.app.scene.getElementsMapIncludingDeleted(),
|
||||||
|
),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -187,25 +154,25 @@ export const drawHighlightForRectWithRotation = (
|
|||||||
context.beginPath();
|
context.beginPath();
|
||||||
|
|
||||||
{
|
{
|
||||||
const topLeftApprox = offsetQuadraticBezier(
|
const topLeftApprox = offsetPointsForQuadraticBezier(
|
||||||
pointFrom(0, 0 + radius),
|
pointFrom(0, 0 + radius),
|
||||||
pointFrom(0, 0),
|
pointFrom(0, 0),
|
||||||
pointFrom(0 + radius, 0),
|
pointFrom(0 + radius, 0),
|
||||||
padding,
|
padding,
|
||||||
);
|
);
|
||||||
const topRightApprox = offsetQuadraticBezier(
|
const topRightApprox = offsetPointsForQuadraticBezier(
|
||||||
pointFrom(element.width - radius, 0),
|
pointFrom(element.width - radius, 0),
|
||||||
pointFrom(element.width, 0),
|
pointFrom(element.width, 0),
|
||||||
pointFrom(element.width, radius),
|
pointFrom(element.width, radius),
|
||||||
padding,
|
padding,
|
||||||
);
|
);
|
||||||
const bottomRightApprox = offsetQuadraticBezier(
|
const bottomRightApprox = offsetPointsForQuadraticBezier(
|
||||||
pointFrom(element.width, element.height - radius),
|
pointFrom(element.width, element.height - radius),
|
||||||
pointFrom(element.width, element.height),
|
pointFrom(element.width, element.height),
|
||||||
pointFrom(element.width - radius, element.height),
|
pointFrom(element.width - radius, element.height),
|
||||||
padding,
|
padding,
|
||||||
);
|
);
|
||||||
const bottomLeftApprox = offsetQuadraticBezier(
|
const bottomLeftApprox = offsetPointsForQuadraticBezier(
|
||||||
pointFrom(radius, element.height),
|
pointFrom(radius, element.height),
|
||||||
pointFrom(0, element.height),
|
pointFrom(0, element.height),
|
||||||
pointFrom(0, element.height - radius),
|
pointFrom(0, element.height - radius),
|
||||||
@ -230,25 +197,25 @@ export const drawHighlightForRectWithRotation = (
|
|||||||
// mask" on a filled shape for the diamond highlight, because stroking creates
|
// mask" on a filled shape for the diamond highlight, because stroking creates
|
||||||
// sharp inset edges on line joins < 90 degrees.
|
// sharp inset edges on line joins < 90 degrees.
|
||||||
{
|
{
|
||||||
const topLeftApprox = offsetQuadraticBezier(
|
const topLeftApprox = offsetPointsForQuadraticBezier(
|
||||||
pointFrom(0 + radius, 0),
|
pointFrom(0 + radius, 0),
|
||||||
pointFrom(0, 0),
|
pointFrom(0, 0),
|
||||||
pointFrom(0, 0 + radius),
|
pointFrom(0, 0 + radius),
|
||||||
-FIXED_BINDING_DISTANCE,
|
-FIXED_BINDING_DISTANCE,
|
||||||
);
|
);
|
||||||
const topRightApprox = offsetQuadraticBezier(
|
const topRightApprox = offsetPointsForQuadraticBezier(
|
||||||
pointFrom(element.width, radius),
|
pointFrom(element.width, radius),
|
||||||
pointFrom(element.width, 0),
|
pointFrom(element.width, 0),
|
||||||
pointFrom(element.width - radius, 0),
|
pointFrom(element.width - radius, 0),
|
||||||
-FIXED_BINDING_DISTANCE,
|
-FIXED_BINDING_DISTANCE,
|
||||||
);
|
);
|
||||||
const bottomRightApprox = offsetQuadraticBezier(
|
const bottomRightApprox = offsetPointsForQuadraticBezier(
|
||||||
pointFrom(element.width - radius, element.height),
|
pointFrom(element.width - radius, element.height),
|
||||||
pointFrom(element.width, element.height),
|
pointFrom(element.width, element.height),
|
||||||
pointFrom(element.width, element.height - radius),
|
pointFrom(element.width, element.height - radius),
|
||||||
-FIXED_BINDING_DISTANCE,
|
-FIXED_BINDING_DISTANCE,
|
||||||
);
|
);
|
||||||
const bottomLeftApprox = offsetQuadraticBezier(
|
const bottomLeftApprox = offsetPointsForQuadraticBezier(
|
||||||
pointFrom(0, element.height - radius),
|
pointFrom(0, element.height - radius),
|
||||||
pointFrom(0, element.height),
|
pointFrom(0, element.height),
|
||||||
pointFrom(radius, element.height),
|
pointFrom(radius, element.height),
|
||||||
@ -325,7 +292,10 @@ export const drawHighlightForDiamondWithRotation = (
|
|||||||
) => {
|
) => {
|
||||||
const [x, y] = pointRotateRads(
|
const [x, y] = pointRotateRads(
|
||||||
pointFrom<GlobalPoint>(element.x, element.y),
|
pointFrom<GlobalPoint>(element.x, element.y),
|
||||||
elementCenterPoint(element),
|
elementCenterPoint(
|
||||||
|
element,
|
||||||
|
window.h.app.scene.getElementsMapIncludingDeleted(),
|
||||||
|
),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
context.save();
|
context.save();
|
||||||
@ -343,32 +313,40 @@ export const drawHighlightForDiamondWithRotation = (
|
|||||||
const horizontalRadius = element.roundness
|
const horizontalRadius = element.roundness
|
||||||
? getCornerRadius(Math.abs(rightY - topY), element)
|
? getCornerRadius(Math.abs(rightY - topY), element)
|
||||||
: (rightY - topY) * 0.01;
|
: (rightY - topY) * 0.01;
|
||||||
const topApprox = offsetCubicBezier(
|
const topApprox = curveOffsetPoints(
|
||||||
pointFrom(topX - verticalRadius, topY + horizontalRadius),
|
curve(
|
||||||
pointFrom(topX, topY),
|
pointFrom(topX - verticalRadius, topY + horizontalRadius),
|
||||||
pointFrom(topX, topY),
|
pointFrom(topX, topY),
|
||||||
pointFrom(topX + verticalRadius, topY + horizontalRadius),
|
pointFrom(topX, topY),
|
||||||
|
pointFrom(topX + verticalRadius, topY + horizontalRadius),
|
||||||
|
),
|
||||||
padding,
|
padding,
|
||||||
);
|
);
|
||||||
const rightApprox = offsetCubicBezier(
|
const rightApprox = curveOffsetPoints(
|
||||||
pointFrom(rightX - verticalRadius, rightY - horizontalRadius),
|
curve(
|
||||||
pointFrom(rightX, rightY),
|
pointFrom(rightX - verticalRadius, rightY - horizontalRadius),
|
||||||
pointFrom(rightX, rightY),
|
pointFrom(rightX, rightY),
|
||||||
pointFrom(rightX - verticalRadius, rightY + horizontalRadius),
|
pointFrom(rightX, rightY),
|
||||||
|
pointFrom(rightX - verticalRadius, rightY + horizontalRadius),
|
||||||
|
),
|
||||||
padding,
|
padding,
|
||||||
);
|
);
|
||||||
const bottomApprox = offsetCubicBezier(
|
const bottomApprox = curveOffsetPoints(
|
||||||
pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius),
|
curve(
|
||||||
pointFrom(bottomX, bottomY),
|
pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius),
|
||||||
pointFrom(bottomX, bottomY),
|
pointFrom(bottomX, bottomY),
|
||||||
pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius),
|
pointFrom(bottomX, bottomY),
|
||||||
|
pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius),
|
||||||
|
),
|
||||||
padding,
|
padding,
|
||||||
);
|
);
|
||||||
const leftApprox = offsetCubicBezier(
|
const leftApprox = curveOffsetPoints(
|
||||||
pointFrom(leftX + verticalRadius, leftY + horizontalRadius),
|
curve(
|
||||||
pointFrom(leftX, leftY),
|
pointFrom(leftX + verticalRadius, leftY + horizontalRadius),
|
||||||
pointFrom(leftX, leftY),
|
pointFrom(leftX, leftY),
|
||||||
pointFrom(leftX + verticalRadius, leftY - horizontalRadius),
|
pointFrom(leftX, leftY),
|
||||||
|
pointFrom(leftX + verticalRadius, leftY - horizontalRadius),
|
||||||
|
),
|
||||||
padding,
|
padding,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -376,13 +354,13 @@ export const drawHighlightForDiamondWithRotation = (
|
|||||||
topApprox[topApprox.length - 1][0],
|
topApprox[topApprox.length - 1][0],
|
||||||
topApprox[topApprox.length - 1][1],
|
topApprox[topApprox.length - 1][1],
|
||||||
);
|
);
|
||||||
context.lineTo(rightApprox[0][0], rightApprox[0][1]);
|
context.lineTo(rightApprox[1][0], rightApprox[1][1]);
|
||||||
drawCatmullRomCubicApprox(context, rightApprox);
|
drawCatmullRomCubicApprox(context, rightApprox);
|
||||||
context.lineTo(bottomApprox[0][0], bottomApprox[0][1]);
|
context.lineTo(bottomApprox[1][0], bottomApprox[1][1]);
|
||||||
drawCatmullRomCubicApprox(context, bottomApprox);
|
drawCatmullRomCubicApprox(context, bottomApprox);
|
||||||
context.lineTo(leftApprox[0][0], leftApprox[0][1]);
|
context.lineTo(leftApprox[1][0], leftApprox[1][1]);
|
||||||
drawCatmullRomCubicApprox(context, leftApprox);
|
drawCatmullRomCubicApprox(context, leftApprox);
|
||||||
context.lineTo(topApprox[0][0], topApprox[0][1]);
|
context.lineTo(topApprox[1][0], topApprox[1][1]);
|
||||||
drawCatmullRomCubicApprox(context, topApprox);
|
drawCatmullRomCubicApprox(context, topApprox);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -398,32 +376,40 @@ export const drawHighlightForDiamondWithRotation = (
|
|||||||
const horizontalRadius = element.roundness
|
const horizontalRadius = element.roundness
|
||||||
? getCornerRadius(Math.abs(rightY - topY), element)
|
? getCornerRadius(Math.abs(rightY - topY), element)
|
||||||
: (rightY - topY) * 0.01;
|
: (rightY - topY) * 0.01;
|
||||||
const topApprox = offsetCubicBezier(
|
const topApprox = curveOffsetPoints(
|
||||||
pointFrom(topX + verticalRadius, topY + horizontalRadius),
|
curve(
|
||||||
pointFrom(topX, topY),
|
pointFrom(topX + verticalRadius, topY + horizontalRadius),
|
||||||
pointFrom(topX, topY),
|
pointFrom(topX, topY),
|
||||||
pointFrom(topX - verticalRadius, topY + horizontalRadius),
|
pointFrom(topX, topY),
|
||||||
|
pointFrom(topX - verticalRadius, topY + horizontalRadius),
|
||||||
|
),
|
||||||
-FIXED_BINDING_DISTANCE,
|
-FIXED_BINDING_DISTANCE,
|
||||||
);
|
);
|
||||||
const rightApprox = offsetCubicBezier(
|
const rightApprox = curveOffsetPoints(
|
||||||
pointFrom(rightX - verticalRadius, rightY + horizontalRadius),
|
curve(
|
||||||
pointFrom(rightX, rightY),
|
pointFrom(rightX - verticalRadius, rightY + horizontalRadius),
|
||||||
pointFrom(rightX, rightY),
|
pointFrom(rightX, rightY),
|
||||||
pointFrom(rightX - verticalRadius, rightY - horizontalRadius),
|
pointFrom(rightX, rightY),
|
||||||
|
pointFrom(rightX - verticalRadius, rightY - horizontalRadius),
|
||||||
|
),
|
||||||
-FIXED_BINDING_DISTANCE,
|
-FIXED_BINDING_DISTANCE,
|
||||||
);
|
);
|
||||||
const bottomApprox = offsetCubicBezier(
|
const bottomApprox = curveOffsetPoints(
|
||||||
pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius),
|
curve(
|
||||||
pointFrom(bottomX, bottomY),
|
pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius),
|
||||||
pointFrom(bottomX, bottomY),
|
pointFrom(bottomX, bottomY),
|
||||||
pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius),
|
pointFrom(bottomX, bottomY),
|
||||||
|
pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius),
|
||||||
|
),
|
||||||
-FIXED_BINDING_DISTANCE,
|
-FIXED_BINDING_DISTANCE,
|
||||||
);
|
);
|
||||||
const leftApprox = offsetCubicBezier(
|
const leftApprox = curveOffsetPoints(
|
||||||
pointFrom(leftX + verticalRadius, leftY - horizontalRadius),
|
curve(
|
||||||
pointFrom(leftX, leftY),
|
pointFrom(leftX + verticalRadius, leftY - horizontalRadius),
|
||||||
pointFrom(leftX, leftY),
|
pointFrom(leftX, leftY),
|
||||||
pointFrom(leftX + verticalRadius, leftY + horizontalRadius),
|
pointFrom(leftX, leftY),
|
||||||
|
pointFrom(leftX + verticalRadius, leftY + horizontalRadius),
|
||||||
|
),
|
||||||
-FIXED_BINDING_DISTANCE,
|
-FIXED_BINDING_DISTANCE,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -431,66 +417,16 @@ export const drawHighlightForDiamondWithRotation = (
|
|||||||
topApprox[topApprox.length - 1][0],
|
topApprox[topApprox.length - 1][0],
|
||||||
topApprox[topApprox.length - 1][1],
|
topApprox[topApprox.length - 1][1],
|
||||||
);
|
);
|
||||||
context.lineTo(leftApprox[0][0], leftApprox[0][1]);
|
context.lineTo(leftApprox[1][0], leftApprox[1][1]);
|
||||||
drawCatmullRomCubicApprox(context, leftApprox);
|
drawCatmullRomCubicApprox(context, leftApprox);
|
||||||
context.lineTo(bottomApprox[0][0], bottomApprox[0][1]);
|
context.lineTo(bottomApprox[1][0], bottomApprox[1][1]);
|
||||||
drawCatmullRomCubicApprox(context, bottomApprox);
|
drawCatmullRomCubicApprox(context, bottomApprox);
|
||||||
context.lineTo(rightApprox[0][0], rightApprox[0][1]);
|
context.lineTo(rightApprox[1][0], rightApprox[1][1]);
|
||||||
drawCatmullRomCubicApprox(context, rightApprox);
|
drawCatmullRomCubicApprox(context, rightApprox);
|
||||||
context.lineTo(topApprox[0][0], topApprox[0][1]);
|
context.lineTo(topApprox[1][0], topApprox[1][1]);
|
||||||
drawCatmullRomCubicApprox(context, topApprox);
|
drawCatmullRomCubicApprox(context, topApprox);
|
||||||
}
|
}
|
||||||
context.closePath();
|
context.closePath();
|
||||||
context.fill();
|
context.fill();
|
||||||
context.restore();
|
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;
|
|
||||||
}
|
|
||||||
|
@ -193,16 +193,10 @@ const renderBindingHighlightForBindableElement = (
|
|||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
zoom: InteractiveCanvasAppState["zoom"],
|
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);
|
const padding = maxBindingGap(element, element.width, element.height, zoom);
|
||||||
|
|
||||||
|
context.fillStyle = "rgba(0,0,0,.05)";
|
||||||
|
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "text":
|
case "text":
|
||||||
@ -216,10 +210,13 @@ const renderBindingHighlightForBindableElement = (
|
|||||||
case "diamond":
|
case "diamond":
|
||||||
drawHighlightForDiamondWithRotation(context, padding, element);
|
drawHighlightForDiamondWithRotation(context, padding, element);
|
||||||
break;
|
break;
|
||||||
case "ellipse":
|
case "ellipse": {
|
||||||
context.lineWidth =
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
||||||
maxBindingGap(element, element.width, element.height, zoom) -
|
const width = x2 - x1;
|
||||||
FIXED_BINDING_DISTANCE;
|
const height = y2 - y1;
|
||||||
|
|
||||||
|
context.strokeStyle = "rgba(0,0,0,.05)";
|
||||||
|
context.lineWidth = padding - FIXED_BINDING_DISTANCE;
|
||||||
|
|
||||||
strokeEllipseWithRotation(
|
strokeEllipseWithRotation(
|
||||||
context,
|
context,
|
||||||
@ -230,6 +227,7 @@ const renderBindingHighlightForBindableElement = (
|
|||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -898,7 +898,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
"currentItemRoundness": "round",
|
"currentItemRoundness": "sharp",
|
||||||
"currentItemStartArrowhead": null,
|
"currentItemStartArrowhead": null,
|
||||||
"currentItemStrokeColor": "#1e1e1e",
|
"currentItemStrokeColor": "#1e1e1e",
|
||||||
"currentItemStrokeStyle": "solid",
|
"currentItemStrokeStyle": "solid",
|
||||||
@ -1016,9 +1016,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 1,
|
"seed": 1,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -1052,9 +1050,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 1,
|
"seed": 1,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -1101,7 +1097,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
"currentItemRoundness": "round",
|
"currentItemRoundness": "sharp",
|
||||||
"currentItemStartArrowhead": null,
|
"currentItemStartArrowhead": null,
|
||||||
"currentItemStrokeColor": "#1e1e1e",
|
"currentItemStrokeColor": "#1e1e1e",
|
||||||
"currentItemStrokeStyle": "solid",
|
"currentItemStrokeStyle": "solid",
|
||||||
@ -1162,7 +1158,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": true,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
@ -1205,7 +1201,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 20,
|
"height": 10,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -1213,20 +1209,18 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
"seed": 1278240551,
|
||||||
},
|
|
||||||
"seed": 449462985,
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 1150084233,
|
"versionNonce": 2019559783,
|
||||||
"width": 20,
|
"width": 10,
|
||||||
"x": -10,
|
"x": -20,
|
||||||
"y": 0,
|
"y": -10,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -1263,23 +1257,21 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 20,
|
"height": 10,
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"width": 20,
|
"width": 10,
|
||||||
"x": -10,
|
"x": -20,
|
||||||
"y": 0,
|
"y": -10,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
@ -1317,7 +1309,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
"currentItemRoundness": "round",
|
"currentItemRoundness": "sharp",
|
||||||
"currentItemStartArrowhead": null,
|
"currentItemStartArrowhead": null,
|
||||||
"currentItemStrokeColor": "#1e1e1e",
|
"currentItemStrokeColor": "#1e1e1e",
|
||||||
"currentItemStrokeStyle": "solid",
|
"currentItemStrokeStyle": "solid",
|
||||||
@ -1427,9 +1419,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 1014066025,
|
"seed": 1014066025,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -1461,9 +1451,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 449462985,
|
"seed": 449462985,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -1518,9 +1506,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
@ -1572,9 +1558,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
@ -1650,7 +1634,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
"currentItemRoundness": "round",
|
"currentItemRoundness": "sharp",
|
||||||
"currentItemStartArrowhead": null,
|
"currentItemStartArrowhead": null,
|
||||||
"currentItemStrokeColor": "#1e1e1e",
|
"currentItemStrokeColor": "#1e1e1e",
|
||||||
"currentItemStrokeStyle": "solid",
|
"currentItemStrokeStyle": "solid",
|
||||||
@ -1760,9 +1744,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 1014066025,
|
"seed": 1014066025,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -1794,9 +1776,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 449462985,
|
"seed": 449462985,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -1851,9 +1831,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
@ -1905,9 +1883,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
@ -1983,7 +1959,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
|||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
"currentItemRoundness": "round",
|
"currentItemRoundness": "sharp",
|
||||||
"currentItemStartArrowhead": null,
|
"currentItemStartArrowhead": null,
|
||||||
"currentItemStrokeColor": "#1e1e1e",
|
"currentItemStrokeColor": "#1e1e1e",
|
||||||
"currentItemStrokeStyle": "solid",
|
"currentItemStrokeStyle": "solid",
|
||||||
@ -2044,7 +2020,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": true,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
@ -2087,7 +2063,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 20,
|
"height": 10,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -2095,20 +2071,18 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
"seed": 1278240551,
|
||||||
},
|
|
||||||
"seed": 449462985,
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 1150084233,
|
"versionNonce": 2019559783,
|
||||||
"width": 20,
|
"width": 10,
|
||||||
"x": -10,
|
"x": -20,
|
||||||
"y": 0,
|
"y": -10,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -2145,23 +2119,21 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 20,
|
"height": 10,
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"width": 20,
|
"width": 10,
|
||||||
"x": -10,
|
"x": -20,
|
||||||
"y": 0,
|
"y": -10,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
@ -2199,7 +2171,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
"currentItemRoundness": "round",
|
"currentItemRoundness": "sharp",
|
||||||
"currentItemStartArrowhead": null,
|
"currentItemStartArrowhead": null,
|
||||||
"currentItemStrokeColor": "#1e1e1e",
|
"currentItemStrokeColor": "#1e1e1e",
|
||||||
"currentItemStrokeStyle": "solid",
|
"currentItemStrokeStyle": "solid",
|
||||||
@ -2299,7 +2271,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 20,
|
"height": 10,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
@ -2307,20 +2279,18 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
"seed": 1278240551,
|
||||||
},
|
|
||||||
"seed": 449462985,
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 1014066025,
|
"versionNonce": 1116226695,
|
||||||
"width": 20,
|
"width": 10,
|
||||||
"x": -10,
|
"x": -20,
|
||||||
"y": 0,
|
"y": -10,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -2357,23 +2327,21 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 20,
|
"height": 10,
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"width": 20,
|
"width": 10,
|
||||||
"x": -10,
|
"x": -20,
|
||||||
"y": 0,
|
"y": -10,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
@ -2440,7 +2408,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
"currentItemRoundness": "round",
|
"currentItemRoundness": "sharp",
|
||||||
"currentItemStartArrowhead": null,
|
"currentItemStartArrowhead": null,
|
||||||
"currentItemStrokeColor": "#1e1e1e",
|
"currentItemStrokeColor": "#1e1e1e",
|
||||||
"currentItemStrokeStyle": "solid",
|
"currentItemStrokeStyle": "solid",
|
||||||
@ -2542,7 +2510,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 20,
|
"height": 10,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -2550,20 +2518,18 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
"seed": 1278240551,
|
||||||
},
|
|
||||||
"seed": 449462985,
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 1150084233,
|
"versionNonce": 2019559783,
|
||||||
"width": 20,
|
"width": 10,
|
||||||
"x": -10,
|
"x": -20,
|
||||||
"y": 0,
|
"y": -10,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -2576,7 +2542,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 20,
|
"height": 10,
|
||||||
"id": "id3",
|
"id": "id3",
|
||||||
"index": "a1",
|
"index": "a1",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -2584,9 +2550,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 1014066025,
|
"seed": 1014066025,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -2595,9 +2559,9 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 5,
|
"version": 5,
|
||||||
"versionNonce": 400692809,
|
"versionNonce": 400692809,
|
||||||
"width": 20,
|
"width": 10,
|
||||||
"x": 0,
|
"x": -10,
|
||||||
"y": 10,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -2634,23 +2598,21 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 20,
|
"height": 10,
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"width": 20,
|
"width": 10,
|
||||||
"x": -10,
|
"x": -20,
|
||||||
"y": 0,
|
"y": -10,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
@ -2688,23 +2650,21 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 20,
|
"height": 10,
|
||||||
"index": "a1",
|
"index": "a1",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"width": 20,
|
"width": 10,
|
||||||
"x": 0,
|
"x": -10,
|
||||||
"y": 10,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
@ -2742,7 +2702,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
"currentItemRoundness": "round",
|
"currentItemRoundness": "sharp",
|
||||||
"currentItemStartArrowhead": null,
|
"currentItemStartArrowhead": null,
|
||||||
"currentItemStrokeColor": "#1e1e1e",
|
"currentItemStrokeColor": "#1e1e1e",
|
||||||
"currentItemStrokeStyle": "solid",
|
"currentItemStrokeStyle": "solid",
|
||||||
@ -2859,9 +2819,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 449462985,
|
"seed": 449462985,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -2895,9 +2853,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 1014066025,
|
"seed": 1014066025,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -2952,9 +2908,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
@ -3006,9 +2960,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
@ -3114,7 +3066,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 60,
|
"currentItemOpacity": 60,
|
||||||
"currentItemRoughness": 2,
|
"currentItemRoughness": 2,
|
||||||
"currentItemRoundness": "round",
|
"currentItemRoundness": "sharp",
|
||||||
"currentItemStartArrowhead": null,
|
"currentItemStartArrowhead": null,
|
||||||
"currentItemStrokeColor": "#e03131",
|
"currentItemStrokeColor": "#e03131",
|
||||||
"currentItemStrokeStyle": "dotted",
|
"currentItemStrokeStyle": "dotted",
|
||||||
@ -3226,9 +3178,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 60,
|
"opacity": 60,
|
||||||
"roughness": 2,
|
"roughness": 2,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 449462985,
|
"seed": 449462985,
|
||||||
"strokeColor": "#e03131",
|
"strokeColor": "#e03131",
|
||||||
"strokeStyle": "dotted",
|
"strokeStyle": "dotted",
|
||||||
@ -3236,7 +3186,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 941653321,
|
"versionNonce": 908564423,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@ -3260,17 +3210,15 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 60,
|
"opacity": 60,
|
||||||
"roughness": 2,
|
"roughness": 2,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
"seed": 1315507081,
|
||||||
},
|
|
||||||
"seed": 289600103,
|
|
||||||
"strokeColor": "#e03131",
|
"strokeColor": "#e03131",
|
||||||
"strokeStyle": "dotted",
|
"strokeStyle": "dotted",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 9,
|
"version": 9,
|
||||||
"versionNonce": 640725609,
|
"versionNonce": 406373543,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
@ -3317,9 +3265,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
@ -3371,9 +3317,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
@ -3597,7 +3541,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
"currentItemRoundness": "round",
|
"currentItemRoundness": "sharp",
|
||||||
"currentItemStartArrowhead": null,
|
"currentItemStartArrowhead": null,
|
||||||
"currentItemStrokeColor": "#1e1e1e",
|
"currentItemStrokeColor": "#1e1e1e",
|
||||||
"currentItemStrokeStyle": "solid",
|
"currentItemStrokeStyle": "solid",
|
||||||
@ -3707,17 +3651,15 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
"seed": 1150084233,
|
||||||
},
|
|
||||||
"seed": 1014066025,
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 23633383,
|
"versionNonce": 1604849351,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
@ -3741,17 +3683,15 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
"seed": 1278240551,
|
||||||
},
|
|
||||||
"seed": 449462985,
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 1150084233,
|
"versionNonce": 401146281,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@ -3798,9 +3738,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
@ -3852,9 +3790,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
@ -3922,7 +3858,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
"currentItemRoundness": "round",
|
"currentItemRoundness": "sharp",
|
||||||
"currentItemStartArrowhead": null,
|
"currentItemStartArrowhead": null,
|
||||||
"currentItemStrokeColor": "#1e1e1e",
|
"currentItemStrokeColor": "#1e1e1e",
|
||||||
"currentItemStrokeStyle": "solid",
|
"currentItemStrokeStyle": "solid",
|
||||||
@ -4032,9 +3968,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 1014066025,
|
"seed": 1014066025,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -4066,9 +4000,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 449462985,
|
"seed": 449462985,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -4123,9 +4055,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
@ -4177,9 +4107,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
@ -4247,7 +4175,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
"currentItemRoundness": "round",
|
"currentItemRoundness": "sharp",
|
||||||
"currentItemStartArrowhead": null,
|
"currentItemStartArrowhead": null,
|
||||||
"currentItemStrokeColor": "#1e1e1e",
|
"currentItemStrokeColor": "#1e1e1e",
|
||||||
"currentItemStrokeStyle": "solid",
|
"currentItemStrokeStyle": "solid",
|
||||||
@ -4360,9 +4288,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 449462985,
|
"seed": 449462985,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -4394,9 +4320,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 238820263,
|
"seed": 238820263,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -4451,9 +4375,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
@ -4505,9 +4427,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
@ -5528,7 +5448,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
"currentItemRoundness": "round",
|
"currentItemRoundness": "sharp",
|
||||||
"currentItemStartArrowhead": null,
|
"currentItemStartArrowhead": null,
|
||||||
"currentItemStrokeColor": "#1e1e1e",
|
"currentItemStrokeColor": "#1e1e1e",
|
||||||
"currentItemStrokeStyle": "solid",
|
"currentItemStrokeStyle": "solid",
|
||||||
@ -5641,9 +5561,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 453191,
|
"seed": 453191,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -5675,17 +5593,15 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
"seed": 1604849351,
|
||||||
},
|
|
||||||
"seed": 400692809,
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 23633383,
|
"versionNonce": 493213705,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 12,
|
"x": 12,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@ -5732,9 +5648,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
@ -5786,9 +5700,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
@ -6749,7 +6661,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
"currentItemRoundness": "round",
|
"currentItemRoundness": "sharp",
|
||||||
"currentItemStartArrowhead": null,
|
"currentItemStartArrowhead": null,
|
||||||
"currentItemStrokeColor": "#1e1e1e",
|
"currentItemStrokeColor": "#1e1e1e",
|
||||||
"currentItemStrokeStyle": "solid",
|
"currentItemStrokeStyle": "solid",
|
||||||
@ -6866,9 +6778,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 449462985,
|
"seed": 449462985,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -6902,9 +6812,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 238820263,
|
"seed": 238820263,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -6959,9 +6867,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
@ -7013,9 +6919,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
@ -7684,7 +7588,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
"currentItemRoundness": "round",
|
"currentItemRoundness": "sharp",
|
||||||
"currentItemStartArrowhead": null,
|
"currentItemStartArrowhead": null,
|
||||||
"currentItemStrokeColor": "#1e1e1e",
|
"currentItemStrokeColor": "#1e1e1e",
|
||||||
"currentItemStrokeStyle": "solid",
|
"currentItemStrokeStyle": "solid",
|
||||||
@ -8684,7 +8588,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
"currentItemRoundness": "round",
|
"currentItemRoundness": "sharp",
|
||||||
"currentItemStartArrowhead": null,
|
"currentItemStartArrowhead": null,
|
||||||
"currentItemStrokeColor": "#1e1e1e",
|
"currentItemStrokeColor": "#1e1e1e",
|
||||||
"currentItemStrokeStyle": "solid",
|
"currentItemStrokeStyle": "solid",
|
||||||
@ -8745,7 +8649,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": true,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
@ -9675,7 +9579,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
"currentItemRoundness": "round",
|
"currentItemRoundness": "sharp",
|
||||||
"currentItemStartArrowhead": null,
|
"currentItemStartArrowhead": null,
|
||||||
"currentItemStrokeColor": "#1e1e1e",
|
"currentItemStrokeColor": "#1e1e1e",
|
||||||
"currentItemStrokeStyle": "solid",
|
"currentItemStrokeStyle": "solid",
|
||||||
@ -9780,7 +9684,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] el
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 20,
|
"height": 10,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -9788,20 +9692,18 @@ exports[`contextMenu element > shows context menu for element > [end of test] el
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
"seed": 1278240551,
|
||||||
},
|
|
||||||
"seed": 449462985,
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 1150084233,
|
"versionNonce": 2019559783,
|
||||||
"width": 20,
|
"width": 10,
|
||||||
"x": -10,
|
"x": -20,
|
||||||
"y": 0,
|
"y": -10,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -9822,9 +9724,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] el
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 1,
|
"seed": 1,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -9856,9 +9756,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] el
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 1,
|
"seed": 1,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -9912,23 +9810,21 @@ exports[`contextMenu element > shows context menu for element > [end of test] un
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 20,
|
"height": 10,
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"width": 20,
|
"width": 10,
|
||||||
"x": -10,
|
"x": -20,
|
||||||
"y": 0,
|
"y": -10,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
@ -71,9 +71,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 2,
|
|
||||||
},
|
|
||||||
"seed": 1278240551,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -107,9 +105,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 2,
|
|
||||||
},
|
|
||||||
"seed": 1278240551,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -155,9 +151,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
|
|||||||
],
|
],
|
||||||
"polygon": false,
|
"polygon": false,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 2,
|
|
||||||
},
|
|
||||||
"seed": 1278240551,
|
"seed": 1278240551,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
@ -193,9 +187,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 1278240551,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -17,9 +17,7 @@ exports[`duplicate element on move when ALT is clicked > rectangle 5`] = `
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 1278240551,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -51,9 +49,7 @@ exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 1604849351,
|
"seed": 1604849351,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -85,9 +81,7 @@ exports[`move element > rectangle 5`] = `
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 1278240551,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -124,9 +118,7 @@ exports[`move element > rectangles with binding arrow 5`] = `
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 1278240551,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -163,9 +155,7 @@ exports[`move element > rectangles with binding arrow 6`] = `
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 1150084233,
|
"seed": 1150084233,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -196,7 +186,7 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": "87.29887",
|
"height": "81.40630",
|
||||||
"id": "id6",
|
"id": "id6",
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@ -210,8 +200,8 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"86.85786",
|
"81.00000",
|
||||||
"87.29887",
|
"81.40630",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@ -232,8 +222,8 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 11,
|
"version": 11,
|
||||||
"versionNonce": 1996028265,
|
"versionNonce": 1996028265,
|
||||||
"width": "86.85786",
|
"width": "81.00000",
|
||||||
"x": "107.07107",
|
"x": "110.00000",
|
||||||
"y": "47.07107",
|
"y": 50,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -95,9 +95,7 @@ exports[`multi point mode in linear elements > line 3`] = `
|
|||||||
],
|
],
|
||||||
"polygon": false,
|
"polygon": false,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 2,
|
|
||||||
},
|
|
||||||
"seed": 1278240551,
|
"seed": 1278240551,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -81,9 +81,7 @@ exports[`select single element on the scene > arrow escape 1`] = `
|
|||||||
],
|
],
|
||||||
"polygon": false,
|
"polygon": false,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 2,
|
|
||||||
},
|
|
||||||
"seed": 1278240551,
|
"seed": 1278240551,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
@ -117,9 +115,7 @@ exports[`select single element on the scene > diamond 1`] = `
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 2,
|
|
||||||
},
|
|
||||||
"seed": 1278240551,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -151,9 +147,7 @@ exports[`select single element on the scene > ellipse 1`] = `
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 2,
|
|
||||||
},
|
|
||||||
"seed": 1278240551,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -185,9 +179,7 @@ exports[`select single element on the scene > rectangle 1`] = `
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 1278240551,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
|
@ -110,8 +110,8 @@ describe("contextMenu element", () => {
|
|||||||
|
|
||||||
it("shows context menu for element", () => {
|
it("shows context menu for element", () => {
|
||||||
UI.clickTool("rectangle");
|
UI.clickTool("rectangle");
|
||||||
mouse.down(10, 10);
|
mouse.down(0, 0);
|
||||||
mouse.up(20, 20);
|
mouse.up(10, 10);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
@ -304,8 +304,8 @@ describe("contextMenu element", () => {
|
|||||||
|
|
||||||
it("selecting 'Copy styles' in context menu copies styles", () => {
|
it("selecting 'Copy styles' in context menu copies styles", () => {
|
||||||
UI.clickTool("rectangle");
|
UI.clickTool("rectangle");
|
||||||
mouse.down(10, 10);
|
mouse.down(0, 0);
|
||||||
mouse.up(20, 20);
|
mouse.up(10, 10);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
@ -389,8 +389,8 @@ describe("contextMenu element", () => {
|
|||||||
|
|
||||||
it("selecting 'Delete' in context menu deletes element", () => {
|
it("selecting 'Delete' in context menu deletes element", () => {
|
||||||
UI.clickTool("rectangle");
|
UI.clickTool("rectangle");
|
||||||
mouse.down(10, 10);
|
mouse.down(0, 0);
|
||||||
mouse.up(20, 20);
|
mouse.up(10, 10);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
@ -405,8 +405,8 @@ describe("contextMenu element", () => {
|
|||||||
|
|
||||||
it("selecting 'Add to library' in context menu adds element to library", async () => {
|
it("selecting 'Add to library' in context menu adds element to library", async () => {
|
||||||
UI.clickTool("rectangle");
|
UI.clickTool("rectangle");
|
||||||
mouse.down(10, 10);
|
mouse.down(0, 0);
|
||||||
mouse.up(20, 20);
|
mouse.up(10, 10);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
@ -424,8 +424,8 @@ describe("contextMenu element", () => {
|
|||||||
|
|
||||||
it("selecting 'Duplicate' in context menu duplicates element", () => {
|
it("selecting 'Duplicate' in context menu duplicates element", () => {
|
||||||
UI.clickTool("rectangle");
|
UI.clickTool("rectangle");
|
||||||
mouse.down(10, 10);
|
mouse.down(0, 0);
|
||||||
mouse.up(20, 20);
|
mouse.up(10, 10);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
|
@ -31,9 +31,7 @@ exports[`restoreElements > should restore arrow element correctly 1`] = `
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 2,
|
|
||||||
},
|
|
||||||
"seed": Any<Number>,
|
"seed": Any<Number>,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
@ -193,9 +191,7 @@ exports[`restoreElements > should restore freedraw element correctly 1`] = `
|
|||||||
],
|
],
|
||||||
"pressures": [],
|
"pressures": [],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": Any<Number>,
|
"seed": Any<Number>,
|
||||||
"simulatePressure": true,
|
"simulatePressure": true,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
@ -242,9 +238,7 @@ exports[`restoreElements > should restore line and draw elements correctly 1`] =
|
|||||||
],
|
],
|
||||||
"polygon": false,
|
"polygon": false,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 2,
|
|
||||||
},
|
|
||||||
"seed": Any<Number>,
|
"seed": Any<Number>,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
@ -292,9 +286,7 @@ exports[`restoreElements > should restore line and draw elements correctly 2`] =
|
|||||||
],
|
],
|
||||||
"polygon": false,
|
"polygon": false,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 2,
|
|
||||||
},
|
|
||||||
"seed": Any<Number>,
|
"seed": Any<Number>,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
@ -334,9 +326,7 @@ exports[`restoreElements > should restore text element correctly passing value f
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"originalText": "text",
|
"originalText": "text",
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": Any<Number>,
|
"seed": Any<Number>,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -378,9 +368,7 @@ exports[`restoreElements > should restore text element correctly with unknown fo
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"originalText": "",
|
"originalText": "",
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": {
|
"roundness": null,
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": Any<Number>,
|
"seed": Any<Number>,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
|
@ -32,6 +32,7 @@ import type {
|
|||||||
ExcalidrawTextContainer,
|
ExcalidrawTextContainer,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
|
ElementsMap,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { createTestHook } from "../../components/App";
|
import { createTestHook } from "../../components/App";
|
||||||
@ -146,6 +147,7 @@ export class Keyboard {
|
|||||||
|
|
||||||
const getElementPointForSelection = (
|
const getElementPointForSelection = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
const { x, y, width, angle } = element;
|
const { x, y, width, angle } = element;
|
||||||
const target = pointFrom<GlobalPoint>(
|
const target = pointFrom<GlobalPoint>(
|
||||||
@ -162,7 +164,7 @@ const getElementPointForSelection = (
|
|||||||
(bounds[1] + bounds[3]) / 2,
|
(bounds[1] + bounds[3]) / 2,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
center = elementCenterPoint(element);
|
center = elementCenterPoint(element, elementsMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTextElement(element)) {
|
if (isTextElement(element)) {
|
||||||
@ -299,7 +301,12 @@ export class Pointer {
|
|||||||
elements = Array.isArray(elements) ? elements : [elements];
|
elements = Array.isArray(elements) ? elements : [elements];
|
||||||
elements.forEach((element) => {
|
elements.forEach((element) => {
|
||||||
this.reset();
|
this.reset();
|
||||||
this.click(...getElementPointForSelection(element));
|
this.click(
|
||||||
|
...getElementPointForSelection(
|
||||||
|
element,
|
||||||
|
h.app.scene.getElementsMapIncludingDeleted(),
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -308,13 +315,23 @@ export class Pointer {
|
|||||||
|
|
||||||
clickOn(element: ExcalidrawElement) {
|
clickOn(element: ExcalidrawElement) {
|
||||||
this.reset();
|
this.reset();
|
||||||
this.click(...getElementPointForSelection(element));
|
this.click(
|
||||||
|
...getElementPointForSelection(
|
||||||
|
element,
|
||||||
|
h.app.scene.getElementsMapIncludingDeleted(),
|
||||||
|
),
|
||||||
|
);
|
||||||
this.reset();
|
this.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
doubleClickOn(element: ExcalidrawElement) {
|
doubleClickOn(element: ExcalidrawElement) {
|
||||||
this.reset();
|
this.reset();
|
||||||
this.doubleClick(...getElementPointForSelection(element));
|
this.doubleClick(
|
||||||
|
...getElementPointForSelection(
|
||||||
|
element,
|
||||||
|
h.app.scene.getElementsMapIncludingDeleted(),
|
||||||
|
),
|
||||||
|
);
|
||||||
this.reset();
|
this.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -598,6 +615,7 @@ export class UI {
|
|||||||
|
|
||||||
const mutations = cropElement(
|
const mutations = cropElement(
|
||||||
element,
|
element,
|
||||||
|
h.scene.getNonDeletedElementsMap(),
|
||||||
handle,
|
handle,
|
||||||
naturalWidth,
|
naturalWidth,
|
||||||
naturalHeight,
|
naturalHeight,
|
||||||
|
@ -70,6 +70,7 @@ const updatePath = (startPoint: GlobalPoint, points: LocalPoint[]) => {
|
|||||||
?.originalPoints?.map((p) => pointFrom<GlobalPoint>(p[0], p[1])) ??
|
?.originalPoints?.map((p) => pointFrom<GlobalPoint>(p[0], p[1])) ??
|
||||||
[],
|
[],
|
||||||
elements: h.elements,
|
elements: h.elements,
|
||||||
|
elementsMap: h.scene.getNonDeletedElementsMap(),
|
||||||
elementsSegments,
|
elementsSegments,
|
||||||
intersectedElements: new Set(),
|
intersectedElements: new Set(),
|
||||||
enclosedElements: new Set(),
|
enclosedElements: new Set(),
|
||||||
|
@ -124,8 +124,8 @@ describe("move element", () => {
|
|||||||
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
||||||
expect([rectA.x, rectA.y]).toEqual([0, 0]);
|
expect([rectA.x, rectA.y]).toEqual([0, 0]);
|
||||||
expect([rectB.x, rectB.y]).toEqual([201, 2]);
|
expect([rectB.x, rectB.y]).toEqual([201, 2]);
|
||||||
expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[107.07, 47.07]]);
|
expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[110, 50]]);
|
||||||
expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([[86.86, 87.3]]);
|
expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([[81, 81.4]]);
|
||||||
|
|
||||||
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
||||||
});
|
});
|
||||||
|
@ -35,7 +35,7 @@ test("unselected bound arrow updates when rotating its target element", async ()
|
|||||||
expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
|
expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
|
||||||
expect(arrow.x).toBeCloseTo(-80);
|
expect(arrow.x).toBeCloseTo(-80);
|
||||||
expect(arrow.y).toBeCloseTo(50);
|
expect(arrow.y).toBeCloseTo(50);
|
||||||
expect(arrow.width).toBeCloseTo(116.7, 1);
|
expect(arrow.width).toBeCloseTo(110.7, 1);
|
||||||
expect(arrow.height).toBeCloseTo(0);
|
expect(arrow.height).toBeCloseTo(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -682,7 +682,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect(diamond.height).toBe(70);
|
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({
|
const rectangle = API.createElement({
|
||||||
type: "rectangle",
|
type: "rectangle",
|
||||||
x: 10,
|
x: 10,
|
||||||
@ -1500,9 +1500,7 @@ describe("textWysiwyg", () => {
|
|||||||
locked: false,
|
locked: false,
|
||||||
opacity: 100,
|
opacity: 100,
|
||||||
roughness: 1,
|
roughness: 1,
|
||||||
roundness: {
|
roundness: null,
|
||||||
type: 3,
|
|
||||||
},
|
|
||||||
strokeColor: "#1e1e1e",
|
strokeColor: "#1e1e1e",
|
||||||
strokeStyle: "solid",
|
strokeStyle: "solid",
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import type { Bounds } from "@excalidraw/element";
|
import { doBoundsIntersect, 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, vectorNormal, vectorNormalize, vectorScale } from "./vector";
|
||||||
import { vector } from "./vector";
|
|
||||||
|
|
||||||
import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types";
|
import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types";
|
||||||
|
|
||||||
@ -105,16 +104,15 @@ export function curveIntersectLineSegment<
|
|||||||
Point extends GlobalPoint | LocalPoint,
|
Point extends GlobalPoint | LocalPoint,
|
||||||
>(c: Curve<Point>, l: LineSegment<Point>): Point[] {
|
>(c: Curve<Point>, l: LineSegment<Point>): Point[] {
|
||||||
// Optimize by doing a cheap bounding box check first
|
// Optimize by doing a cheap bounding box check first
|
||||||
const bounds = curveBounds(c);
|
const b1 = curveBounds(c);
|
||||||
if (
|
const b2 = [
|
||||||
rectangleIntersectLineSegment(
|
Math.min(l[0][0], l[1][0]),
|
||||||
rectangle(
|
Math.min(l[0][1], l[1][1]),
|
||||||
pointFrom(bounds[0], bounds[1]),
|
Math.max(l[0][0], l[1][0]),
|
||||||
pointFrom(bounds[2], bounds[3]),
|
Math.max(l[0][1], l[1][1]),
|
||||||
),
|
] as Bounds;
|
||||||
l,
|
|
||||||
).length === 0
|
if (!doBoundsIntersect(b1, b2)) {
|
||||||
) {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -303,3 +301,108 @@ function curveBounds<Point extends GlobalPoint | LocalPoint>(
|
|||||||
const y = [P0[1], P1[1], P2[1], P3[1]];
|
const y = [P0[1], P1[1], P2[1], P3[1]];
|
||||||
return [Math.min(...x), Math.min(...y), Math.max(...x), Math.max(...y)];
|
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 "./angle";
|
||||||
export * from "./curve";
|
export * from "./curve";
|
||||||
|
export * from "./ellipse";
|
||||||
export * from "./line";
|
export * from "./line";
|
||||||
export * from "./point";
|
export * from "./point";
|
||||||
export * from "./polygon";
|
export * from "./polygon";
|
||||||
|
@ -21,13 +21,23 @@ export function vector(
|
|||||||
*
|
*
|
||||||
* @param p The point to turn into a vector
|
* @param p The point to turn into a vector
|
||||||
* @param origin The origin point in a given coordiante system
|
* @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>(
|
export function vectorFromPoint<Point extends GlobalPoint | LocalPoint>(
|
||||||
p: Point,
|
p: Point,
|
||||||
origin: Point = [0, 0] as Point,
|
origin: Point = [0, 0] as Point,
|
||||||
|
threshold?: number,
|
||||||
|
defaultValue: Vector = [0, 1] as Vector,
|
||||||
): 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));
|
|
||||||
};
|
|
@ -24,7 +24,7 @@ exports[`exportToSvg > with default arguments 1`] = `
|
|||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
"currentItemRoundness": "round",
|
"currentItemRoundness": "sharp",
|
||||||
"currentItemStartArrowhead": null,
|
"currentItemStartArrowhead": null,
|
||||||
"currentItemStrokeColor": "#1e1e1e",
|
"currentItemStrokeColor": "#1e1e1e",
|
||||||
"currentItemStrokeStyle": "solid",
|
"currentItemStrokeStyle": "solid",
|
||||||
|
@ -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