Compare commits
10 Commits
master
...
arrow-boun
Author | SHA1 | Date | |
---|---|---|---|
![]() |
46c6916e1d | ||
![]() |
bec662eaf6 | ||
![]() |
068edfc1cd | ||
![]() |
4672bb948c | ||
![]() |
49fbad32ac | ||
![]() |
24858c9ace | ||
![]() |
fdbf316cc1 | ||
![]() |
eec3f67d72 | ||
![]() |
0cbf244bd8 | ||
![]() |
795511ee6b |
@ -233,7 +233,7 @@ import {
|
|||||||
findShapeByKey,
|
findShapeByKey,
|
||||||
getBoundTextShape,
|
getBoundTextShape,
|
||||||
getCornerRadius,
|
getCornerRadius,
|
||||||
getElementShape,
|
getElementShapes,
|
||||||
isPathALoop,
|
isPathALoop,
|
||||||
} from "../shapes";
|
} from "../shapes";
|
||||||
import { getSelectionBoxShape } from "../../utils/geometry/shape";
|
import { getSelectionBoxShape } from "../../utils/geometry/shape";
|
||||||
@ -5009,7 +5009,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
element: elementWithHighestZIndex,
|
element: elementWithHighestZIndex,
|
||||||
shape: getElementShape(
|
shapes: getElementShapes(
|
||||||
elementWithHighestZIndex,
|
elementWithHighestZIndex,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
),
|
),
|
||||||
@ -5121,7 +5121,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
element,
|
element,
|
||||||
shape: getElementShape(element, this.scene.getNonDeletedElementsMap()),
|
shapes: getElementShapes(element, this.scene.getNonDeletedElementsMap()),
|
||||||
threshold: this.getElementHitThreshold(),
|
threshold: this.getElementHitThreshold(),
|
||||||
frameNameBound: isFrameLikeElement(element)
|
frameNameBound: isFrameLikeElement(element)
|
||||||
? this.frameNameBoundsCache.get(element)
|
? this.frameNameBoundsCache.get(element)
|
||||||
@ -5153,7 +5153,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
element: elements[index],
|
element: elements[index],
|
||||||
shape: getElementShape(
|
shapes: getElementShapes(
|
||||||
elements[index],
|
elements[index],
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
),
|
),
|
||||||
@ -5437,7 +5437,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
x: sceneX,
|
x: sceneX,
|
||||||
y: sceneY,
|
y: sceneY,
|
||||||
element: container,
|
element: container,
|
||||||
shape: getElementShape(
|
shapes: getElementShapes(
|
||||||
container,
|
container,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
),
|
),
|
||||||
@ -6211,7 +6211,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
x: scenePointerX,
|
x: scenePointerX,
|
||||||
y: scenePointerY,
|
y: scenePointerY,
|
||||||
element,
|
element,
|
||||||
shape: getElementShape(
|
shapes: getElementShapes(
|
||||||
element,
|
element,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
),
|
),
|
||||||
@ -9344,7 +9344,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
x: pointerDownState.origin.x,
|
x: pointerDownState.origin.x,
|
||||||
y: pointerDownState.origin.y,
|
y: pointerDownState.origin.y,
|
||||||
element: hitElement,
|
element: hitElement,
|
||||||
shape: getElementShape(
|
shapes: getElementShapes(
|
||||||
hitElement,
|
hitElement,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
),
|
),
|
||||||
|
@ -52,7 +52,7 @@ import { LinearElementEditor } from "./linearElementEditor";
|
|||||||
import { arrayToMap, tupleToCoors } from "../utils";
|
import { arrayToMap, tupleToCoors } from "../utils";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||||
import { aabbForElement, getElementShape, pointInsideBounds } from "../shapes";
|
import { aabbForElement, getElementShapes, pointInsideBounds } from "../shapes";
|
||||||
import {
|
import {
|
||||||
compareHeading,
|
compareHeading,
|
||||||
HEADING_DOWN,
|
HEADING_DOWN,
|
||||||
@ -1406,9 +1406,9 @@ export const bindingBorderTest = (
|
|||||||
): boolean => {
|
): boolean => {
|
||||||
const threshold = maxBindingGap(element, element.width, element.height, zoom);
|
const threshold = maxBindingGap(element, element.width, element.height, zoom);
|
||||||
|
|
||||||
const shape = getElementShape(element, elementsMap);
|
const shapes = getElementShapes(element, elementsMap);
|
||||||
return (
|
return (
|
||||||
isPointOnShape(pointFrom(x, y), shape, threshold) ||
|
shapes.some((shape) => isPointOnShape(pointFrom(x, y), shape, threshold)) ||
|
||||||
(fullShape === true &&
|
(fullShape === true &&
|
||||||
pointInsideBounds(pointFrom(x, y), aabbForElement(element)))
|
pointInsideBounds(pointFrom(x, y), aabbForElement(element)))
|
||||||
);
|
);
|
||||||
|
@ -8,7 +8,6 @@ import type {
|
|||||||
ElementsMap,
|
ElementsMap,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
|
||||||
import type { Drawable, Op } from "roughjs/bin/core";
|
import type { Drawable, Op } from "roughjs/bin/core";
|
||||||
import type { AppState } from "../types";
|
import type { AppState } from "../types";
|
||||||
import { generateRoughOptions } from "../scene/Shape";
|
import { generateRoughOptions } from "../scene/Shape";
|
||||||
@ -24,13 +23,7 @@ import { getBoundTextElement, getContainerElement } from "./textElement";
|
|||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
import { arrayToMap, invariant } from "../utils";
|
import { arrayToMap, invariant } from "../utils";
|
||||||
import type {
|
import type { Degrees, GlobalPoint, LineSegment, Radians } from "../../math";
|
||||||
Degrees,
|
|
||||||
GlobalPoint,
|
|
||||||
LineSegment,
|
|
||||||
LocalPoint,
|
|
||||||
Radians,
|
|
||||||
} from "../../math";
|
|
||||||
import {
|
import {
|
||||||
degreesToRadians,
|
degreesToRadians,
|
||||||
lineSegment,
|
lineSegment,
|
||||||
@ -39,7 +32,6 @@ import {
|
|||||||
pointFromArray,
|
pointFromArray,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
} from "../../math";
|
} from "../../math";
|
||||||
import type { Mutable } from "../utility-types";
|
|
||||||
|
|
||||||
export type RectangleBox = {
|
export type RectangleBox = {
|
||||||
x: number;
|
x: number;
|
||||||
@ -732,36 +724,12 @@ export const getArrowheadPoints = (
|
|||||||
return [x2, y2, x3, y3, x4, y4];
|
return [x2, y2, x3, y3, x4, y4];
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateLinearElementShape = (
|
|
||||||
element: ExcalidrawLinearElement,
|
|
||||||
): Drawable => {
|
|
||||||
const generator = rough.generator();
|
|
||||||
const options = generateRoughOptions(element);
|
|
||||||
|
|
||||||
const method = (() => {
|
|
||||||
if (element.roundness) {
|
|
||||||
return "curve";
|
|
||||||
}
|
|
||||||
if (options.fill) {
|
|
||||||
return "polygon";
|
|
||||||
}
|
|
||||||
return "linearPath";
|
|
||||||
})();
|
|
||||||
|
|
||||||
return generator[method](
|
|
||||||
element.points as Mutable<LocalPoint>[] as RoughPoint[],
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getLinearElementRotatedBounds = (
|
const getLinearElementRotatedBounds = (
|
||||||
element: ExcalidrawLinearElement,
|
element: ExcalidrawLinearElement,
|
||||||
cx: number,
|
cx: number,
|
||||||
cy: number,
|
cy: number,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
): Bounds => {
|
): Bounds => {
|
||||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
|
||||||
|
|
||||||
if (element.points.length < 2) {
|
if (element.points.length < 2) {
|
||||||
const [pointX, pointY] = element.points[0];
|
const [pointX, pointY] = element.points[0];
|
||||||
const [x, y] = pointRotateRads(
|
const [x, y] = pointRotateRads(
|
||||||
@ -771,6 +739,7 @@ const getLinearElementRotatedBounds = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
let coords: Bounds = [x, y, x, y];
|
let coords: Bounds = [x, y, x, y];
|
||||||
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
|
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
|
||||||
element,
|
element,
|
||||||
@ -788,18 +757,48 @@ const getLinearElementRotatedBounds = (
|
|||||||
return coords;
|
return coords;
|
||||||
}
|
}
|
||||||
|
|
||||||
// first element is always the curve
|
const cachedShape =
|
||||||
const cachedShape = ShapeCache.get(element)?.[0];
|
ShapeCache.get(element) ?? ShapeCache.generateElementShape(element, null);
|
||||||
const shape = cachedShape ?? generateLinearElementShape(element);
|
|
||||||
const ops = getCurvePathOps(shape);
|
const [arrowCurve, ...arrowhead] = cachedShape;
|
||||||
|
|
||||||
const transformXY = ([x, y]: GlobalPoint) =>
|
const transformXY = ([x, y]: GlobalPoint) =>
|
||||||
pointRotateRads<GlobalPoint>(
|
pointRotateRads<GlobalPoint>(
|
||||||
pointFrom(element.x + x, element.y + y),
|
pointFrom(element.x + x, element.y + y),
|
||||||
pointFrom(cx, cy),
|
pointFrom(cx, cy),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
const res = getMinMaxXYFromCurvePathOps(ops, transformXY);
|
|
||||||
let coords: Bounds = [res[0], res[1], res[2], res[3]];
|
let coords = getMinMaxXYFromCurvePathOps(
|
||||||
|
getCurvePathOps(arrowCurve),
|
||||||
|
transformXY,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const shape of arrowhead) {
|
||||||
|
let [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(
|
||||||
|
getCurvePathOps(shape),
|
||||||
|
);
|
||||||
|
|
||||||
|
[minX, minY] = pointRotateRads<GlobalPoint>(
|
||||||
|
pointFrom(minX + element.x, minY + element.y),
|
||||||
|
pointFrom(cx, cy),
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
[maxX, maxY] = pointRotateRads<GlobalPoint>(
|
||||||
|
pointFrom(maxX + element.x, maxY + element.y),
|
||||||
|
pointFrom(cx, cy),
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
|
||||||
|
coords = [
|
||||||
|
Math.min(minX, coords[0]),
|
||||||
|
Math.min(minY, coords[1]),
|
||||||
|
Math.max(maxX, coords[2]),
|
||||||
|
Math.max(maxY, coords[3]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
|
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
|
||||||
element,
|
element,
|
||||||
@ -814,6 +813,7 @@ const getLinearElementRotatedBounds = (
|
|||||||
coordsWithBoundText[3],
|
coordsWithBoundText[3],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return coords;
|
return coords;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ export type HitTestArgs<Point extends GlobalPoint | LocalPoint> = {
|
|||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
element: ExcalidrawElement;
|
element: ExcalidrawElement;
|
||||||
shape: GeometricShape<Point>;
|
shapes: GeometricShape<Point>[];
|
||||||
threshold?: number;
|
threshold?: number;
|
||||||
frameNameBound?: FrameNameBounds | null;
|
frameNameBound?: FrameNameBounds | null;
|
||||||
};
|
};
|
||||||
@ -54,16 +54,18 @@ export const hitElementItself = <Point extends GlobalPoint | LocalPoint>({
|
|||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
element,
|
element,
|
||||||
shape,
|
shapes,
|
||||||
threshold = 10,
|
threshold = 10,
|
||||||
frameNameBound = null,
|
frameNameBound = null,
|
||||||
}: HitTestArgs<Point>) => {
|
}: HitTestArgs<Point>) => {
|
||||||
let hit = shouldTestInside(element)
|
const testInside = shouldTestInside(element);
|
||||||
? // Since `inShape` tests STRICTLY againt the insides of a shape
|
|
||||||
// we would need `onShape` as well to include the "borders"
|
let hit = shapes.some((shape) =>
|
||||||
isPointInShape(pointFrom(x, y), shape) ||
|
testInside || shape.isClosed
|
||||||
isPointOnShape(pointFrom(x, y), shape, threshold)
|
? isPointInShape(pointFrom(x, y), shape) ||
|
||||||
: isPointOnShape(pointFrom(x, y), shape, threshold);
|
isPointOnShape(pointFrom(x, y), shape, threshold)
|
||||||
|
: isPointOnShape(pointFrom(x, y), shape, threshold),
|
||||||
|
);
|
||||||
|
|
||||||
// hit test against a frame's name
|
// hit test against a frame's name
|
||||||
if (!hit && frameNameBound) {
|
if (!hit && frameNameBound) {
|
||||||
|
@ -1720,10 +1720,10 @@ export class LinearElementEditor {
|
|||||||
includeBoundText: boolean = false,
|
includeBoundText: boolean = false,
|
||||||
): [number, number, number, number, number, number] => {
|
): [number, number, number, number, number, number] => {
|
||||||
let coords: [number, number, number, number, number, number];
|
let coords: [number, number, number, number, number, number];
|
||||||
let x1;
|
let x1 = Infinity;
|
||||||
let y1;
|
let y1 = Infinity;
|
||||||
let x2;
|
let x2 = -Infinity;
|
||||||
let y2;
|
let y2 = -Infinity;
|
||||||
if (element.points.length < 2 || !ShapeCache.get(element)) {
|
if (element.points.length < 2 || !ShapeCache.get(element)) {
|
||||||
// XXX this is just a poor estimate and not very useful
|
// XXX this is just a poor estimate and not very useful
|
||||||
const { minX, minY, maxX, maxY } = element.points.reduce(
|
const { minX, minY, maxX, maxY } = element.points.reduce(
|
||||||
@ -1745,14 +1745,15 @@ export class LinearElementEditor {
|
|||||||
} else {
|
} else {
|
||||||
const shape = ShapeCache.generateElementShape(element, null);
|
const shape = ShapeCache.generateElementShape(element, null);
|
||||||
|
|
||||||
// first element is always the curve
|
for (const s of shape) {
|
||||||
const ops = getCurvePathOps(shape[0]);
|
const ops = getCurvePathOps(s);
|
||||||
|
|
||||||
const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops);
|
const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops);
|
||||||
x1 = minX + element.x;
|
x1 = Math.min(minX + element.x, x1);
|
||||||
y1 = minY + element.y;
|
y1 = Math.min(minY + element.y, y1);
|
||||||
x2 = maxX + element.x;
|
x2 = Math.max(maxX + element.x, x2);
|
||||||
y2 = maxY + element.y;
|
y2 = Math.max(maxY + element.y, y2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const cx = (x1 + x2) / 2;
|
const cx = (x1 + x2) / 2;
|
||||||
const cy = (y1 + y2) / 2;
|
const cy = (y1 + y2) / 2;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { pointsOnBezierCurves } from "points-on-curve";
|
||||||
import {
|
import {
|
||||||
isPoint,
|
isPoint,
|
||||||
pointFrom,
|
pointFrom,
|
||||||
@ -7,6 +8,7 @@ import {
|
|||||||
pointsEqual,
|
pointsEqual,
|
||||||
type GlobalPoint,
|
type GlobalPoint,
|
||||||
type LocalPoint,
|
type LocalPoint,
|
||||||
|
polygonFromPoints,
|
||||||
} from "../math";
|
} from "../math";
|
||||||
import {
|
import {
|
||||||
getClosedCurveShape,
|
getClosedCurveShape,
|
||||||
@ -14,6 +16,7 @@ import {
|
|||||||
getCurveShape,
|
getCurveShape,
|
||||||
getEllipseShape,
|
getEllipseShape,
|
||||||
getFreedrawShape,
|
getFreedrawShape,
|
||||||
|
getPointsOnRoughCurve,
|
||||||
getPolygonShape,
|
getPolygonShape,
|
||||||
type GeometricShape,
|
type GeometricShape,
|
||||||
} from "../utils/geometry/shape";
|
} from "../utils/geometry/shape";
|
||||||
@ -141,10 +144,10 @@ export const findShapeByKey = (key: string) => {
|
|||||||
* get the pure geometric shape of an excalidraw element
|
* get the pure geometric shape of an excalidraw element
|
||||||
* which is then used for hit detection
|
* which is then used for hit detection
|
||||||
*/
|
*/
|
||||||
export const getElementShape = <Point extends GlobalPoint | LocalPoint>(
|
export const getElementShapes = <Point extends GlobalPoint | LocalPoint>(
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
): GeometricShape<Point> => {
|
): GeometricShape<Point>[] => {
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "diamond":
|
case "diamond":
|
||||||
@ -155,40 +158,102 @@ export const getElementShape = <Point extends GlobalPoint | LocalPoint>(
|
|||||||
case "iframe":
|
case "iframe":
|
||||||
case "text":
|
case "text":
|
||||||
case "selection":
|
case "selection":
|
||||||
return getPolygonShape(element);
|
return [getPolygonShape(element)];
|
||||||
case "arrow":
|
case "arrow":
|
||||||
case "line": {
|
case "line": {
|
||||||
const roughShape =
|
const [curve, ...arrowheads] =
|
||||||
ShapeCache.get(element)?.[0] ??
|
ShapeCache.get(element) ??
|
||||||
ShapeCache.generateElementShape(element, null)[0];
|
ShapeCache.generateElementShape(element, null);
|
||||||
const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap);
|
const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap);
|
||||||
|
const center = pointFrom<Point>(cx, cy);
|
||||||
|
const startingPoint = pointFrom<Point>(element.x, element.y);
|
||||||
|
|
||||||
return shouldTestInside(element)
|
if (shouldTestInside(element)) {
|
||||||
? getClosedCurveShape<Point>(
|
return [
|
||||||
|
getClosedCurveShape<Point>(
|
||||||
element,
|
element,
|
||||||
roughShape,
|
curve,
|
||||||
pointFrom<Point>(element.x, element.y),
|
startingPoint,
|
||||||
element.angle,
|
element.angle,
|
||||||
pointFrom(cx, cy),
|
center,
|
||||||
)
|
),
|
||||||
: getCurveShape<Point>(
|
];
|
||||||
roughShape,
|
}
|
||||||
pointFrom<Point>(element.x, element.y),
|
|
||||||
element.angle,
|
// otherwise return the curve shape (and also the shape of its arrowheads)
|
||||||
pointFrom(cx, cy),
|
const arrowheadShapes: GeometricShape<Point>[] = [];
|
||||||
|
const transform = (p: Point): Point =>
|
||||||
|
pointRotateRads(
|
||||||
|
pointFrom(p[0] + startingPoint[0], p[1] + startingPoint[1]),
|
||||||
|
center,
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const arrowhead of arrowheads) {
|
||||||
|
if (arrowhead.shape === "polygon") {
|
||||||
|
const ops = arrowhead.sets[0].ops;
|
||||||
|
|
||||||
|
const otherPoints = ops.slice(1);
|
||||||
|
const arrowheadShape: GeometricShape<Point> = {
|
||||||
|
type: "polygon",
|
||||||
|
data: polygonFromPoints(
|
||||||
|
otherPoints.map((otherPoint) =>
|
||||||
|
transform(
|
||||||
|
pointFrom<Point>(otherPoint.data[0], otherPoint.data[1]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isClosed: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
arrowheadShapes.push(arrowheadShape);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arrowhead.shape === "circle") {
|
||||||
|
const polygonPoints = pointsOnBezierCurves(
|
||||||
|
getPointsOnRoughCurve(arrowhead),
|
||||||
|
15,
|
||||||
|
2,
|
||||||
|
).map((p) => transform(p as Point)) as Point[];
|
||||||
|
|
||||||
|
arrowheadShapes.push({
|
||||||
|
type: "polygon",
|
||||||
|
data: polygonFromPoints(polygonPoints),
|
||||||
|
isClosed: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arrowhead.shape === "line") {
|
||||||
|
arrowheadShapes.push(
|
||||||
|
getCurveShape<Point>(
|
||||||
|
arrowhead,
|
||||||
|
element.angle,
|
||||||
|
center,
|
||||||
|
startingPoint,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
getCurveShape<Point>(
|
||||||
|
curve,
|
||||||
|
element.angle,
|
||||||
|
pointFrom(cx, cy),
|
||||||
|
startingPoint,
|
||||||
|
),
|
||||||
|
...arrowheadShapes,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
case "ellipse":
|
case "ellipse":
|
||||||
return getEllipseShape(element);
|
return [getEllipseShape(element)];
|
||||||
|
|
||||||
case "freedraw": {
|
case "freedraw": {
|
||||||
const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap);
|
const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap);
|
||||||
return getFreedrawShape(
|
return [
|
||||||
element,
|
getFreedrawShape(element, pointFrom(cx, cy), shouldTestInside(element)),
|
||||||
pointFrom(cx, cy),
|
];
|
||||||
shouldTestInside(element),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -201,21 +266,23 @@ export const getBoundTextShape = <Point extends GlobalPoint | LocalPoint>(
|
|||||||
|
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
if (element.type === "arrow") {
|
if (element.type === "arrow") {
|
||||||
return getElementShape(
|
return (
|
||||||
{
|
getElementShapes<Point>(
|
||||||
...boundTextElement,
|
{
|
||||||
// arrow's bound text accurate position is not stored in the element's property
|
...boundTextElement,
|
||||||
// but rather calculated and returned from the following static method
|
// arrow's bound text accurate position is not stored in the element's property
|
||||||
...LinearElementEditor.getBoundTextElementPosition(
|
// but rather calculated and returned from the following static method
|
||||||
element,
|
...LinearElementEditor.getBoundTextElementPosition(
|
||||||
boundTextElement,
|
element,
|
||||||
elementsMap,
|
boundTextElement,
|
||||||
),
|
elementsMap,
|
||||||
},
|
),
|
||||||
elementsMap,
|
},
|
||||||
|
elementsMap,
|
||||||
|
)[0] ?? null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return getElementShape(boundTextElement, elementsMap);
|
return getElementShapes<Point>(boundTextElement, elementsMap)[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -418,19 +418,12 @@ describe("element binding", () => {
|
|||||||
expect(arrow.startBinding?.elementId).toBe(rectLeft.id);
|
expect(arrow.startBinding?.elementId).toBe(rectLeft.id);
|
||||||
expect(arrow.endBinding?.elementId).toBe(rectRight.id);
|
expect(arrow.endBinding?.elementId).toBe(rectRight.id);
|
||||||
|
|
||||||
// Drag arrow off of bound rectangle range
|
|
||||||
const handles = getTransformHandles(
|
|
||||||
arrow,
|
|
||||||
h.state.zoom,
|
|
||||||
arrayToMap(h.elements),
|
|
||||||
"mouse",
|
|
||||||
).se!;
|
|
||||||
|
|
||||||
Keyboard.keyDown(KEYS.CTRL_OR_CMD);
|
Keyboard.keyDown(KEYS.CTRL_OR_CMD);
|
||||||
const elX = handles[0] + handles[2] / 2;
|
mouse.downAt(
|
||||||
const elY = handles[1] + handles[3] / 2;
|
arrow.x + arrow.points[arrow.points.length - 1][0],
|
||||||
mouse.downAt(elX, elY);
|
arrow.y + arrow.points[arrow.points.length - 1][1],
|
||||||
mouse.moveTo(300, 400);
|
);
|
||||||
|
mouse.moveTo(300, 300);
|
||||||
mouse.up();
|
mouse.up();
|
||||||
|
|
||||||
expect(arrow.startBinding).not.toBe(null);
|
expect(arrow.startBinding).not.toBe(null);
|
||||||
|
@ -198,8 +198,8 @@ const checkElementsBoundingBox = async (
|
|||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
// Check if width and height did not change
|
// Check if width and height did not change
|
||||||
expect(x2 - x1).toBeCloseTo(x22 - x12, -1);
|
expect(Math.abs(x2 - x1 - (x22 - x12))).toBeLessThanOrEqual(toleranceInPx);
|
||||||
expect(y2 - y1).toBeCloseTo(y22 - y12, -1);
|
expect(Math.abs(y2 - y1 - (y22 - y12))).toBeLessThanOrEqual(toleranceInPx);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -535,7 +535,7 @@ describe("arrow element", () => {
|
|||||||
|
|
||||||
UI.resize([rectangle, arrow], "nw", [300, 350]);
|
UI.resize([rectangle, arrow], "nw", [300, 350]);
|
||||||
|
|
||||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(-0.144, 2);
|
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(-0.168, 2);
|
||||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.25);
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.25);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -73,7 +73,7 @@ export type Ellipse<Point extends GlobalPoint | LocalPoint> = {
|
|||||||
halfHeight: number;
|
halfHeight: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GeometricShape<Point extends GlobalPoint | LocalPoint> =
|
export type GeometricShape<Point extends GlobalPoint | LocalPoint> = (
|
||||||
| {
|
| {
|
||||||
type: "line";
|
type: "line";
|
||||||
data: LineSegment<Point>;
|
data: LineSegment<Point>;
|
||||||
@ -97,7 +97,10 @@ export type GeometricShape<Point extends GlobalPoint | LocalPoint> =
|
|||||||
| {
|
| {
|
||||||
type: "polycurve";
|
type: "polycurve";
|
||||||
data: Polycurve<Point>;
|
data: Polycurve<Point>;
|
||||||
};
|
}
|
||||||
|
) & {
|
||||||
|
isClosed?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type RectangularElement =
|
type RectangularElement =
|
||||||
| ExcalidrawRectangleElement
|
| ExcalidrawRectangleElement
|
||||||
@ -203,9 +206,9 @@ export const getCurvePathOps = (shape: Drawable): Op[] => {
|
|||||||
// linear
|
// linear
|
||||||
export const getCurveShape = <Point extends GlobalPoint | LocalPoint>(
|
export const getCurveShape = <Point extends GlobalPoint | LocalPoint>(
|
||||||
roughShape: Drawable,
|
roughShape: Drawable,
|
||||||
startingPoint: Point = pointFrom(0, 0),
|
|
||||||
angleInRadian: Radians,
|
angleInRadian: Radians,
|
||||||
center: Point,
|
center: Point,
|
||||||
|
startingPoint: Point = pointFrom(0, 0),
|
||||||
): GeometricShape<Point> => {
|
): GeometricShape<Point> => {
|
||||||
const transform = (p: Point): Point =>
|
const transform = (p: Point): Point =>
|
||||||
pointRotateRads(
|
pointRotateRads(
|
||||||
@ -285,6 +288,35 @@ export const getFreedrawShape = <Point extends GlobalPoint | LocalPoint>(
|
|||||||
) as GeometricShape<Point>;
|
) as GeometricShape<Point>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getPointsOnRoughCurve = <Point extends GlobalPoint | LocalPoint>(
|
||||||
|
roughCurve: Drawable,
|
||||||
|
) => {
|
||||||
|
const ops = getCurvePathOps(roughCurve);
|
||||||
|
|
||||||
|
const points: Point[] = [];
|
||||||
|
let odd = false;
|
||||||
|
for (const operation of ops) {
|
||||||
|
if (operation.op === "move") {
|
||||||
|
odd = !odd;
|
||||||
|
if (odd) {
|
||||||
|
points.push(pointFrom(operation.data[0], operation.data[1]));
|
||||||
|
}
|
||||||
|
} else if (operation.op === "bcurveTo") {
|
||||||
|
if (odd) {
|
||||||
|
points.push(pointFrom(operation.data[0], operation.data[1]));
|
||||||
|
points.push(pointFrom(operation.data[2], operation.data[3]));
|
||||||
|
points.push(pointFrom(operation.data[4], operation.data[5]));
|
||||||
|
}
|
||||||
|
} else if (operation.op === "lineTo") {
|
||||||
|
if (odd) {
|
||||||
|
points.push(pointFrom(operation.data[0], operation.data[1]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return points;
|
||||||
|
};
|
||||||
|
|
||||||
export const getClosedCurveShape = <Point extends GlobalPoint | LocalPoint>(
|
export const getClosedCurveShape = <Point extends GlobalPoint | LocalPoint>(
|
||||||
element: ExcalidrawLinearElement,
|
element: ExcalidrawLinearElement,
|
||||||
roughShape: Drawable,
|
roughShape: Drawable,
|
||||||
@ -308,31 +340,10 @@ export const getClosedCurveShape = <Point extends GlobalPoint | LocalPoint>(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const ops = getCurvePathOps(roughShape);
|
const polygonPoints = pointsOnBezierCurves(
|
||||||
|
getPointsOnRoughCurve(roughShape),
|
||||||
const points: Point[] = [];
|
10,
|
||||||
let odd = false;
|
5,
|
||||||
for (const operation of ops) {
|
|
||||||
if (operation.op === "move") {
|
|
||||||
odd = !odd;
|
|
||||||
if (odd) {
|
|
||||||
points.push(pointFrom(operation.data[0], operation.data[1]));
|
|
||||||
}
|
|
||||||
} else if (operation.op === "bcurveTo") {
|
|
||||||
if (odd) {
|
|
||||||
points.push(pointFrom(operation.data[0], operation.data[1]));
|
|
||||||
points.push(pointFrom(operation.data[2], operation.data[3]));
|
|
||||||
points.push(pointFrom(operation.data[4], operation.data[5]));
|
|
||||||
}
|
|
||||||
} else if (operation.op === "lineTo") {
|
|
||||||
if (odd) {
|
|
||||||
points.push(pointFrom(operation.data[0], operation.data[1]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const polygonPoints = pointsOnBezierCurves(points, 10, 5).map((p) =>
|
|
||||||
transform(p as Point),
|
|
||||||
) as Point[];
|
) as Point[];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user