merge with master
This commit is contained in:
@@ -39,6 +39,7 @@ import {
|
||||
isBindingElement,
|
||||
isBoundToContainer,
|
||||
isElbowArrow,
|
||||
isFixedPointBinding,
|
||||
isFrameLikeElement,
|
||||
isLinearElement,
|
||||
isRectangularElement,
|
||||
@@ -65,7 +66,7 @@ import {
|
||||
import type { LocalPoint, Radians } from "../../math";
|
||||
import {
|
||||
lineSegment,
|
||||
point,
|
||||
pointFrom,
|
||||
pointRotateRads,
|
||||
type GlobalPoint,
|
||||
vectorFromPoint,
|
||||
@@ -719,7 +720,7 @@ export const getHeadingForElbowArrowSnap = (
|
||||
return vectorToHeading(
|
||||
vectorFromPoint(
|
||||
p,
|
||||
point<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(
|
||||
bindableElement.x + bindableElement.width / 2,
|
||||
bindableElement.y + bindableElement.height / 2,
|
||||
),
|
||||
@@ -765,15 +766,15 @@ export const bindPointToSnapToElementOutline = (
|
||||
const intersections = [
|
||||
...(intersectElementWithLine(
|
||||
bindableElement,
|
||||
point(p[0], p[1] - 2 * bindableElement.height),
|
||||
point(p[0], p[1] + 2 * bindableElement.height),
|
||||
pointFrom(p[0], p[1] - 2 * bindableElement.height),
|
||||
pointFrom(p[0], p[1] + 2 * bindableElement.height),
|
||||
FIXED_BINDING_DISTANCE,
|
||||
elementsMap,
|
||||
) ?? []),
|
||||
...(intersectElementWithLine(
|
||||
bindableElement,
|
||||
point(p[0] - 2 * bindableElement.width, p[1]),
|
||||
point(p[0] + 2 * bindableElement.width, p[1]),
|
||||
pointFrom(p[0] - 2 * bindableElement.width, p[1]),
|
||||
pointFrom(p[0] + 2 * bindableElement.width, p[1]),
|
||||
FIXED_BINDING_DISTANCE,
|
||||
elementsMap,
|
||||
) ?? []),
|
||||
@@ -797,7 +798,7 @@ export const bindPointToSnapToElementOutline = (
|
||||
isVertical
|
||||
? Math.abs(p[1] - i[1]) < 0.1
|
||||
: Math.abs(p[0] - i[0]) < 0.1,
|
||||
)[0] ?? point;
|
||||
)[0] ?? p;
|
||||
}
|
||||
|
||||
return p;
|
||||
@@ -814,25 +815,25 @@ const headingToMidBindPoint = (
|
||||
switch (true) {
|
||||
case compareHeading(heading, HEADING_UP):
|
||||
return pointRotateRads(
|
||||
point((aabb[0] + aabb[2]) / 2 + 0.1, aabb[1]),
|
||||
pointFrom((aabb[0] + aabb[2]) / 2 + 0.1, aabb[1]),
|
||||
center,
|
||||
bindableElement.angle,
|
||||
);
|
||||
case compareHeading(heading, HEADING_RIGHT):
|
||||
return pointRotateRads(
|
||||
point(aabb[2], (aabb[1] + aabb[3]) / 2 + 0.1),
|
||||
pointFrom(aabb[2], (aabb[1] + aabb[3]) / 2 + 0.1),
|
||||
center,
|
||||
bindableElement.angle,
|
||||
);
|
||||
case compareHeading(heading, HEADING_DOWN):
|
||||
return pointRotateRads(
|
||||
point((aabb[0] + aabb[2]) / 2 - 0.1, aabb[3]),
|
||||
pointFrom((aabb[0] + aabb[2]) / 2 - 0.1, aabb[3]),
|
||||
center,
|
||||
bindableElement.angle,
|
||||
);
|
||||
default:
|
||||
return pointRotateRads(
|
||||
point(aabb[0], (aabb[1] + aabb[3]) / 2 - 0.1),
|
||||
pointFrom(aabb[0], (aabb[1] + aabb[3]) / 2 - 0.1),
|
||||
center,
|
||||
bindableElement.angle,
|
||||
);
|
||||
@@ -843,7 +844,7 @@ export const avoidRectangularCorner = (
|
||||
element: ExcalidrawBindableElement,
|
||||
p: GlobalPoint,
|
||||
): GlobalPoint => {
|
||||
const center = point<GlobalPoint>(
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
@@ -853,13 +854,13 @@ export const avoidRectangularCorner = (
|
||||
// Top left
|
||||
if (nonRotatedPoint[1] - element.y > -FIXED_BINDING_DISTANCE) {
|
||||
return pointRotateRads<GlobalPoint>(
|
||||
point(element.x - FIXED_BINDING_DISTANCE, element.y),
|
||||
pointFrom(element.x - FIXED_BINDING_DISTANCE, element.y),
|
||||
center,
|
||||
element.angle,
|
||||
);
|
||||
}
|
||||
return pointRotateRads(
|
||||
point(element.x, element.y - FIXED_BINDING_DISTANCE),
|
||||
pointFrom(element.x, element.y - FIXED_BINDING_DISTANCE),
|
||||
center,
|
||||
element.angle,
|
||||
);
|
||||
@@ -870,13 +871,16 @@ export const avoidRectangularCorner = (
|
||||
// Bottom left
|
||||
if (nonRotatedPoint[0] - element.x > -FIXED_BINDING_DISTANCE) {
|
||||
return pointRotateRads(
|
||||
point(element.x, element.y + element.height + FIXED_BINDING_DISTANCE),
|
||||
pointFrom(
|
||||
element.x,
|
||||
element.y + element.height + FIXED_BINDING_DISTANCE,
|
||||
),
|
||||
center,
|
||||
element.angle,
|
||||
);
|
||||
}
|
||||
return pointRotateRads(
|
||||
point(element.x - FIXED_BINDING_DISTANCE, element.y + element.height),
|
||||
pointFrom(element.x - FIXED_BINDING_DISTANCE, element.y + element.height),
|
||||
center,
|
||||
element.angle,
|
||||
);
|
||||
@@ -890,7 +894,7 @@ export const avoidRectangularCorner = (
|
||||
element.width + FIXED_BINDING_DISTANCE
|
||||
) {
|
||||
return pointRotateRads(
|
||||
point(
|
||||
pointFrom(
|
||||
element.x + element.width,
|
||||
element.y + element.height + FIXED_BINDING_DISTANCE,
|
||||
),
|
||||
@@ -899,7 +903,7 @@ export const avoidRectangularCorner = (
|
||||
);
|
||||
}
|
||||
return pointRotateRads(
|
||||
point(
|
||||
pointFrom(
|
||||
element.x + element.width + FIXED_BINDING_DISTANCE,
|
||||
element.y + element.height,
|
||||
),
|
||||
@@ -916,13 +920,16 @@ export const avoidRectangularCorner = (
|
||||
element.width + FIXED_BINDING_DISTANCE
|
||||
) {
|
||||
return pointRotateRads(
|
||||
point(element.x + element.width, element.y - FIXED_BINDING_DISTANCE),
|
||||
pointFrom(
|
||||
element.x + element.width,
|
||||
element.y - FIXED_BINDING_DISTANCE,
|
||||
),
|
||||
center,
|
||||
element.angle,
|
||||
);
|
||||
}
|
||||
return pointRotateRads(
|
||||
point(element.x + element.width + FIXED_BINDING_DISTANCE, element.y),
|
||||
pointFrom(element.x + element.width + FIXED_BINDING_DISTANCE, element.y),
|
||||
center,
|
||||
element.angle,
|
||||
);
|
||||
@@ -937,7 +944,10 @@ export const snapToMid = (
|
||||
tolerance: number = 0.05,
|
||||
): GlobalPoint => {
|
||||
const { x, y, width, height, angle } = element;
|
||||
const center = point<GlobalPoint>(x + width / 2 - 0.1, y + height / 2 - 0.1);
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
x + width / 2 - 0.1,
|
||||
y + height / 2 - 0.1,
|
||||
);
|
||||
const nonRotated = pointRotateRads(p, center, -angle as Radians);
|
||||
|
||||
// snap-to-center point is adaptive to element size, but we don't want to go
|
||||
@@ -952,7 +962,7 @@ export const snapToMid = (
|
||||
) {
|
||||
// LEFT
|
||||
return pointRotateRads(
|
||||
point(x - FIXED_BINDING_DISTANCE, center[1]),
|
||||
pointFrom(x - FIXED_BINDING_DISTANCE, center[1]),
|
||||
center,
|
||||
angle,
|
||||
);
|
||||
@@ -963,7 +973,7 @@ export const snapToMid = (
|
||||
) {
|
||||
// TOP
|
||||
return pointRotateRads(
|
||||
point(center[0], y - FIXED_BINDING_DISTANCE),
|
||||
pointFrom(center[0], y - FIXED_BINDING_DISTANCE),
|
||||
center,
|
||||
angle,
|
||||
);
|
||||
@@ -974,7 +984,7 @@ export const snapToMid = (
|
||||
) {
|
||||
// RIGHT
|
||||
return pointRotateRads(
|
||||
point(x + width + FIXED_BINDING_DISTANCE, center[1]),
|
||||
pointFrom(x + width + FIXED_BINDING_DISTANCE, center[1]),
|
||||
center,
|
||||
angle,
|
||||
);
|
||||
@@ -985,7 +995,7 @@ export const snapToMid = (
|
||||
) {
|
||||
// DOWN
|
||||
return pointRotateRads(
|
||||
point(center[0], y + height + FIXED_BINDING_DISTANCE),
|
||||
pointFrom(center[0], y + height + FIXED_BINDING_DISTANCE),
|
||||
center,
|
||||
angle,
|
||||
);
|
||||
@@ -1013,7 +1023,7 @@ const updateBoundPoint = (
|
||||
const direction = startOrEnd === "startBinding" ? -1 : 1;
|
||||
const edgePointIndex = direction === -1 ? 0 : linearElement.points.length - 1;
|
||||
|
||||
if (isElbowArrow(linearElement)) {
|
||||
if (isElbowArrow(linearElement) && isFixedPointBinding(binding)) {
|
||||
const fixedPoint =
|
||||
normalizeFixedPoint(binding.fixedPoint) ??
|
||||
calculateFixedPointForElbowArrowBinding(
|
||||
@@ -1022,11 +1032,11 @@ const updateBoundPoint = (
|
||||
startOrEnd === "startBinding" ? "start" : "end",
|
||||
elementsMap,
|
||||
).fixedPoint;
|
||||
const globalMidPoint = point<GlobalPoint>(
|
||||
const globalMidPoint = pointFrom<GlobalPoint>(
|
||||
bindableElement.x + bindableElement.width / 2,
|
||||
bindableElement.y + bindableElement.height / 2,
|
||||
);
|
||||
const global = point<GlobalPoint>(
|
||||
const global = pointFrom<GlobalPoint>(
|
||||
bindableElement.x + fixedPoint[0] * bindableElement.width,
|
||||
bindableElement.y + fixedPoint[1] * bindableElement.height,
|
||||
);
|
||||
@@ -1117,7 +1127,7 @@ export const calculateFixedPointForElbowArrowBinding = (
|
||||
hoveredElement,
|
||||
elementsMap,
|
||||
);
|
||||
const globalMidPoint = point(
|
||||
const globalMidPoint = pointFrom(
|
||||
bounds[0] + (bounds[2] - bounds[0]) / 2,
|
||||
bounds[1] + (bounds[3] - bounds[1]) / 2,
|
||||
);
|
||||
@@ -1336,9 +1346,9 @@ export const bindingBorderTest = (
|
||||
const threshold = maxBindingGap(element, element.width, element.height);
|
||||
const shape = getElementShape(element, elementsMap);
|
||||
return (
|
||||
isPointOnShape(point(x, y), shape, threshold) ||
|
||||
isPointOnShape(pointFrom(x, y), shape, threshold) ||
|
||||
(fullShape === true &&
|
||||
pointInsideBounds(point(x, y), aabbForElement(element)))
|
||||
pointInsideBounds(pointFrom(x, y), aabbForElement(element)))
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2196,11 +2206,11 @@ export const getGlobalFixedPointForBindableElement = (
|
||||
const [fixedX, fixedY] = normalizeFixedPoint(fixedPointRatio);
|
||||
|
||||
return pointRotateRads(
|
||||
point(
|
||||
pointFrom(
|
||||
element.x + element.width * fixedX,
|
||||
element.y + element.height * fixedY,
|
||||
),
|
||||
point<GlobalPoint>(
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
),
|
||||
@@ -2228,7 +2238,7 @@ const getGlobalFixedPoints = (
|
||||
arrow.startBinding.fixedPoint,
|
||||
startElement as ExcalidrawBindableElement,
|
||||
)
|
||||
: point<GlobalPoint>(
|
||||
: pointFrom<GlobalPoint>(
|
||||
arrow.x + arrow.points[0][0],
|
||||
arrow.y + arrow.points[0][1],
|
||||
);
|
||||
@@ -2238,7 +2248,7 @@ const getGlobalFixedPoints = (
|
||||
arrow.endBinding.fixedPoint,
|
||||
endElement as ExcalidrawBindableElement,
|
||||
)
|
||||
: point<GlobalPoint>(
|
||||
: pointFrom<GlobalPoint>(
|
||||
arrow.x + arrow.points[arrow.points.length - 1][0],
|
||||
arrow.y + arrow.points[arrow.points.length - 1][1],
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { LocalPoint } from "../../math";
|
||||
import { point } from "../../math";
|
||||
import { pointFrom } from "../../math";
|
||||
import { ROUNDNESS } from "../constants";
|
||||
import { arrayToMap } from "../utils";
|
||||
import { getElementAbsoluteCoords, getElementBounds } from "./bounds";
|
||||
@@ -125,9 +125,9 @@ describe("getElementBounds", () => {
|
||||
a: 0.6447741904932416,
|
||||
}),
|
||||
points: [
|
||||
point<LocalPoint>(0, 0),
|
||||
point<LocalPoint>(67.33984375, 92.48828125),
|
||||
point<LocalPoint>(-102.7890625, 52.15625),
|
||||
pointFrom<LocalPoint>(0, 0),
|
||||
pointFrom<LocalPoint>(67.33984375, 92.48828125),
|
||||
pointFrom<LocalPoint>(-102.7890625, 52.15625),
|
||||
],
|
||||
} as ExcalidrawLinearElement;
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ import type {
|
||||
import {
|
||||
degreesToRadians,
|
||||
lineSegment,
|
||||
point,
|
||||
pointFrom,
|
||||
pointDistance,
|
||||
pointFromArray,
|
||||
pointRotateRads,
|
||||
@@ -113,8 +113,8 @@ export class ElementBounds {
|
||||
const [minX, minY, maxX, maxY] = getBoundsFromPoints(
|
||||
element.points.map(([x, y]) =>
|
||||
pointRotateRads(
|
||||
point(x, y),
|
||||
point(cx - element.x, cy - element.y),
|
||||
pointFrom(x, y),
|
||||
pointFrom(cx - element.x, cy - element.y),
|
||||
element.angle,
|
||||
),
|
||||
),
|
||||
@@ -130,23 +130,23 @@ export class ElementBounds {
|
||||
bounds = getLinearElementRotatedBounds(element, cx, cy, elementsMap);
|
||||
} else if (element.type === "diamond") {
|
||||
const [x11, y11] = pointRotateRads(
|
||||
point(cx, y1),
|
||||
point(cx, cy),
|
||||
pointFrom(cx, y1),
|
||||
pointFrom(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
const [x12, y12] = pointRotateRads(
|
||||
point(cx, y2),
|
||||
point(cx, cy),
|
||||
pointFrom(cx, y2),
|
||||
pointFrom(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
const [x22, y22] = pointRotateRads(
|
||||
point(x1, cy),
|
||||
point(cx, cy),
|
||||
pointFrom(x1, cy),
|
||||
pointFrom(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
const [x21, y21] = pointRotateRads(
|
||||
point(x2, cy),
|
||||
point(cx, cy),
|
||||
pointFrom(x2, cy),
|
||||
pointFrom(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
const minX = Math.min(x11, x12, x22, x21);
|
||||
@@ -164,23 +164,23 @@ export class ElementBounds {
|
||||
bounds = [cx - ww, cy - hh, cx + ww, cy + hh];
|
||||
} else {
|
||||
const [x11, y11] = pointRotateRads(
|
||||
point(x1, y1),
|
||||
point(cx, cy),
|
||||
pointFrom(x1, y1),
|
||||
pointFrom(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
const [x12, y12] = pointRotateRads(
|
||||
point(x1, y2),
|
||||
point(cx, cy),
|
||||
pointFrom(x1, y2),
|
||||
pointFrom(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
const [x22, y22] = pointRotateRads(
|
||||
point(x2, y2),
|
||||
point(cx, cy),
|
||||
pointFrom(x2, y2),
|
||||
pointFrom(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
const [x21, y21] = pointRotateRads(
|
||||
point(x2, y1),
|
||||
point(cx, cy),
|
||||
pointFrom(x2, y1),
|
||||
pointFrom(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
const minX = Math.min(x11, x12, x22, x21);
|
||||
@@ -255,7 +255,7 @@ export const getElementLineSegments = (
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
const center: GlobalPoint = point(cx, cy);
|
||||
const center: GlobalPoint = pointFrom(cx, cy);
|
||||
|
||||
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
||||
const segments: LineSegment<GlobalPoint>[] = [];
|
||||
@@ -266,7 +266,7 @@ export const getElementLineSegments = (
|
||||
segments.push(
|
||||
lineSegment(
|
||||
pointRotateRads(
|
||||
point(
|
||||
pointFrom(
|
||||
element.points[i][0] + element.x,
|
||||
element.points[i][1] + element.y,
|
||||
),
|
||||
@@ -274,7 +274,7 @@ export const getElementLineSegments = (
|
||||
element.angle,
|
||||
),
|
||||
pointRotateRads(
|
||||
point(
|
||||
pointFrom(
|
||||
element.points[i + 1][0] + element.x,
|
||||
element.points[i + 1][1] + element.y,
|
||||
),
|
||||
@@ -470,7 +470,7 @@ export const getMinMaxXYFromCurvePathOps = (
|
||||
ops: Op[],
|
||||
transformXY?: (p: GlobalPoint) => GlobalPoint,
|
||||
): Bounds => {
|
||||
let currentP: GlobalPoint = point(0, 0);
|
||||
let currentP: GlobalPoint = pointFrom(0, 0);
|
||||
|
||||
const { minX, minY, maxX, maxY } = ops.reduce(
|
||||
(limits, { op, data }) => {
|
||||
@@ -484,9 +484,9 @@ export const getMinMaxXYFromCurvePathOps = (
|
||||
// move operation does not draw anything; so, it always
|
||||
// returns false
|
||||
} else if (op === "bcurveTo") {
|
||||
const _p1 = point<GlobalPoint>(data[0], data[1]);
|
||||
const _p2 = point<GlobalPoint>(data[2], data[3]);
|
||||
const _p3 = point<GlobalPoint>(data[4], data[5]);
|
||||
const _p1 = pointFrom<GlobalPoint>(data[0], data[1]);
|
||||
const _p2 = pointFrom<GlobalPoint>(data[2], data[3]);
|
||||
const _p3 = pointFrom<GlobalPoint>(data[4], data[5]);
|
||||
|
||||
const p1 = transformXY ? transformXY(_p1) : _p1;
|
||||
const p2 = transformXY ? transformXY(_p2) : _p2;
|
||||
@@ -591,21 +591,21 @@ export const getArrowheadPoints = (
|
||||
|
||||
invariant(data.length === 6, "Op data length is not 6");
|
||||
|
||||
const p3 = point(data[4], data[5]);
|
||||
const p2 = point(data[2], data[3]);
|
||||
const p1 = point(data[0], data[1]);
|
||||
const p3 = pointFrom(data[4], data[5]);
|
||||
const p2 = pointFrom(data[2], data[3]);
|
||||
const p1 = pointFrom(data[0], data[1]);
|
||||
|
||||
// We need to find p0 of the bezier curve.
|
||||
// It is typically the last point of the previous
|
||||
// curve; it can also be the position of moveTo operation.
|
||||
const prevOp = ops[index - 1];
|
||||
let p0 = point(0, 0);
|
||||
let p0 = pointFrom(0, 0);
|
||||
if (prevOp.op === "move") {
|
||||
const p = pointFromArray(prevOp.data);
|
||||
invariant(p != null, "Op data is not a point");
|
||||
p0 = p;
|
||||
} else if (prevOp.op === "bcurveTo") {
|
||||
p0 = point(prevOp.data[4], prevOp.data[5]);
|
||||
p0 = pointFrom(prevOp.data[4], prevOp.data[5]);
|
||||
}
|
||||
|
||||
// B(t) = p0 * (1-t)^3 + 3p1 * t * (1-t)^2 + 3p2 * t^2 * (1-t) + p3 * t^3
|
||||
@@ -671,13 +671,13 @@ export const getArrowheadPoints = (
|
||||
|
||||
// Return points
|
||||
const [x3, y3] = pointRotateRads(
|
||||
point(xs, ys),
|
||||
point(x2, y2),
|
||||
pointFrom(xs, ys),
|
||||
pointFrom(x2, y2),
|
||||
((-angle * Math.PI) / 180) as Radians,
|
||||
);
|
||||
const [x4, y4] = pointRotateRads(
|
||||
point(xs, ys),
|
||||
point(x2, y2),
|
||||
pointFrom(xs, ys),
|
||||
pointFrom(x2, y2),
|
||||
degreesToRadians(angle),
|
||||
);
|
||||
|
||||
@@ -690,8 +690,8 @@ export const getArrowheadPoints = (
|
||||
const [px, py] = element.points.length > 1 ? element.points[1] : [0, 0];
|
||||
|
||||
[ox, oy] = pointRotateRads(
|
||||
point(x2 + minSize * 2, y2),
|
||||
point(x2, y2),
|
||||
pointFrom(x2 + minSize * 2, y2),
|
||||
pointFrom(x2, y2),
|
||||
Math.atan2(py - y2, px - x2) as Radians,
|
||||
);
|
||||
} else {
|
||||
@@ -701,8 +701,8 @@ export const getArrowheadPoints = (
|
||||
: [0, 0];
|
||||
|
||||
[ox, oy] = pointRotateRads(
|
||||
point(x2 - minSize * 2, y2),
|
||||
point(x2, y2),
|
||||
pointFrom(x2 - minSize * 2, y2),
|
||||
pointFrom(x2, y2),
|
||||
Math.atan2(y2 - py, x2 - px) as Radians,
|
||||
);
|
||||
}
|
||||
@@ -746,8 +746,8 @@ const getLinearElementRotatedBounds = (
|
||||
if (element.points.length < 2) {
|
||||
const [pointX, pointY] = element.points[0];
|
||||
const [x, y] = pointRotateRads(
|
||||
point(element.x + pointX, element.y + pointY),
|
||||
point(cx, cy),
|
||||
pointFrom(element.x + pointX, element.y + pointY),
|
||||
pointFrom(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
|
||||
@@ -775,8 +775,8 @@ const getLinearElementRotatedBounds = (
|
||||
const ops = getCurvePathOps(shape);
|
||||
const transformXY = ([x, y]: GlobalPoint) =>
|
||||
pointRotateRads<GlobalPoint>(
|
||||
point(element.x + x, element.y + y),
|
||||
point(cx, cy),
|
||||
pointFrom(element.x + x, element.y + y),
|
||||
pointFrom(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
const res = getMinMaxXYFromCurvePathOps(ops, transformXY);
|
||||
@@ -931,8 +931,8 @@ export const getClosestElementBounds = (
|
||||
elements.forEach((element) => {
|
||||
const [x1, y1, x2, y2] = getElementBounds(element, elementsMap);
|
||||
const distance = pointDistance(
|
||||
point((x1 + x2) / 2, (y1 + y2) / 2),
|
||||
point(from.x, from.y),
|
||||
pointFrom((x1 + x2) / 2, (y1 + y2) / 2),
|
||||
pointFrom(from.x, from.y),
|
||||
);
|
||||
|
||||
if (distance < minDistance) {
|
||||
@@ -990,7 +990,7 @@ export const getVisibleSceneBounds = ({
|
||||
};
|
||||
|
||||
export const getCenterForBounds = (bounds: Bounds): GlobalPoint =>
|
||||
point(
|
||||
pointFrom(
|
||||
bounds[0] + (bounds[2] - bounds[0]) / 2,
|
||||
bounds[1] + (bounds[3] - bounds[1]) / 2,
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from "./typeChecks";
|
||||
import { getBoundTextShape, isPathALoop } from "../shapes";
|
||||
import type { GlobalPoint, LocalPoint, Polygon } from "../../math";
|
||||
import { isPointWithinBounds, point } from "../../math";
|
||||
import { isPointWithinBounds, pointFrom } from "../../math";
|
||||
|
||||
export const shouldTestInside = (element: ExcalidrawElement) => {
|
||||
if (element.type === "arrow") {
|
||||
@@ -61,13 +61,13 @@ export const hitElementItself = <Point extends GlobalPoint | LocalPoint>({
|
||||
let hit = shouldTestInside(element)
|
||||
? // Since `inShape` tests STRICTLY againt the insides of a shape
|
||||
// we would need `onShape` as well to include the "borders"
|
||||
isPointInShape(point(x, y), shape) ||
|
||||
isPointOnShape(point(x, y), shape, threshold)
|
||||
: isPointOnShape(point(x, y), shape, threshold);
|
||||
isPointInShape(pointFrom(x, y), shape) ||
|
||||
isPointOnShape(pointFrom(x, y), shape, threshold)
|
||||
: isPointOnShape(pointFrom(x, y), shape, threshold);
|
||||
|
||||
// hit test against a frame's name
|
||||
if (!hit && frameNameBound) {
|
||||
hit = isPointInShape(point(x, y), {
|
||||
hit = isPointInShape(pointFrom(x, y), {
|
||||
type: "polygon",
|
||||
data: getPolygonShape(frameNameBound as ExcalidrawRectangleElement)
|
||||
.data as Polygon<Point>,
|
||||
@@ -89,7 +89,11 @@ export const hitElementBoundingBox = (
|
||||
y1 -= tolerance;
|
||||
x2 += tolerance;
|
||||
y2 += tolerance;
|
||||
return isPointWithinBounds(point(x1, y1), point(x, y), point(x2, y2));
|
||||
return isPointWithinBounds(
|
||||
pointFrom(x1, y1),
|
||||
pointFrom(x, y),
|
||||
pointFrom(x2, y2),
|
||||
);
|
||||
};
|
||||
|
||||
export const hitElementBoundingBoxOnly = <
|
||||
@@ -115,5 +119,5 @@ export const hitElementBoundText = <Point extends GlobalPoint | LocalPoint>(
|
||||
y: number,
|
||||
textShape: GeometricShape<Point> | null,
|
||||
): boolean => {
|
||||
return !!textShape && isPointInShape(point(x, y), textShape);
|
||||
return !!textShape && isPointInShape(pointFrom(x, y), textShape);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type Point } from "points-on-curve";
|
||||
import {
|
||||
type Radians,
|
||||
point,
|
||||
pointFrom,
|
||||
pointCenter,
|
||||
pointRotateRads,
|
||||
vectorFromPoint,
|
||||
@@ -64,8 +64,8 @@ const _cropElement = (
|
||||
*/
|
||||
|
||||
const rotatedPointer = pointRotateRads(
|
||||
point(pointerX, pointerY),
|
||||
point(element.x + element.width / 2, element.y + element.height / 2),
|
||||
pointFrom(pointerX, pointerY),
|
||||
pointFrom(element.x + element.width / 2, element.y + element.height / 2),
|
||||
-element.angle as Radians,
|
||||
);
|
||||
|
||||
@@ -199,8 +199,8 @@ const recomputeOrigin = (
|
||||
stateAtCropStart.height,
|
||||
true,
|
||||
);
|
||||
const startTopLeft = point(x1, y1);
|
||||
const startBottomRight = point(x2, y2);
|
||||
const startTopLeft = pointFrom(x1, y1);
|
||||
const startBottomRight = pointFrom(x2, y2);
|
||||
const startCenter: any = pointCenter(startTopLeft, startBottomRight);
|
||||
|
||||
const [newBoundsX1, newBoundsY1, newBoundsX2, newBoundsY2] =
|
||||
@@ -267,16 +267,16 @@ export const getUncroppedImageElement = (
|
||||
);
|
||||
|
||||
const topLeftVector = vectorFromPoint(
|
||||
pointRotateRads(point(x1, y1), point(cx, cy), element.angle),
|
||||
pointRotateRads(pointFrom(x1, y1), pointFrom(cx, cy), element.angle),
|
||||
);
|
||||
const topRightVector = vectorFromPoint(
|
||||
pointRotateRads(point(x2, y1), point(cx, cy), element.angle),
|
||||
pointRotateRads(pointFrom(x2, y1), pointFrom(cx, cy), element.angle),
|
||||
);
|
||||
const topEdgeNormalized = vectorNormalize(
|
||||
vectorSubtract(topRightVector, topLeftVector),
|
||||
);
|
||||
const bottomLeftVector = vectorFromPoint(
|
||||
pointRotateRads(point(x1, y2), point(cx, cy), element.angle),
|
||||
pointRotateRads(pointFrom(x1, y2), pointFrom(cx, cy), element.angle),
|
||||
);
|
||||
const leftEdgeVector = vectorSubtract(bottomLeftVector, topLeftVector);
|
||||
const leftEdgeNormalized = vectorNormalize(leftEdgeVector);
|
||||
|
||||
@@ -36,7 +36,6 @@ export const dragSelectedElements = (
|
||||
) => {
|
||||
if (
|
||||
_selectedElements.length === 1 &&
|
||||
isArrowElement(_selectedElements[0]) &&
|
||||
isElbowArrow(_selectedElements[0]) &&
|
||||
(_selectedElements[0].startBinding || _selectedElements[0].endBinding)
|
||||
) {
|
||||
@@ -44,13 +43,7 @@ export const dragSelectedElements = (
|
||||
}
|
||||
|
||||
const selectedElements = _selectedElements.filter(
|
||||
(el) =>
|
||||
!(
|
||||
isArrowElement(el) &&
|
||||
isElbowArrow(el) &&
|
||||
el.startBinding &&
|
||||
el.endBinding
|
||||
),
|
||||
(el) => !(isElbowArrow(el) && el.startBinding && el.endBinding),
|
||||
);
|
||||
|
||||
// we do not want a frame and its elements to be selected at the same time
|
||||
|
||||
@@ -45,6 +45,12 @@ const RE_GENERIC_EMBED =
|
||||
const RE_GIPHY =
|
||||
/giphy.com\/(?:clips|embed|gifs)\/[a-zA-Z0-9]*?-?([a-zA-Z0-9]+)(?:[^a-zA-Z0-9]|$)/;
|
||||
|
||||
const RE_REDDIT =
|
||||
/^(?:http(?:s)?:\/\/)?(?:www\.)?reddit\.com\/r\/([a-zA-Z0-9_]+)\/comments\/([a-zA-Z0-9_]+)\/([a-zA-Z0-9_]+)\/?(?:\?[^#\s]*)?(?:#[^\s]*)?$/;
|
||||
|
||||
const RE_REDDIT_EMBED =
|
||||
/^<blockquote[\s\S]*?\shref=["'](https?:\/\/(?:www\.)?reddit\.com\/[^"']*)/i;
|
||||
|
||||
const ALLOWED_DOMAINS = new Set([
|
||||
"youtube.com",
|
||||
"youtu.be",
|
||||
@@ -59,6 +65,7 @@ const ALLOWED_DOMAINS = new Set([
|
||||
"stackblitz.com",
|
||||
"val.town",
|
||||
"giphy.com",
|
||||
"reddit.com",
|
||||
]);
|
||||
|
||||
const ALLOW_SAME_ORIGIN = new Set([
|
||||
@@ -71,6 +78,7 @@ const ALLOW_SAME_ORIGIN = new Set([
|
||||
"x.com",
|
||||
"*.simplepdf.eu",
|
||||
"stackblitz.com",
|
||||
"reddit.com",
|
||||
]);
|
||||
|
||||
export const createSrcDoc = (body: string) => {
|
||||
@@ -218,6 +226,24 @@ export const getEmbedLink = (
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (RE_REDDIT.test(link)) {
|
||||
const [, page, postId, title] = link.match(RE_REDDIT)!;
|
||||
const safeURL = sanitizeHTMLAttribute(
|
||||
`https://reddit.com/r/${page}/comments/${postId}/${title}`,
|
||||
);
|
||||
const ret: IframeDataWithSandbox = {
|
||||
type: "document",
|
||||
srcdoc: (theme: string) =>
|
||||
createSrcDoc(
|
||||
`<blockquote class="reddit-embed-bq" data-embed-theme="${theme}"><a href="${safeURL}"></a><br></blockquote><script async="" src="https://embed.reddit.com/widgets.js" charset="UTF-8"></script>`,
|
||||
),
|
||||
intrinsicSize: { w: 480, h: 480 },
|
||||
sandbox: { allowSameOrigin },
|
||||
};
|
||||
embeddedLinkCache.set(originalLink, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (RE_GH_GIST.test(link)) {
|
||||
const [, user, gistId] = link.match(RE_GH_GIST)!;
|
||||
const safeURL = sanitizeHTMLAttribute(
|
||||
@@ -361,6 +387,11 @@ export const maybeParseEmbedSrc = (str: string): string => {
|
||||
return twitterMatch[1];
|
||||
}
|
||||
|
||||
const redditMatch = str.match(RE_REDDIT_EMBED);
|
||||
if (redditMatch && redditMatch.length === 2) {
|
||||
return redditMatch[1];
|
||||
}
|
||||
|
||||
const gistMatch = str.match(RE_GH_GIST_EMBED);
|
||||
if (gistMatch && gistMatch.length === 2) {
|
||||
return gistMatch[1];
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
isFlowchartNodeElement,
|
||||
} from "./typeChecks";
|
||||
import { invariant } from "../utils";
|
||||
import { point, type LocalPoint } from "../../math";
|
||||
import { pointFrom, type LocalPoint } from "../../math";
|
||||
import { aabbForElement } from "../shapes";
|
||||
|
||||
type LinkDirection = "up" | "right" | "down" | "left";
|
||||
@@ -421,7 +421,7 @@ const createBindingArrow = (
|
||||
strokeColor: appState.currentItemStrokeColor,
|
||||
strokeStyle: appState.currentItemStrokeStyle,
|
||||
strokeWidth: appState.currentItemStrokeWidth,
|
||||
points: [point(0, 0), point(endX, endY)],
|
||||
points: [pointFrom(0, 0), pointFrom(endX, endY)],
|
||||
elbowed: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
Radians,
|
||||
} from "../../math";
|
||||
import {
|
||||
point,
|
||||
pointFrom,
|
||||
pointRotateRads,
|
||||
pointScaleFromOrigin,
|
||||
radiansToDegrees,
|
||||
@@ -82,7 +82,7 @@ export const headingForPointFromElement = <
|
||||
|
||||
const top = pointRotateRads(
|
||||
pointScaleFromOrigin(
|
||||
point(element.x + element.width / 2, element.y),
|
||||
pointFrom(element.x + element.width / 2, element.y),
|
||||
midPoint,
|
||||
SEARCH_CONE_MULTIPLIER,
|
||||
),
|
||||
@@ -91,7 +91,7 @@ export const headingForPointFromElement = <
|
||||
);
|
||||
const right = pointRotateRads(
|
||||
pointScaleFromOrigin(
|
||||
point(element.x + element.width, element.y + element.height / 2),
|
||||
pointFrom(element.x + element.width, element.y + element.height / 2),
|
||||
midPoint,
|
||||
SEARCH_CONE_MULTIPLIER,
|
||||
),
|
||||
@@ -100,7 +100,7 @@ export const headingForPointFromElement = <
|
||||
);
|
||||
const bottom = pointRotateRads(
|
||||
pointScaleFromOrigin(
|
||||
point(element.x + element.width / 2, element.y + element.height),
|
||||
pointFrom(element.x + element.width / 2, element.y + element.height),
|
||||
midPoint,
|
||||
SEARCH_CONE_MULTIPLIER,
|
||||
),
|
||||
@@ -109,7 +109,7 @@ export const headingForPointFromElement = <
|
||||
);
|
||||
const left = pointRotateRads(
|
||||
pointScaleFromOrigin(
|
||||
point(element.x, element.y + element.height / 2),
|
||||
pointFrom(element.x, element.y + element.height / 2),
|
||||
midPoint,
|
||||
SEARCH_CONE_MULTIPLIER,
|
||||
),
|
||||
@@ -133,22 +133,22 @@ export const headingForPointFromElement = <
|
||||
}
|
||||
|
||||
const topLeft = pointScaleFromOrigin(
|
||||
point(aabb[0], aabb[1]),
|
||||
pointFrom(aabb[0], aabb[1]),
|
||||
midPoint,
|
||||
SEARCH_CONE_MULTIPLIER,
|
||||
) as Point;
|
||||
const topRight = pointScaleFromOrigin(
|
||||
point(aabb[2], aabb[1]),
|
||||
pointFrom(aabb[2], aabb[1]),
|
||||
midPoint,
|
||||
SEARCH_CONE_MULTIPLIER,
|
||||
) as Point;
|
||||
const bottomLeft = pointScaleFromOrigin(
|
||||
point(aabb[0], aabb[3]),
|
||||
pointFrom(aabb[0], aabb[3]),
|
||||
midPoint,
|
||||
SEARCH_CONE_MULTIPLIER,
|
||||
) as Point;
|
||||
const bottomRight = pointScaleFromOrigin(
|
||||
point(aabb[2], aabb[3]),
|
||||
pointFrom(aabb[2], aabb[3]),
|
||||
midPoint,
|
||||
SEARCH_CONE_MULTIPLIER,
|
||||
) as Point;
|
||||
|
||||
@@ -49,7 +49,7 @@ import type Scene from "../scene/Scene";
|
||||
import type { Radians } from "../../math";
|
||||
import {
|
||||
pointCenter,
|
||||
point,
|
||||
pointFrom,
|
||||
pointRotateRads,
|
||||
pointsEqual,
|
||||
vector,
|
||||
@@ -102,12 +102,13 @@ export class LinearElementEditor {
|
||||
public readonly endBindingElement: ExcalidrawBindableElement | null | "keep";
|
||||
public readonly hoverPointIndex: number;
|
||||
public readonly segmentMidPointHoveredCoords: GlobalPoint | null;
|
||||
public readonly elbowed: boolean;
|
||||
|
||||
constructor(element: NonDeleted<ExcalidrawLinearElement>) {
|
||||
this.elementId = element.id as string & {
|
||||
_brand: "excalidrawLinearElementId";
|
||||
};
|
||||
if (!pointsEqual(element.points[0], point(0, 0))) {
|
||||
if (!pointsEqual(element.points[0], pointFrom(0, 0))) {
|
||||
console.error("Linear element is not normalized", Error().stack);
|
||||
}
|
||||
|
||||
@@ -131,6 +132,7 @@ export class LinearElementEditor {
|
||||
};
|
||||
this.hoverPointIndex = -1;
|
||||
this.segmentMidPointHoveredCoords = null;
|
||||
this.elbowed = isElbowArrow(element) && element.elbowed;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -285,7 +287,7 @@ export class LinearElementEditor {
|
||||
element,
|
||||
elementsMap,
|
||||
referencePoint,
|
||||
point(scenePointerX, scenePointerY),
|
||||
pointFrom(scenePointerX, scenePointerY),
|
||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||
);
|
||||
|
||||
@@ -294,7 +296,7 @@ export class LinearElementEditor {
|
||||
[
|
||||
{
|
||||
index: selectedIndex,
|
||||
point: point(
|
||||
point: pointFrom(
|
||||
width + referencePoint[0],
|
||||
height + referencePoint[1],
|
||||
),
|
||||
@@ -327,7 +329,7 @@ export class LinearElementEditor {
|
||||
scenePointerY - linearElementEditor.pointerOffset.y,
|
||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||
)
|
||||
: point(
|
||||
: pointFrom(
|
||||
element.points[pointIndex][0] + deltaX,
|
||||
element.points[pointIndex][1] + deltaY,
|
||||
);
|
||||
@@ -588,11 +590,11 @@ export class LinearElementEditor {
|
||||
linearElementEditor.segmentMidPointHoveredCoords;
|
||||
if (existingSegmentMidpointHitCoords) {
|
||||
const distance = pointDistance(
|
||||
point(
|
||||
pointFrom(
|
||||
existingSegmentMidpointHitCoords[0],
|
||||
existingSegmentMidpointHitCoords[1],
|
||||
),
|
||||
point(scenePointer.x, scenePointer.y),
|
||||
pointFrom(scenePointer.x, scenePointer.y),
|
||||
);
|
||||
if (distance <= threshold) {
|
||||
return existingSegmentMidpointHitCoords;
|
||||
@@ -604,8 +606,8 @@ export class LinearElementEditor {
|
||||
while (index < midPoints.length) {
|
||||
if (midPoints[index] !== null) {
|
||||
const distance = pointDistance(
|
||||
point(midPoints[index]![0], midPoints[index]![1]),
|
||||
point(scenePointer.x, scenePointer.y),
|
||||
pointFrom(midPoints[index]![0], midPoints[index]![1]),
|
||||
pointFrom(scenePointer.x, scenePointer.y),
|
||||
);
|
||||
if (distance <= threshold) {
|
||||
return midPoints[index];
|
||||
@@ -624,8 +626,8 @@ export class LinearElementEditor {
|
||||
zoom: AppState["zoom"],
|
||||
) {
|
||||
let distance = pointDistance(
|
||||
point(startPoint[0], startPoint[1]),
|
||||
point(endPoint[0], endPoint[1]),
|
||||
pointFrom(startPoint[0], startPoint[1]),
|
||||
pointFrom(endPoint[0], endPoint[1]),
|
||||
);
|
||||
if (element.points.length > 2 && element.roundness) {
|
||||
distance = getBezierCurveLength(element, endPoint);
|
||||
@@ -827,11 +829,11 @@ export class LinearElementEditor {
|
||||
const targetPoint =
|
||||
clickedPointIndex > -1 &&
|
||||
pointRotateRads(
|
||||
point(
|
||||
pointFrom(
|
||||
element.x + element.points[clickedPointIndex][0],
|
||||
element.y + element.points[clickedPointIndex][1],
|
||||
),
|
||||
point(cx, cy),
|
||||
pointFrom(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
|
||||
@@ -926,11 +928,11 @@ export class LinearElementEditor {
|
||||
element,
|
||||
elementsMap,
|
||||
lastCommittedPoint,
|
||||
point(scenePointerX, scenePointerY),
|
||||
pointFrom(scenePointerX, scenePointerY),
|
||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||
);
|
||||
|
||||
newPoint = point(
|
||||
newPoint = pointFrom(
|
||||
width + lastCommittedPoint[0],
|
||||
height + lastCommittedPoint[1],
|
||||
);
|
||||
@@ -982,8 +984,8 @@ export class LinearElementEditor {
|
||||
|
||||
const { x, y } = element;
|
||||
return pointRotateRads(
|
||||
point(x + p[0], y + p[1]),
|
||||
point(cx, cy),
|
||||
pointFrom(x + p[0], y + p[1]),
|
||||
pointFrom(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
}
|
||||
@@ -999,8 +1001,8 @@ export class LinearElementEditor {
|
||||
return element.points.map((p) => {
|
||||
const { x, y } = element;
|
||||
return pointRotateRads(
|
||||
point(x + p[0], y + p[1]),
|
||||
point(cx, cy),
|
||||
pointFrom(x + p[0], y + p[1]),
|
||||
pointFrom(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
});
|
||||
@@ -1023,8 +1025,12 @@ export class LinearElementEditor {
|
||||
const { x, y } = element;
|
||||
|
||||
return p
|
||||
? pointRotateRads(point(x + p[0], y + p[1]), point(cx, cy), element.angle)
|
||||
: pointRotateRads(point(x, y), point(cx, cy), element.angle);
|
||||
? pointRotateRads(
|
||||
pointFrom(x + p[0], y + p[1]),
|
||||
pointFrom(cx, cy),
|
||||
element.angle,
|
||||
)
|
||||
: pointRotateRads(pointFrom(x, y), pointFrom(cx, cy), element.angle);
|
||||
}
|
||||
|
||||
static pointFromAbsoluteCoords(
|
||||
@@ -1034,7 +1040,7 @@ export class LinearElementEditor {
|
||||
): LocalPoint {
|
||||
if (isElbowArrow(element)) {
|
||||
// No rotation for elbow arrows
|
||||
return point(
|
||||
return pointFrom(
|
||||
absoluteCoords[0] - element.x,
|
||||
absoluteCoords[1] - element.y,
|
||||
);
|
||||
@@ -1044,11 +1050,11 @@ export class LinearElementEditor {
|
||||
const cx = (x1 + x2) / 2;
|
||||
const cy = (y1 + y2) / 2;
|
||||
const [x, y] = pointRotateRads(
|
||||
point(absoluteCoords[0], absoluteCoords[1]),
|
||||
point(cx, cy),
|
||||
pointFrom(absoluteCoords[0], absoluteCoords[1]),
|
||||
pointFrom(cx, cy),
|
||||
-element.angle as Radians,
|
||||
);
|
||||
return point(x - element.x, y - element.y);
|
||||
return pointFrom(x - element.x, y - element.y);
|
||||
}
|
||||
|
||||
static getPointIndexUnderCursor(
|
||||
@@ -1069,7 +1075,7 @@ export class LinearElementEditor {
|
||||
while (--idx > -1) {
|
||||
const p = pointHandles[idx];
|
||||
if (
|
||||
pointDistance(point(x, y), point(p[0], p[1])) * zoom.value <
|
||||
pointDistance(pointFrom(x, y), pointFrom(p[0], p[1])) * zoom.value <
|
||||
// +1px to account for outline stroke
|
||||
LinearElementEditor.POINT_HANDLE_SIZE + 1
|
||||
) {
|
||||
@@ -1091,12 +1097,12 @@ export class LinearElementEditor {
|
||||
const cx = (x1 + x2) / 2;
|
||||
const cy = (y1 + y2) / 2;
|
||||
const [rotatedX, rotatedY] = pointRotateRads(
|
||||
point(pointerOnGrid[0], pointerOnGrid[1]),
|
||||
point(cx, cy),
|
||||
pointFrom(pointerOnGrid[0], pointerOnGrid[1]),
|
||||
pointFrom(cx, cy),
|
||||
-element.angle as Radians,
|
||||
);
|
||||
|
||||
return point(rotatedX - element.x, rotatedY - element.y);
|
||||
return pointFrom(rotatedX - element.x, rotatedY - element.y);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1116,7 +1122,7 @@ export class LinearElementEditor {
|
||||
|
||||
return {
|
||||
points: points.map((p) => {
|
||||
return point(p[0] - offsetX, p[1] - offsetY);
|
||||
return pointFrom(p[0] - offsetX, p[1] - offsetY);
|
||||
}),
|
||||
x: element.x + offsetX,
|
||||
y: element.y + offsetY,
|
||||
@@ -1170,8 +1176,8 @@ export class LinearElementEditor {
|
||||
}
|
||||
acc.push(
|
||||
nextPoint
|
||||
? point((p[0] + nextPoint[0]) / 2, (p[1] + nextPoint[1]) / 2)
|
||||
: point(p[0], p[1]),
|
||||
? pointFrom((p[0] + nextPoint[0]) / 2, (p[1] + nextPoint[1]) / 2)
|
||||
: pointFrom(p[0], p[1]),
|
||||
);
|
||||
|
||||
nextSelectedIndices.push(indexCursor + 1);
|
||||
@@ -1192,7 +1198,7 @@ export class LinearElementEditor {
|
||||
[
|
||||
{
|
||||
index: element.points.length - 1,
|
||||
point: point(lastPoint[0] + 30, lastPoint[1] + 30),
|
||||
point: pointFrom(lastPoint[0] + 30, lastPoint[1] + 30),
|
||||
},
|
||||
],
|
||||
elementsMap,
|
||||
@@ -1233,7 +1239,9 @@ export class LinearElementEditor {
|
||||
const nextPoints = element.points.reduce((acc: LocalPoint[], p, idx) => {
|
||||
if (!pointIndices.includes(idx)) {
|
||||
acc.push(
|
||||
!acc.length ? point(0, 0) : point(p[0] - offsetX, p[1] - offsetY),
|
||||
!acc.length
|
||||
? pointFrom(0, 0)
|
||||
: pointFrom(p[0] - offsetX, p[1] - offsetY),
|
||||
);
|
||||
}
|
||||
return acc;
|
||||
@@ -1310,9 +1318,9 @@ export class LinearElementEditor {
|
||||
const deltaY =
|
||||
selectedPointData.point[1] - points[selectedPointData.index][1];
|
||||
|
||||
return point(p[0] + deltaX - offsetX, p[1] + deltaY - offsetY);
|
||||
return pointFrom(p[0] + deltaX - offsetX, p[1] + deltaY - offsetY);
|
||||
}
|
||||
return offsetX || offsetY ? point(p[0] - offsetX, p[1] - offsetY) : p;
|
||||
return offsetX || offsetY ? pointFrom(p[0] - offsetX, p[1] - offsetY) : p;
|
||||
});
|
||||
|
||||
LinearElementEditor._updatePoints(
|
||||
@@ -1366,8 +1374,8 @@ export class LinearElementEditor {
|
||||
|
||||
const origin = linearElementEditor.pointerDownState.origin!;
|
||||
const dist = pointDistance(
|
||||
point(origin.x, origin.y),
|
||||
point(pointerCoords.x, pointerCoords.y),
|
||||
pointFrom(origin.x, origin.y),
|
||||
pointFrom(pointerCoords.x, pointerCoords.y),
|
||||
);
|
||||
if (
|
||||
!appState.editingLinearElement &&
|
||||
@@ -1477,7 +1485,9 @@ export class LinearElementEditor {
|
||||
nextPoints,
|
||||
vector(offsetX, offsetY),
|
||||
bindings,
|
||||
options,
|
||||
{
|
||||
isDragging: options?.isDragging,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
const nextCoords = getElementPointsCoords(element, nextPoints);
|
||||
@@ -1489,8 +1499,8 @@ export class LinearElementEditor {
|
||||
const dX = prevCenterX - nextCenterX;
|
||||
const dY = prevCenterY - nextCenterY;
|
||||
const rotated = pointRotateRads(
|
||||
point(offsetX, offsetY),
|
||||
point(dX, dY),
|
||||
pointFrom(offsetX, offsetY),
|
||||
pointFrom(dX, dY),
|
||||
element.angle,
|
||||
);
|
||||
mutateElement(element, {
|
||||
@@ -1536,8 +1546,8 @@ export class LinearElementEditor {
|
||||
);
|
||||
|
||||
return pointRotateRads(
|
||||
point(width, height),
|
||||
point(0, 0),
|
||||
pointFrom(width, height),
|
||||
pointFrom(0, 0),
|
||||
-element.angle as Radians,
|
||||
);
|
||||
}
|
||||
@@ -1607,36 +1617,36 @@ export class LinearElementEditor {
|
||||
);
|
||||
const boundTextX2 = boundTextX1 + boundTextElement.width;
|
||||
const boundTextY2 = boundTextY1 + boundTextElement.height;
|
||||
const centerPoint = point(cx, cy);
|
||||
const centerPoint = pointFrom(cx, cy);
|
||||
|
||||
const topLeftRotatedPoint = pointRotateRads(
|
||||
point(x1, y1),
|
||||
pointFrom(x1, y1),
|
||||
centerPoint,
|
||||
element.angle,
|
||||
);
|
||||
const topRightRotatedPoint = pointRotateRads(
|
||||
point(x2, y1),
|
||||
pointFrom(x2, y1),
|
||||
centerPoint,
|
||||
element.angle,
|
||||
);
|
||||
|
||||
const counterRotateBoundTextTopLeft = pointRotateRads(
|
||||
point(boundTextX1, boundTextY1),
|
||||
pointFrom(boundTextX1, boundTextY1),
|
||||
centerPoint,
|
||||
-element.angle as Radians,
|
||||
);
|
||||
const counterRotateBoundTextTopRight = pointRotateRads(
|
||||
point(boundTextX2, boundTextY1),
|
||||
pointFrom(boundTextX2, boundTextY1),
|
||||
centerPoint,
|
||||
-element.angle as Radians,
|
||||
);
|
||||
const counterRotateBoundTextBottomLeft = pointRotateRads(
|
||||
point(boundTextX1, boundTextY2),
|
||||
pointFrom(boundTextX1, boundTextY2),
|
||||
centerPoint,
|
||||
-element.angle as Radians,
|
||||
);
|
||||
const counterRotateBoundTextBottomRight = pointRotateRads(
|
||||
point(boundTextX2, boundTextY2),
|
||||
pointFrom(boundTextX2, boundTextY2),
|
||||
centerPoint,
|
||||
-element.angle as Radians,
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { FONT_FAMILY, ROUNDNESS } from "../constants";
|
||||
import { isPrimitive } from "../utils";
|
||||
import type { ExcalidrawLinearElement } from "./types";
|
||||
import type { LocalPoint } from "../../math";
|
||||
import { point } from "../../math";
|
||||
import { pointFrom } from "../../math";
|
||||
|
||||
const assertCloneObjects = (source: any, clone: any) => {
|
||||
for (const key in clone) {
|
||||
@@ -38,7 +38,7 @@ describe("duplicating single elements", () => {
|
||||
element.__proto__ = { hello: "world" };
|
||||
|
||||
mutateElement(element, {
|
||||
points: [point<LocalPoint>(1, 2), point<LocalPoint>(3, 4)],
|
||||
points: [pointFrom<LocalPoint>(1, 2), pointFrom<LocalPoint>(3, 4)],
|
||||
});
|
||||
|
||||
const copy = duplicateElement(null, new Map(), element);
|
||||
|
||||
@@ -223,7 +223,6 @@ export const newTextElement = (
|
||||
verticalAlign?: VerticalAlign;
|
||||
containerId?: ExcalidrawTextContainer["id"] | null;
|
||||
lineHeight?: ExcalidrawTextElement["lineHeight"];
|
||||
strokeWidth?: ExcalidrawTextElement["strokeWidth"];
|
||||
autoResize?: ExcalidrawTextElement["autoResize"];
|
||||
} & ElementConstructorOpts,
|
||||
): NonDeleted<ExcalidrawTextElement> => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
ExcalidrawTextElementWithContainer,
|
||||
ExcalidrawImageElement,
|
||||
ElementsMap,
|
||||
ExcalidrawArrowElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
SceneElementsMap,
|
||||
} from "./types";
|
||||
@@ -57,7 +58,7 @@ import type { GlobalPoint } from "../../math";
|
||||
import {
|
||||
pointCenter,
|
||||
normalizeRadians,
|
||||
point,
|
||||
pointFrom,
|
||||
pointFromPair,
|
||||
pointRotateRads,
|
||||
type Radians,
|
||||
@@ -239,8 +240,8 @@ const resizeSingleTextElement = (
|
||||
);
|
||||
// rotation pointer with reverse angle
|
||||
const [rotatedX, rotatedY] = pointRotateRads(
|
||||
point(pointerX, pointerY),
|
||||
point(cx, cy),
|
||||
pointFrom(pointerX, pointerY),
|
||||
pointFrom(cx, cy),
|
||||
-element.angle as Radians,
|
||||
);
|
||||
let scaleX = 0;
|
||||
@@ -275,23 +276,23 @@ const resizeSingleTextElement = (
|
||||
const startBottomRight = [x2, y2];
|
||||
const startCenter = [cx, cy];
|
||||
|
||||
let newTopLeft = point<GlobalPoint>(x1, y1);
|
||||
let newTopLeft = pointFrom<GlobalPoint>(x1, y1);
|
||||
if (["n", "w", "nw"].includes(transformHandleType)) {
|
||||
newTopLeft = point<GlobalPoint>(
|
||||
newTopLeft = pointFrom<GlobalPoint>(
|
||||
startBottomRight[0] - Math.abs(nextWidth),
|
||||
startBottomRight[1] - Math.abs(nextHeight),
|
||||
);
|
||||
}
|
||||
if (transformHandleType === "ne") {
|
||||
const bottomLeft = [startTopLeft[0], startBottomRight[1]];
|
||||
newTopLeft = point<GlobalPoint>(
|
||||
newTopLeft = pointFrom<GlobalPoint>(
|
||||
bottomLeft[0],
|
||||
bottomLeft[1] - Math.abs(nextHeight),
|
||||
);
|
||||
}
|
||||
if (transformHandleType === "sw") {
|
||||
const topRight = [startBottomRight[0], startTopLeft[1]];
|
||||
newTopLeft = point<GlobalPoint>(
|
||||
newTopLeft = pointFrom<GlobalPoint>(
|
||||
topRight[0] - Math.abs(nextWidth),
|
||||
topRight[1],
|
||||
);
|
||||
@@ -310,12 +311,20 @@ const resizeSingleTextElement = (
|
||||
}
|
||||
|
||||
const angle = element.angle;
|
||||
const rotatedTopLeft = pointRotateRads(newTopLeft, point(cx, cy), angle);
|
||||
const newCenter = point<GlobalPoint>(
|
||||
const rotatedTopLeft = pointRotateRads(
|
||||
newTopLeft,
|
||||
pointFrom(cx, cy),
|
||||
angle,
|
||||
);
|
||||
const newCenter = pointFrom<GlobalPoint>(
|
||||
newTopLeft[0] + Math.abs(nextWidth) / 2,
|
||||
newTopLeft[1] + Math.abs(nextHeight) / 2,
|
||||
);
|
||||
const rotatedNewCenter = pointRotateRads(newCenter, point(cx, cy), angle);
|
||||
const rotatedNewCenter = pointRotateRads(
|
||||
newCenter,
|
||||
pointFrom(cx, cy),
|
||||
angle,
|
||||
);
|
||||
newTopLeft = pointRotateRads(
|
||||
rotatedTopLeft,
|
||||
rotatedNewCenter,
|
||||
@@ -340,12 +349,12 @@ const resizeSingleTextElement = (
|
||||
stateAtResizeStart.height,
|
||||
true,
|
||||
);
|
||||
const startTopLeft = point<GlobalPoint>(x1, y1);
|
||||
const startBottomRight = point<GlobalPoint>(x2, y2);
|
||||
const startTopLeft = pointFrom<GlobalPoint>(x1, y1);
|
||||
const startBottomRight = pointFrom<GlobalPoint>(x2, y2);
|
||||
const startCenter = pointCenter(startTopLeft, startBottomRight);
|
||||
|
||||
const rotatedPointer = pointRotateRads(
|
||||
point(pointerX, pointerY),
|
||||
pointFrom(pointerX, pointerY),
|
||||
startCenter,
|
||||
-stateAtResizeStart.angle as Radians,
|
||||
);
|
||||
@@ -418,7 +427,7 @@ const resizeSingleTextElement = (
|
||||
startCenter,
|
||||
angle,
|
||||
);
|
||||
const newCenter = point(
|
||||
const newCenter = pointFrom(
|
||||
newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
|
||||
newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
|
||||
);
|
||||
@@ -460,13 +469,13 @@ export const resizeSingleElement = (
|
||||
stateAtResizeStart.height,
|
||||
true,
|
||||
);
|
||||
const startTopLeft = point(x1, y1);
|
||||
const startBottomRight = point(x2, y2);
|
||||
const startTopLeft = pointFrom(x1, y1);
|
||||
const startBottomRight = pointFrom(x2, y2);
|
||||
const startCenter = pointCenter(startTopLeft, startBottomRight);
|
||||
|
||||
// Calculate new dimensions based on cursor position
|
||||
const rotatedPointer = pointRotateRads(
|
||||
point(pointerX, pointerY),
|
||||
pointFrom(pointerX, pointerY),
|
||||
startCenter,
|
||||
-stateAtResizeStart.angle as Radians,
|
||||
);
|
||||
@@ -647,7 +656,7 @@ export const resizeSingleElement = (
|
||||
startCenter,
|
||||
angle,
|
||||
);
|
||||
const newCenter = point(
|
||||
const newCenter = pointFrom(
|
||||
newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
|
||||
newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
|
||||
);
|
||||
@@ -816,20 +825,20 @@ export const resizeMultipleElements = (
|
||||
const direction = transformHandleType;
|
||||
|
||||
const anchorsMap: Record<TransformHandleDirection, GlobalPoint> = {
|
||||
ne: point(minX, maxY),
|
||||
se: point(minX, minY),
|
||||
sw: point(maxX, minY),
|
||||
nw: point(maxX, maxY),
|
||||
e: point(minX, minY + height / 2),
|
||||
w: point(maxX, minY + height / 2),
|
||||
n: point(minX + width / 2, maxY),
|
||||
s: point(minX + width / 2, minY),
|
||||
ne: pointFrom(minX, maxY),
|
||||
se: pointFrom(minX, minY),
|
||||
sw: pointFrom(maxX, minY),
|
||||
nw: pointFrom(maxX, maxY),
|
||||
e: pointFrom(minX, minY + height / 2),
|
||||
w: pointFrom(maxX, minY + height / 2),
|
||||
n: pointFrom(minX + width / 2, maxY),
|
||||
s: pointFrom(minX + width / 2, minY),
|
||||
};
|
||||
|
||||
// anchor point must be on the opposite side of the dragged selection handle
|
||||
// or be the center of the selection if shouldResizeFromCenter
|
||||
const [anchorX, anchorY] = shouldResizeFromCenter
|
||||
? point(midX, midY)
|
||||
? pointFrom(midX, midY)
|
||||
: anchorsMap[direction];
|
||||
|
||||
const resizeFromCenterScale = shouldResizeFromCenter ? 2 : 1;
|
||||
@@ -909,6 +918,8 @@ export const resizeMultipleElements = (
|
||||
fontSize?: ExcalidrawTextElement["fontSize"];
|
||||
scale?: ExcalidrawImageElement["scale"];
|
||||
boundTextFontSize?: ExcalidrawTextElement["fontSize"];
|
||||
startBinding?: ExcalidrawArrowElement["startBinding"];
|
||||
endBinding?: ExcalidrawArrowElement["endBinding"];
|
||||
};
|
||||
}[] = [];
|
||||
|
||||
@@ -993,19 +1004,6 @@ export const resizeMultipleElements = (
|
||||
|
||||
mutateElement(element, update, false);
|
||||
|
||||
if (isArrowElement(element) && isElbowArrow(element)) {
|
||||
mutateElbowArrow(
|
||||
element,
|
||||
elementsMap,
|
||||
element.points,
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
informMutation: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
updateBoundElements(element, elementsMap, {
|
||||
simultaneouslyUpdated: elementsToUpdate,
|
||||
oldSize: { width: oldWidth, height: oldHeight },
|
||||
@@ -1054,12 +1052,12 @@ const rotateMultipleElements = (
|
||||
const origAngle =
|
||||
originalElements.get(element.id)?.angle ?? element.angle;
|
||||
const [rotatedCX, rotatedCY] = pointRotateRads(
|
||||
point(cx, cy),
|
||||
point(centerX, centerY),
|
||||
pointFrom(cx, cy),
|
||||
pointFrom(centerX, centerY),
|
||||
(centerAngle + origAngle - element.angle) as Radians,
|
||||
);
|
||||
|
||||
if (isArrowElement(element) && isElbowArrow(element)) {
|
||||
if (isElbowArrow(element)) {
|
||||
const points = getArrowLocalFixedPoints(element, elementsMap);
|
||||
mutateElbowArrow(element, elementsMap, points);
|
||||
} else {
|
||||
@@ -1111,40 +1109,44 @@ export const getResizeOffsetXY = (
|
||||
const angle = (
|
||||
selectedElements.length === 1 ? selectedElements[0].angle : 0
|
||||
) as Radians;
|
||||
[x, y] = pointRotateRads(point(x, y), point(cx, cy), -angle as Radians);
|
||||
[x, y] = pointRotateRads(
|
||||
pointFrom(x, y),
|
||||
pointFrom(cx, cy),
|
||||
-angle as Radians,
|
||||
);
|
||||
switch (transformHandleType) {
|
||||
case "n":
|
||||
return pointRotateRads(
|
||||
point(x - (x1 + x2) / 2, y - y1),
|
||||
point(0, 0),
|
||||
pointFrom(x - (x1 + x2) / 2, y - y1),
|
||||
pointFrom(0, 0),
|
||||
angle,
|
||||
);
|
||||
case "s":
|
||||
return pointRotateRads(
|
||||
point(x - (x1 + x2) / 2, y - y2),
|
||||
point(0, 0),
|
||||
pointFrom(x - (x1 + x2) / 2, y - y2),
|
||||
pointFrom(0, 0),
|
||||
angle,
|
||||
);
|
||||
case "w":
|
||||
return pointRotateRads(
|
||||
point(x - x1, y - (y1 + y2) / 2),
|
||||
point(0, 0),
|
||||
pointFrom(x - x1, y - (y1 + y2) / 2),
|
||||
pointFrom(0, 0),
|
||||
angle,
|
||||
);
|
||||
case "e":
|
||||
return pointRotateRads(
|
||||
point(x - x2, y - (y1 + y2) / 2),
|
||||
point(0, 0),
|
||||
pointFrom(x - x2, y - (y1 + y2) / 2),
|
||||
pointFrom(0, 0),
|
||||
angle,
|
||||
);
|
||||
case "nw":
|
||||
return pointRotateRads(point(x - x1, y - y1), point(0, 0), angle);
|
||||
return pointRotateRads(pointFrom(x - x1, y - y1), pointFrom(0, 0), angle);
|
||||
case "ne":
|
||||
return pointRotateRads(point(x - x2, y - y1), point(0, 0), angle);
|
||||
return pointRotateRads(pointFrom(x - x2, y - y1), pointFrom(0, 0), angle);
|
||||
case "sw":
|
||||
return pointRotateRads(point(x - x1, y - y2), point(0, 0), angle);
|
||||
return pointRotateRads(pointFrom(x - x1, y - y2), pointFrom(0, 0), angle);
|
||||
case "se":
|
||||
return pointRotateRads(point(x - x2, y - y2), point(0, 0), angle);
|
||||
return pointRotateRads(pointFrom(x - x2, y - y2), pointFrom(0, 0), angle);
|
||||
default:
|
||||
return [0, 0];
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import { SIDE_RESIZING_THRESHOLD } from "../constants";
|
||||
import { isLinearElement } from "./typeChecks";
|
||||
import type { GlobalPoint, LineSegment, LocalPoint } from "../../math";
|
||||
import {
|
||||
point,
|
||||
pointFrom,
|
||||
pointOnLineSegment,
|
||||
pointRotateRads,
|
||||
type Radians,
|
||||
@@ -92,16 +92,20 @@ export const resizeTest = <Point extends GlobalPoint | LocalPoint>(
|
||||
if (!(isLinearElement(element) && element.points.length <= 2)) {
|
||||
const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value;
|
||||
const sides = getSelectionBorders(
|
||||
point(x1 - SPACING, y1 - SPACING),
|
||||
point(x2 + SPACING, y2 + SPACING),
|
||||
point(cx, cy),
|
||||
pointFrom(x1 - SPACING, y1 - SPACING),
|
||||
pointFrom(x2 + SPACING, y2 + SPACING),
|
||||
pointFrom(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
|
||||
for (const [dir, side] of Object.entries(sides)) {
|
||||
// test to see if x, y are on the line segment
|
||||
if (
|
||||
pointOnLineSegment(point(x, y), side as LineSegment<Point>, SPACING)
|
||||
pointOnLineSegment(
|
||||
pointFrom(x, y),
|
||||
side as LineSegment<Point>,
|
||||
SPACING,
|
||||
)
|
||||
) {
|
||||
return dir as TransformHandleType;
|
||||
}
|
||||
@@ -178,9 +182,9 @@ export const getTransformHandleTypeFromCoords = <
|
||||
const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value;
|
||||
|
||||
const sides = getSelectionBorders(
|
||||
point(x1 - SPACING, y1 - SPACING),
|
||||
point(x2 + SPACING, y2 + SPACING),
|
||||
point(cx, cy),
|
||||
pointFrom(x1 - SPACING, y1 - SPACING),
|
||||
pointFrom(x2 + SPACING, y2 + SPACING),
|
||||
pointFrom(cx, cy),
|
||||
0 as Radians,
|
||||
);
|
||||
|
||||
@@ -188,7 +192,7 @@ export const getTransformHandleTypeFromCoords = <
|
||||
// test to see if x, y are on the line segment
|
||||
if (
|
||||
pointOnLineSegment(
|
||||
point(scenePointerX, scenePointerY),
|
||||
pointFrom(scenePointerX, scenePointerY),
|
||||
side as LineSegment<Point>,
|
||||
SPACING,
|
||||
)
|
||||
@@ -265,10 +269,10 @@ const getSelectionBorders = <Point extends LocalPoint | GlobalPoint>(
|
||||
center: Point,
|
||||
angle: Radians,
|
||||
) => {
|
||||
const topLeft = pointRotateRads(point(x1, y1), center, angle);
|
||||
const topRight = pointRotateRads(point(x2, y1), center, angle);
|
||||
const bottomLeft = pointRotateRads(point(x1, y2), center, angle);
|
||||
const bottomRight = pointRotateRads(point(x2, y2), center, angle);
|
||||
const topLeft = pointRotateRads(pointFrom(x1, y1), center, angle);
|
||||
const topRight = pointRotateRads(pointFrom(x2, y1), center, angle);
|
||||
const bottomLeft = pointRotateRads(pointFrom(x1, y2), center, angle);
|
||||
const bottomRight = pointRotateRads(pointFrom(x2, y2), center, angle);
|
||||
|
||||
return {
|
||||
n: [topLeft, topRight],
|
||||
|
||||
@@ -17,7 +17,7 @@ import type {
|
||||
ExcalidrawElbowArrowElement,
|
||||
} from "./types";
|
||||
import { ARROW_TYPE } from "../constants";
|
||||
import { point } from "../../math";
|
||||
import { pointFrom } from "../../math";
|
||||
|
||||
const { h } = window;
|
||||
|
||||
@@ -32,8 +32,8 @@ describe("elbow arrow routing", () => {
|
||||
}) as ExcalidrawElbowArrowElement;
|
||||
scene.insertElement(arrow);
|
||||
mutateElbowArrow(arrow, scene.getNonDeletedElementsMap(), [
|
||||
point(-45 - arrow.x, -100.1 - arrow.y),
|
||||
point(45 - arrow.x, 99.9 - arrow.y),
|
||||
pointFrom(-45 - arrow.x, -100.1 - arrow.y),
|
||||
pointFrom(45 - arrow.x, 99.9 - arrow.y),
|
||||
]);
|
||||
expect(arrow.points).toEqual([
|
||||
[0, 0],
|
||||
@@ -69,7 +69,7 @@ describe("elbow arrow routing", () => {
|
||||
y: -100.1,
|
||||
width: 90,
|
||||
height: 200,
|
||||
points: [point(0, 0), point(90, 200)],
|
||||
points: [pointFrom(0, 0), pointFrom(90, 200)],
|
||||
}) as ExcalidrawElbowArrowElement;
|
||||
scene.insertElement(rectangle1);
|
||||
scene.insertElement(rectangle2);
|
||||
@@ -81,7 +81,7 @@ describe("elbow arrow routing", () => {
|
||||
expect(arrow.startBinding).not.toBe(null);
|
||||
expect(arrow.endBinding).not.toBe(null);
|
||||
|
||||
mutateElbowArrow(arrow, elementsMap, [point(0, 0), point(90, 200)]);
|
||||
mutateElbowArrow(arrow, elementsMap, [pointFrom(0, 0), pointFrom(90, 200)]);
|
||||
|
||||
expect(arrow.points).toEqual([
|
||||
[0, 0],
|
||||
@@ -94,7 +94,16 @@ describe("elbow arrow routing", () => {
|
||||
|
||||
describe("elbow arrow ui", () => {
|
||||
beforeEach(async () => {
|
||||
localStorage.clear();
|
||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 1,
|
||||
clientY: 1,
|
||||
});
|
||||
const contextMenu = UI.queryContextMenu();
|
||||
fireEvent.click(queryByTestId(contextMenu!, "stats")!);
|
||||
});
|
||||
|
||||
it("can follow bound shapes", async () => {
|
||||
@@ -130,8 +139,8 @@ describe("elbow arrow ui", () => {
|
||||
expect(arrow.elbowed).toBe(true);
|
||||
expect(arrow.points).toEqual([
|
||||
[0, 0],
|
||||
[35, 0],
|
||||
[35, 200],
|
||||
[45, 0],
|
||||
[45, 200],
|
||||
[90, 200],
|
||||
]);
|
||||
});
|
||||
@@ -163,14 +172,6 @@ describe("elbow arrow ui", () => {
|
||||
h.state,
|
||||
)[0] as ExcalidrawArrowElement;
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 1,
|
||||
clientY: 1,
|
||||
});
|
||||
const contextMenu = UI.queryContextMenu();
|
||||
fireEvent.click(queryByTestId(contextMenu!, "stats")!);
|
||||
|
||||
mouse.click(51, 51);
|
||||
|
||||
const inputAngle = UI.queryStatsProperty("A")?.querySelector(
|
||||
@@ -182,8 +183,8 @@ describe("elbow arrow ui", () => {
|
||||
[0, 0],
|
||||
[35, 0],
|
||||
[35, 90],
|
||||
[25, 90],
|
||||
[25, 165],
|
||||
[35, 90], // Note that coordinates are rounded above!
|
||||
[35, 165],
|
||||
[103, 165],
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Radians } from "../../math";
|
||||
import {
|
||||
point,
|
||||
pointFrom,
|
||||
pointScaleFromOrigin,
|
||||
pointTranslate,
|
||||
vector,
|
||||
@@ -36,11 +36,11 @@ import {
|
||||
HEADING_UP,
|
||||
vectorToHeading,
|
||||
} from "./heading";
|
||||
import type { ElementUpdate } from "./mutateElement";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { isBindableElement, isRectanguloidElement } from "./typeChecks";
|
||||
import type {
|
||||
ExcalidrawElbowArrowElement,
|
||||
FixedPointBinding,
|
||||
NonDeletedSceneElementsMap,
|
||||
SceneElementsMap,
|
||||
} from "./types";
|
||||
@@ -72,16 +72,48 @@ export const mutateElbowArrow = (
|
||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
nextPoints: readonly LocalPoint[],
|
||||
offset?: Vector,
|
||||
otherUpdates?: {
|
||||
startBinding?: FixedPointBinding | null;
|
||||
endBinding?: FixedPointBinding | null;
|
||||
otherUpdates?: Omit<
|
||||
ElementUpdate<ExcalidrawElbowArrowElement>,
|
||||
"angle" | "x" | "y" | "width" | "height" | "elbowed" | "points"
|
||||
>,
|
||||
options?: {
|
||||
isDragging?: boolean;
|
||||
informMutation?: boolean;
|
||||
},
|
||||
) => {
|
||||
const update = updateElbowArrow(
|
||||
arrow,
|
||||
elementsMap,
|
||||
nextPoints,
|
||||
offset,
|
||||
options,
|
||||
);
|
||||
if (update) {
|
||||
mutateElement(
|
||||
arrow,
|
||||
{
|
||||
...otherUpdates,
|
||||
...update,
|
||||
angle: 0 as Radians,
|
||||
},
|
||||
options?.informMutation,
|
||||
);
|
||||
} else {
|
||||
console.error("Elbow arrow cannot find a route");
|
||||
}
|
||||
};
|
||||
|
||||
export const updateElbowArrow = (
|
||||
arrow: ExcalidrawElbowArrowElement,
|
||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
nextPoints: readonly LocalPoint[],
|
||||
offset?: Vector,
|
||||
options?: {
|
||||
isDragging?: boolean;
|
||||
disableBinding?: boolean;
|
||||
informMutation?: boolean;
|
||||
},
|
||||
) => {
|
||||
): ElementUpdate<ExcalidrawElbowArrowElement> | null => {
|
||||
const origStartGlobalPoint: GlobalPoint = pointTranslate(
|
||||
pointTranslate<LocalPoint, GlobalPoint>(
|
||||
nextPoints[0],
|
||||
@@ -235,6 +267,8 @@ export const mutateElbowArrow = (
|
||||
BASE_PADDING,
|
||||
),
|
||||
boundsOverlap,
|
||||
hoveredStartElement && aabbForElement(hoveredStartElement),
|
||||
hoveredEndElement && aabbForElement(hoveredEndElement),
|
||||
);
|
||||
const startDonglePosition = getDonglePosition(
|
||||
dynamicAABBs[0],
|
||||
@@ -295,18 +329,10 @@ export const mutateElbowArrow = (
|
||||
startDongle && points.unshift(startGlobalPoint);
|
||||
endDongle && points.push(endGlobalPoint);
|
||||
|
||||
mutateElement(
|
||||
arrow,
|
||||
{
|
||||
...otherUpdates,
|
||||
...normalizedArrowElementUpdate(simplifyElbowArrowPoints(points), 0, 0),
|
||||
angle: 0 as Radians,
|
||||
},
|
||||
options?.informMutation,
|
||||
);
|
||||
} else {
|
||||
console.error("Elbow arrow cannot find a route");
|
||||
return normalizedArrowElementUpdate(simplifyElbowArrowPoints(points), 0, 0);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const offsetFromHeading = (
|
||||
@@ -475,7 +501,11 @@ const generateDynamicAABBs = (
|
||||
startDifference?: [number, number, number, number],
|
||||
endDifference?: [number, number, number, number],
|
||||
disableSideHack?: boolean,
|
||||
startElementBounds?: Bounds | null,
|
||||
endElementBounds?: Bounds | null,
|
||||
): Bounds[] => {
|
||||
const startEl = startElementBounds ?? a;
|
||||
const endEl = endElementBounds ?? b;
|
||||
const [startUp, startRight, startDown, startLeft] = startDifference ?? [
|
||||
0, 0, 0, 0,
|
||||
];
|
||||
@@ -484,29 +514,29 @@ const generateDynamicAABBs = (
|
||||
const first = [
|
||||
a[0] > b[2]
|
||||
? a[1] > b[3] || a[3] < b[1]
|
||||
? Math.min((a[0] + b[2]) / 2, a[0] - startLeft)
|
||||
: (a[0] + b[2]) / 2
|
||||
? Math.min((startEl[0] + endEl[2]) / 2, a[0] - startLeft)
|
||||
: (startEl[0] + endEl[2]) / 2
|
||||
: a[0] > b[0]
|
||||
? a[0] - startLeft
|
||||
: common[0] - startLeft,
|
||||
a[1] > b[3]
|
||||
? a[0] > b[2] || a[2] < b[0]
|
||||
? Math.min((a[1] + b[3]) / 2, a[1] - startUp)
|
||||
: (a[1] + b[3]) / 2
|
||||
? Math.min((startEl[1] + endEl[3]) / 2, a[1] - startUp)
|
||||
: (startEl[1] + endEl[3]) / 2
|
||||
: a[1] > b[1]
|
||||
? a[1] - startUp
|
||||
: common[1] - startUp,
|
||||
a[2] < b[0]
|
||||
? a[1] > b[3] || a[3] < b[1]
|
||||
? Math.max((a[2] + b[0]) / 2, a[2] + startRight)
|
||||
: (a[2] + b[0]) / 2
|
||||
? Math.max((startEl[2] + endEl[0]) / 2, a[2] + startRight)
|
||||
: (startEl[2] + endEl[0]) / 2
|
||||
: a[2] < b[2]
|
||||
? a[2] + startRight
|
||||
: common[2] + startRight,
|
||||
a[3] < b[1]
|
||||
? a[0] > b[2] || a[2] < b[0]
|
||||
? Math.max((a[3] + b[1]) / 2, a[3] + startDown)
|
||||
: (a[3] + b[1]) / 2
|
||||
? Math.max((startEl[3] + endEl[1]) / 2, a[3] + startDown)
|
||||
: (startEl[3] + endEl[1]) / 2
|
||||
: a[3] < b[3]
|
||||
? a[3] + startDown
|
||||
: common[3] + startDown,
|
||||
@@ -514,29 +544,29 @@ const generateDynamicAABBs = (
|
||||
const second = [
|
||||
b[0] > a[2]
|
||||
? b[1] > a[3] || b[3] < a[1]
|
||||
? Math.min((b[0] + a[2]) / 2, b[0] - endLeft)
|
||||
: (b[0] + a[2]) / 2
|
||||
? Math.min((endEl[0] + startEl[2]) / 2, b[0] - endLeft)
|
||||
: (endEl[0] + startEl[2]) / 2
|
||||
: b[0] > a[0]
|
||||
? b[0] - endLeft
|
||||
: common[0] - endLeft,
|
||||
b[1] > a[3]
|
||||
? b[0] > a[2] || b[2] < a[0]
|
||||
? Math.min((b[1] + a[3]) / 2, b[1] - endUp)
|
||||
: (b[1] + a[3]) / 2
|
||||
? Math.min((endEl[1] + startEl[3]) / 2, b[1] - endUp)
|
||||
: (endEl[1] + startEl[3]) / 2
|
||||
: b[1] > a[1]
|
||||
? b[1] - endUp
|
||||
: common[1] - endUp,
|
||||
b[2] < a[0]
|
||||
? b[1] > a[3] || b[3] < a[1]
|
||||
? Math.max((b[2] + a[0]) / 2, b[2] + endRight)
|
||||
: (b[2] + a[0]) / 2
|
||||
? Math.max((endEl[2] + startEl[0]) / 2, b[2] + endRight)
|
||||
: (endEl[2] + startEl[0]) / 2
|
||||
: b[2] < a[2]
|
||||
? b[2] + endRight
|
||||
: common[2] + endRight,
|
||||
b[3] < a[1]
|
||||
? b[0] > a[2] || b[2] < a[0]
|
||||
? Math.max((b[3] + a[1]) / 2, b[3] + endDown)
|
||||
: (b[3] + a[1]) / 2
|
||||
? Math.max((endEl[3] + startEl[1]) / 2, b[3] + endDown)
|
||||
: (endEl[3] + startEl[1]) / 2
|
||||
: b[3] < a[3]
|
||||
? b[3] + endDown
|
||||
: common[3] + endDown,
|
||||
@@ -713,13 +743,13 @@ const getDonglePosition = (
|
||||
): GlobalPoint => {
|
||||
switch (heading) {
|
||||
case HEADING_UP:
|
||||
return point(p[0], bounds[1]);
|
||||
return pointFrom(p[0], bounds[1]);
|
||||
case HEADING_RIGHT:
|
||||
return point(bounds[2], p[1]);
|
||||
return pointFrom(bounds[2], p[1]);
|
||||
case HEADING_DOWN:
|
||||
return point(p[0], bounds[3]);
|
||||
return pointFrom(p[0], bounds[3]);
|
||||
}
|
||||
return point(bounds[0], p[1]);
|
||||
return pointFrom(bounds[0], p[1]);
|
||||
};
|
||||
|
||||
const estimateSegmentCount = (
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ElementsMap, ExcalidrawElement } from "./types";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { isFreeDrawElement, isLinearElement } from "./typeChecks";
|
||||
import { SHIFT_LOCKING_ANGLE } from "../constants";
|
||||
import type { AppState, Zoom } from "../types";
|
||||
import type { AppState, Offsets, Zoom } from "../types";
|
||||
import { getCommonBounds, getElementBounds } from "./bounds";
|
||||
import { viewportCoordsToSceneCoords } from "../utils";
|
||||
|
||||
@@ -67,12 +67,7 @@ export const isElementCompletelyInViewport = (
|
||||
scrollY: number;
|
||||
},
|
||||
elementsMap: ElementsMap,
|
||||
padding?: Partial<{
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
left: number;
|
||||
}>,
|
||||
padding?: Offsets,
|
||||
) => {
|
||||
const [x1, y1, x2, y2] = getCommonBounds(elements, elementsMap); // scene coordinates
|
||||
const topLeftSceneCoords = viewportCoordsToSceneCoords(
|
||||
|
||||
@@ -284,16 +284,17 @@ export const measureText = (
|
||||
text: string,
|
||||
font: FontString,
|
||||
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||
forceAdvanceWidth?: true,
|
||||
) => {
|
||||
text = text
|
||||
const _text = text
|
||||
.split("\n")
|
||||
// replace empty lines with single space because leading/trailing empty
|
||||
// lines would be stripped from computation
|
||||
.map((x) => x || " ")
|
||||
.join("\n");
|
||||
const fontSize = parseFloat(font);
|
||||
const height = getTextHeight(text, fontSize, lineHeight);
|
||||
const width = getTextWidth(text, font);
|
||||
const height = getTextHeight(_text, fontSize, lineHeight);
|
||||
const width = getTextWidth(_text, font, forceAdvanceWidth);
|
||||
return { width, height };
|
||||
};
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import type {
|
||||
import { API } from "../tests/helpers/api";
|
||||
import { getOriginalContainerHeightFromCache } from "./containerCache";
|
||||
import { getTextEditor, updateTextEditor } from "../tests/queries/dom";
|
||||
import { point } from "../../math";
|
||||
import { pointFrom } from "../../math";
|
||||
|
||||
// Unmount ReactDOM from root
|
||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||
@@ -42,7 +42,7 @@ describe("textWysiwyg", () => {
|
||||
type: "line",
|
||||
width: 100,
|
||||
height: 0,
|
||||
points: [point(0, 0), point(100, 0)],
|
||||
points: [pointFrom(0, 0), pointFrom(100, 0)],
|
||||
});
|
||||
const textSize = 20;
|
||||
const text = API.createElement({
|
||||
|
||||
@@ -247,7 +247,7 @@ export const textWysiwyg = ({
|
||||
|
||||
// adding left and right padding buffer, so that browser does not cut the glyphs (does not work in Safari)
|
||||
const padding = !isSafari
|
||||
? Math.ceil(updatedTextElement.fontSize / 2)
|
||||
? Math.ceil(updatedTextElement.fontSize / appState.zoom.value / 2)
|
||||
: 0;
|
||||
|
||||
// Make sure text editor height doesn't go beyond viewport
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
isIOS,
|
||||
} from "../constants";
|
||||
import type { Radians } from "../../math";
|
||||
import { point, pointRotateRads } from "../../math";
|
||||
import { pointFrom, pointRotateRads } from "../../math";
|
||||
|
||||
export type TransformHandleDirection =
|
||||
| "n"
|
||||
@@ -95,8 +95,8 @@ const generateTransformHandle = (
|
||||
angle: Radians,
|
||||
): TransformHandle => {
|
||||
const [xx, yy] = pointRotateRads(
|
||||
point(x + width / 2, y + height / 2),
|
||||
point(cx, cy),
|
||||
pointFrom(x + width / 2, y + height / 2),
|
||||
pointFrom(cx, cy),
|
||||
angle,
|
||||
);
|
||||
return [xx - width / 2, yy - height / 2, width, height];
|
||||
|
||||
@@ -320,9 +320,12 @@ export const getDefaultRoundnessTypeForElement = (
|
||||
};
|
||||
|
||||
export const isFixedPointBinding = (
|
||||
binding: PointBinding,
|
||||
binding: PointBinding | FixedPointBinding,
|
||||
): binding is FixedPointBinding => {
|
||||
return binding.fixedPoint != null;
|
||||
return (
|
||||
Object.hasOwn(binding, "fixedPoint") &&
|
||||
(binding as FixedPointBinding).fixedPoint != null
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: Move this to @excalidraw/math
|
||||
|
||||
@@ -202,6 +202,7 @@ export type ExcalidrawElement =
|
||||
| ExcalidrawGenericElement
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawLinearElement
|
||||
| ExcalidrawArrowElement
|
||||
| ExcalidrawFreeDrawElement
|
||||
| ExcalidrawImageElement
|
||||
| ExcalidrawFrameElement
|
||||
@@ -277,15 +278,19 @@ export type PointBinding = {
|
||||
elementId: ExcalidrawBindableElement["id"];
|
||||
focus: number;
|
||||
gap: number;
|
||||
// Represents the fixed point binding information in form of a vertical and
|
||||
// horizontal ratio (i.e. a percentage value in the 0.0-1.0 range). This ratio
|
||||
// gives the user selected fixed point by multiplying the bound element width
|
||||
// with fixedPoint[0] and the bound element height with fixedPoint[1] to get the
|
||||
// bound element-local point coordinate.
|
||||
fixedPoint: FixedPoint | null;
|
||||
};
|
||||
|
||||
export type FixedPointBinding = Merge<PointBinding, { fixedPoint: FixedPoint }>;
|
||||
export type FixedPointBinding = Merge<
|
||||
PointBinding,
|
||||
{
|
||||
// Represents the fixed point binding information in form of a vertical and
|
||||
// horizontal ratio (i.e. a percentage value in the 0.0-1.0 range). This ratio
|
||||
// gives the user selected fixed point by multiplying the bound element width
|
||||
// with fixedPoint[0] and the bound element height with fixedPoint[1] to get the
|
||||
// bound element-local point coordinate.
|
||||
fixedPoint: FixedPoint;
|
||||
}
|
||||
>;
|
||||
|
||||
export type Arrowhead =
|
||||
| "arrow"
|
||||
|
||||
Reference in New Issue
Block a user