Merge branch 'master' into mtolmacs/fix/small-elbow-routing
This commit is contained in:
commit
4eb1bd8036
@ -1,17 +1,17 @@
|
||||
import rough from "roughjs/bin/rough";
|
||||
|
||||
import {
|
||||
rescalePoints,
|
||||
arrayToMap,
|
||||
invariant,
|
||||
rescalePoints,
|
||||
sizeOf,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
degreesToRadians,
|
||||
lineSegment,
|
||||
pointFrom,
|
||||
pointDistance,
|
||||
pointFrom,
|
||||
pointFromArray,
|
||||
pointRotateRads,
|
||||
} from "@excalidraw/math";
|
||||
@ -33,8 +33,8 @@ import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||
|
||||
import { ShapeCache } from "./ShapeCache";
|
||||
import { generateRoughOptions } from "./Shape";
|
||||
import { ShapeCache } from "./ShapeCache";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||
import {
|
||||
@ -52,20 +52,20 @@ import {
|
||||
deconstructRectanguloidElement,
|
||||
} from "./utils";
|
||||
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawLinearElement,
|
||||
Arrowhead,
|
||||
ExcalidrawFreeDrawElement,
|
||||
NonDeleted,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
ElementsMap,
|
||||
ExcalidrawRectanguloidElement,
|
||||
ExcalidrawEllipseElement,
|
||||
ElementsMapOrArray,
|
||||
} from "./types";
|
||||
import type { Drawable, Op } from "roughjs/bin/core";
|
||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||
import type {
|
||||
Arrowhead,
|
||||
ElementsMap,
|
||||
ElementsMapOrArray,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawEllipseElement,
|
||||
ExcalidrawFreeDrawElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawRectanguloidElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
NonDeleted,
|
||||
} from "./types";
|
||||
|
||||
export type RectangleBox = {
|
||||
x: number;
|
||||
|
@ -10,7 +10,12 @@ import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import StatsDragInput from "./DragInput";
|
||||
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
||||
import {
|
||||
getAtomicUnits,
|
||||
getStepSizedValue,
|
||||
isPropertyEditable,
|
||||
STEP_SIZE,
|
||||
} from "./utils";
|
||||
import { getElementsInAtomicUnit, moveElement } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
@ -26,8 +31,6 @@ interface MultiPositionProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
const STEP_SIZE = 10;
|
||||
|
||||
const moveElements = (
|
||||
property: MultiPositionProps["property"],
|
||||
changeInTopX: number,
|
||||
|
@ -11,7 +11,7 @@ import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import StatsDragInput from "./DragInput";
|
||||
import { getStepSizedValue, moveElement } from "./utils";
|
||||
import { getStepSizedValue, moveElement, STEP_SIZE } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
import type { AppState } from "../../types";
|
||||
@ -24,8 +24,6 @@ interface PositionProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
const STEP_SIZE = 10;
|
||||
|
||||
const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||
accumulatedChange,
|
||||
instantChange,
|
||||
|
@ -12,6 +12,8 @@ import {
|
||||
isInGroup,
|
||||
} from "@excalidraw/element/groups";
|
||||
|
||||
import { getFrameChildren } from "@excalidraw/element/frame";
|
||||
|
||||
import type { Radians } from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
@ -36,6 +38,7 @@ export type StatsInputProperty =
|
||||
| "gridStep";
|
||||
|
||||
export const SMALLEST_DELTA = 0.01;
|
||||
export const STEP_SIZE = 10;
|
||||
|
||||
export const isPropertyEditable = (
|
||||
element: ExcalidrawElement,
|
||||
@ -169,6 +172,68 @@ export const moveElement = (
|
||||
{ informMutation: shouldInformMutation, isDragging: false },
|
||||
);
|
||||
}
|
||||
|
||||
if (isFrameLikeElement(originalElement)) {
|
||||
const originalChildren = getFrameChildren(
|
||||
originalElementsMap,
|
||||
originalElement.id,
|
||||
);
|
||||
originalChildren.forEach((child) => {
|
||||
const latestChildElement = elementsMap.get(child.id);
|
||||
|
||||
if (!latestChildElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [childCX, childCY] = [
|
||||
child.x + child.width / 2,
|
||||
child.y + child.height / 2,
|
||||
];
|
||||
const [childTopLeftX, childTopLeftY] = pointRotateRads(
|
||||
pointFrom(child.x, child.y),
|
||||
pointFrom(childCX, childCY),
|
||||
child.angle,
|
||||
);
|
||||
|
||||
const childNewTopLeftX = Math.round(childTopLeftX + changeInX);
|
||||
const childNewTopLeftY = Math.round(childTopLeftY + changeInY);
|
||||
|
||||
const [childX, childY] = pointRotateRads(
|
||||
pointFrom(childNewTopLeftX, childNewTopLeftY),
|
||||
pointFrom(childCX + changeInX, childCY + changeInY),
|
||||
-child.angle as Radians,
|
||||
);
|
||||
|
||||
scene.mutateElement(
|
||||
latestChildElement,
|
||||
{
|
||||
x: childX,
|
||||
y: childY,
|
||||
},
|
||||
{ informMutation: shouldInformMutation, isDragging: false },
|
||||
);
|
||||
updateBindings(latestChildElement, scene, {
|
||||
simultaneouslyUpdated: originalChildren,
|
||||
});
|
||||
|
||||
const boundTextElement = getBoundTextElement(
|
||||
latestChildElement,
|
||||
originalElementsMap,
|
||||
);
|
||||
if (boundTextElement) {
|
||||
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
|
||||
latestBoundTextElement &&
|
||||
scene.mutateElement(
|
||||
latestBoundTextElement,
|
||||
{
|
||||
x: boundTextElement.x + changeInX,
|
||||
y: boundTextElement.y + changeInY,
|
||||
},
|
||||
{ informMutation: shouldInformMutation, isDragging: false },
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getAtomicUnits = (
|
||||
|
@ -1,7 +1,30 @@
|
||||
import { THEME, THEME_FILTER } from "@excalidraw/common";
|
||||
import { elementCenterPoint, THEME, THEME_FILTER } from "@excalidraw/common";
|
||||
|
||||
import { FIXED_BINDING_DISTANCE } from "@excalidraw/element/binding";
|
||||
import { getDiamondPoints } from "@excalidraw/element/bounds";
|
||||
import { getCornerRadius } from "@excalidraw/element/shapes";
|
||||
|
||||
import {
|
||||
bezierEquation,
|
||||
curve,
|
||||
curveTangent,
|
||||
type GlobalPoint,
|
||||
pointFrom,
|
||||
pointFromVector,
|
||||
pointRotateRads,
|
||||
vector,
|
||||
vectorNormal,
|
||||
vectorNormalize,
|
||||
vectorScale,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
ExcalidrawDiamondElement,
|
||||
ExcalidrawRectanguloidElement,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type { StaticCanvasRenderConfig } from "../scene/types";
|
||||
import type { StaticCanvasAppState, AppState } from "../types";
|
||||
import type { AppState, StaticCanvasAppState } from "../types";
|
||||
|
||||
export const fillCircle = (
|
||||
context: CanvasRenderingContext2D,
|
||||
@ -72,3 +95,399 @@ export const bootstrapCanvas = ({
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
function drawCatmullRomQuadraticApprox(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
points: GlobalPoint[],
|
||||
segments = 20,
|
||||
) {
|
||||
ctx.lineTo(points[0][0], points[0][1]);
|
||||
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
const p0 = points[i - 1 < 0 ? 0 : i - 1];
|
||||
const p1 = points[i];
|
||||
const p2 = points[i + 1 >= points.length ? points.length - 1 : i + 1];
|
||||
|
||||
for (let t = 0; t <= 1; t += 1 / segments) {
|
||||
const t2 = t * t;
|
||||
|
||||
const x =
|
||||
(1 - t) * (1 - t) * p0[0] + 2 * (1 - t) * t * p1[0] + t2 * p2[0];
|
||||
|
||||
const y =
|
||||
(1 - t) * (1 - t) * p0[1] + 2 * (1 - t) * t * p1[1] + t2 * p2[1];
|
||||
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawCatmullRomCubicApprox(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
points: GlobalPoint[],
|
||||
segments = 20,
|
||||
) {
|
||||
ctx.lineTo(points[0][0], points[0][1]);
|
||||
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
const p0 = points[i - 1 < 0 ? 0 : i - 1];
|
||||
const p1 = points[i];
|
||||
const p2 = points[i + 1 >= points.length ? points.length - 1 : i + 1];
|
||||
const p3 = points[i + 2 >= points.length ? points.length - 1 : i + 2];
|
||||
|
||||
for (let t = 0; t <= 1; t += 1 / segments) {
|
||||
const t2 = t * t;
|
||||
const t3 = t2 * t;
|
||||
|
||||
const x =
|
||||
0.5 *
|
||||
(2 * p1[0] +
|
||||
(-p0[0] + p2[0]) * t +
|
||||
(2 * p0[0] - 5 * p1[0] + 4 * p2[0] - p3[0]) * t2 +
|
||||
(-p0[0] + 3 * p1[0] - 3 * p2[0] + p3[0]) * t3);
|
||||
|
||||
const y =
|
||||
0.5 *
|
||||
(2 * p1[1] +
|
||||
(-p0[1] + p2[1]) * t +
|
||||
(2 * p0[1] - 5 * p1[1] + 4 * p2[1] - p3[1]) * t2 +
|
||||
(-p0[1] + 3 * p1[1] - 3 * p2[1] + p3[1]) * t3);
|
||||
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const drawHighlightForRectWithRotation = (
|
||||
context: CanvasRenderingContext2D,
|
||||
element: ExcalidrawRectanguloidElement,
|
||||
padding: number,
|
||||
) => {
|
||||
const [x, y] = pointRotateRads(
|
||||
pointFrom<GlobalPoint>(element.x, element.y),
|
||||
elementCenterPoint(element),
|
||||
element.angle,
|
||||
);
|
||||
|
||||
context.save();
|
||||
context.translate(x, y);
|
||||
context.rotate(element.angle);
|
||||
|
||||
let radius = getCornerRadius(
|
||||
Math.min(element.width, element.height),
|
||||
element,
|
||||
);
|
||||
if (radius === 0) {
|
||||
radius = 0.01;
|
||||
}
|
||||
|
||||
context.beginPath();
|
||||
|
||||
{
|
||||
const topLeftApprox = offsetQuadraticBezier(
|
||||
pointFrom(0, 0 + radius),
|
||||
pointFrom(0, 0),
|
||||
pointFrom(0 + radius, 0),
|
||||
padding,
|
||||
);
|
||||
const topRightApprox = offsetQuadraticBezier(
|
||||
pointFrom(element.width - radius, 0),
|
||||
pointFrom(element.width, 0),
|
||||
pointFrom(element.width, radius),
|
||||
padding,
|
||||
);
|
||||
const bottomRightApprox = offsetQuadraticBezier(
|
||||
pointFrom(element.width, element.height - radius),
|
||||
pointFrom(element.width, element.height),
|
||||
pointFrom(element.width - radius, element.height),
|
||||
padding,
|
||||
);
|
||||
const bottomLeftApprox = offsetQuadraticBezier(
|
||||
pointFrom(radius, element.height),
|
||||
pointFrom(0, element.height),
|
||||
pointFrom(0, element.height - radius),
|
||||
padding,
|
||||
);
|
||||
|
||||
context.moveTo(
|
||||
topLeftApprox[topLeftApprox.length - 1][0],
|
||||
topLeftApprox[topLeftApprox.length - 1][1],
|
||||
);
|
||||
context.lineTo(topRightApprox[0][0], topRightApprox[0][1]);
|
||||
drawCatmullRomQuadraticApprox(context, topRightApprox);
|
||||
context.lineTo(bottomRightApprox[0][0], bottomRightApprox[0][1]);
|
||||
drawCatmullRomQuadraticApprox(context, bottomRightApprox);
|
||||
context.lineTo(bottomLeftApprox[0][0], bottomLeftApprox[0][1]);
|
||||
drawCatmullRomQuadraticApprox(context, bottomLeftApprox);
|
||||
context.lineTo(topLeftApprox[0][0], topLeftApprox[0][1]);
|
||||
drawCatmullRomQuadraticApprox(context, topLeftApprox);
|
||||
}
|
||||
|
||||
// Counter-clockwise for the cutout in the middle. We need to have an "inverse
|
||||
// mask" on a filled shape for the diamond highlight, because stroking creates
|
||||
// sharp inset edges on line joins < 90 degrees.
|
||||
{
|
||||
const topLeftApprox = offsetQuadraticBezier(
|
||||
pointFrom(0 + radius, 0),
|
||||
pointFrom(0, 0),
|
||||
pointFrom(0, 0 + radius),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const topRightApprox = offsetQuadraticBezier(
|
||||
pointFrom(element.width, radius),
|
||||
pointFrom(element.width, 0),
|
||||
pointFrom(element.width - radius, 0),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const bottomRightApprox = offsetQuadraticBezier(
|
||||
pointFrom(element.width - radius, element.height),
|
||||
pointFrom(element.width, element.height),
|
||||
pointFrom(element.width, element.height - radius),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const bottomLeftApprox = offsetQuadraticBezier(
|
||||
pointFrom(0, element.height - radius),
|
||||
pointFrom(0, element.height),
|
||||
pointFrom(radius, element.height),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
|
||||
context.moveTo(
|
||||
topLeftApprox[topLeftApprox.length - 1][0],
|
||||
topLeftApprox[topLeftApprox.length - 1][1],
|
||||
);
|
||||
context.lineTo(bottomLeftApprox[0][0], bottomLeftApprox[0][1]);
|
||||
drawCatmullRomQuadraticApprox(context, bottomLeftApprox);
|
||||
context.lineTo(bottomRightApprox[0][0], bottomRightApprox[0][1]);
|
||||
drawCatmullRomQuadraticApprox(context, bottomRightApprox);
|
||||
context.lineTo(topRightApprox[0][0], topRightApprox[0][1]);
|
||||
drawCatmullRomQuadraticApprox(context, topRightApprox);
|
||||
context.lineTo(topLeftApprox[0][0], topLeftApprox[0][1]);
|
||||
drawCatmullRomQuadraticApprox(context, topLeftApprox);
|
||||
}
|
||||
|
||||
context.closePath();
|
||||
context.fill();
|
||||
|
||||
context.restore();
|
||||
};
|
||||
|
||||
export const strokeEllipseWithRotation = (
|
||||
context: CanvasRenderingContext2D,
|
||||
width: number,
|
||||
height: number,
|
||||
cx: number,
|
||||
cy: number,
|
||||
angle: number,
|
||||
) => {
|
||||
context.beginPath();
|
||||
context.ellipse(cx, cy, width / 2, height / 2, angle, 0, Math.PI * 2);
|
||||
context.stroke();
|
||||
};
|
||||
|
||||
export const strokeRectWithRotation = (
|
||||
context: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
cx: number,
|
||||
cy: number,
|
||||
angle: number,
|
||||
fill: boolean = false,
|
||||
/** should account for zoom */
|
||||
radius: number = 0,
|
||||
) => {
|
||||
context.save();
|
||||
context.translate(cx, cy);
|
||||
context.rotate(angle);
|
||||
if (fill) {
|
||||
context.fillRect(x - cx, y - cy, width, height);
|
||||
}
|
||||
if (radius && context.roundRect) {
|
||||
context.beginPath();
|
||||
context.roundRect(x - cx, y - cy, width, height, radius);
|
||||
context.stroke();
|
||||
context.closePath();
|
||||
} else {
|
||||
context.strokeRect(x - cx, y - cy, width, height);
|
||||
}
|
||||
context.restore();
|
||||
};
|
||||
|
||||
export const drawHighlightForDiamondWithRotation = (
|
||||
context: CanvasRenderingContext2D,
|
||||
padding: number,
|
||||
element: ExcalidrawDiamondElement,
|
||||
) => {
|
||||
const [x, y] = pointRotateRads(
|
||||
pointFrom<GlobalPoint>(element.x, element.y),
|
||||
elementCenterPoint(element),
|
||||
element.angle,
|
||||
);
|
||||
context.save();
|
||||
context.translate(x, y);
|
||||
context.rotate(element.angle);
|
||||
|
||||
{
|
||||
context.beginPath();
|
||||
|
||||
const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
|
||||
getDiamondPoints(element);
|
||||
const verticalRadius = element.roundness
|
||||
? getCornerRadius(Math.abs(topX - leftX), element)
|
||||
: (topX - leftX) * 0.01;
|
||||
const horizontalRadius = element.roundness
|
||||
? getCornerRadius(Math.abs(rightY - topY), element)
|
||||
: (rightY - topY) * 0.01;
|
||||
const topApprox = offsetCubicBezier(
|
||||
pointFrom(topX - verticalRadius, topY + horizontalRadius),
|
||||
pointFrom(topX, topY),
|
||||
pointFrom(topX, topY),
|
||||
pointFrom(topX + verticalRadius, topY + horizontalRadius),
|
||||
padding,
|
||||
);
|
||||
const rightApprox = offsetCubicBezier(
|
||||
pointFrom(rightX - verticalRadius, rightY - horizontalRadius),
|
||||
pointFrom(rightX, rightY),
|
||||
pointFrom(rightX, rightY),
|
||||
pointFrom(rightX - verticalRadius, rightY + horizontalRadius),
|
||||
padding,
|
||||
);
|
||||
const bottomApprox = offsetCubicBezier(
|
||||
pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius),
|
||||
pointFrom(bottomX, bottomY),
|
||||
pointFrom(bottomX, bottomY),
|
||||
pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius),
|
||||
padding,
|
||||
);
|
||||
const leftApprox = offsetCubicBezier(
|
||||
pointFrom(leftX + verticalRadius, leftY + horizontalRadius),
|
||||
pointFrom(leftX, leftY),
|
||||
pointFrom(leftX, leftY),
|
||||
pointFrom(leftX + verticalRadius, leftY - horizontalRadius),
|
||||
padding,
|
||||
);
|
||||
|
||||
context.moveTo(
|
||||
topApprox[topApprox.length - 1][0],
|
||||
topApprox[topApprox.length - 1][1],
|
||||
);
|
||||
context.lineTo(rightApprox[0][0], rightApprox[0][1]);
|
||||
drawCatmullRomCubicApprox(context, rightApprox);
|
||||
context.lineTo(bottomApprox[0][0], bottomApprox[0][1]);
|
||||
drawCatmullRomCubicApprox(context, bottomApprox);
|
||||
context.lineTo(leftApprox[0][0], leftApprox[0][1]);
|
||||
drawCatmullRomCubicApprox(context, leftApprox);
|
||||
context.lineTo(topApprox[0][0], topApprox[0][1]);
|
||||
drawCatmullRomCubicApprox(context, topApprox);
|
||||
}
|
||||
|
||||
// Counter-clockwise for the cutout in the middle. We need to have an "inverse
|
||||
// mask" on a filled shape for the diamond highlight, because stroking creates
|
||||
// sharp inset edges on line joins < 90 degrees.
|
||||
{
|
||||
const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
|
||||
getDiamondPoints(element);
|
||||
const verticalRadius = element.roundness
|
||||
? getCornerRadius(Math.abs(topX - leftX), element)
|
||||
: (topX - leftX) * 0.01;
|
||||
const horizontalRadius = element.roundness
|
||||
? getCornerRadius(Math.abs(rightY - topY), element)
|
||||
: (rightY - topY) * 0.01;
|
||||
const topApprox = offsetCubicBezier(
|
||||
pointFrom(topX + verticalRadius, topY + horizontalRadius),
|
||||
pointFrom(topX, topY),
|
||||
pointFrom(topX, topY),
|
||||
pointFrom(topX - verticalRadius, topY + horizontalRadius),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const rightApprox = offsetCubicBezier(
|
||||
pointFrom(rightX - verticalRadius, rightY + horizontalRadius),
|
||||
pointFrom(rightX, rightY),
|
||||
pointFrom(rightX, rightY),
|
||||
pointFrom(rightX - verticalRadius, rightY - horizontalRadius),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const bottomApprox = offsetCubicBezier(
|
||||
pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius),
|
||||
pointFrom(bottomX, bottomY),
|
||||
pointFrom(bottomX, bottomY),
|
||||
pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const leftApprox = offsetCubicBezier(
|
||||
pointFrom(leftX + verticalRadius, leftY - horizontalRadius),
|
||||
pointFrom(leftX, leftY),
|
||||
pointFrom(leftX, leftY),
|
||||
pointFrom(leftX + verticalRadius, leftY + horizontalRadius),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
|
||||
context.moveTo(
|
||||
topApprox[topApprox.length - 1][0],
|
||||
topApprox[topApprox.length - 1][1],
|
||||
);
|
||||
context.lineTo(leftApprox[0][0], leftApprox[0][1]);
|
||||
drawCatmullRomCubicApprox(context, leftApprox);
|
||||
context.lineTo(bottomApprox[0][0], bottomApprox[0][1]);
|
||||
drawCatmullRomCubicApprox(context, bottomApprox);
|
||||
context.lineTo(rightApprox[0][0], rightApprox[0][1]);
|
||||
drawCatmullRomCubicApprox(context, rightApprox);
|
||||
context.lineTo(topApprox[0][0], topApprox[0][1]);
|
||||
drawCatmullRomCubicApprox(context, topApprox);
|
||||
}
|
||||
context.closePath();
|
||||
context.fill();
|
||||
context.restore();
|
||||
};
|
||||
|
||||
function offsetCubicBezier(
|
||||
p0: GlobalPoint,
|
||||
p1: GlobalPoint,
|
||||
p2: GlobalPoint,
|
||||
p3: GlobalPoint,
|
||||
offsetDist: number,
|
||||
steps = 20,
|
||||
) {
|
||||
const offsetPoints = [];
|
||||
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const t = i / steps;
|
||||
const c = curve(p0, p1, p2, p3);
|
||||
const point = bezierEquation(c, t);
|
||||
const tangent = vectorNormalize(curveTangent(c, t));
|
||||
const normal = vectorNormal(tangent);
|
||||
|
||||
offsetPoints.push(pointFromVector(vectorScale(normal, offsetDist), point));
|
||||
}
|
||||
|
||||
return offsetPoints;
|
||||
}
|
||||
|
||||
function offsetQuadraticBezier(
|
||||
p0: GlobalPoint,
|
||||
p1: GlobalPoint,
|
||||
p2: GlobalPoint,
|
||||
offsetDist: number,
|
||||
steps = 20,
|
||||
) {
|
||||
const offsetPoints = [];
|
||||
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const t = i / steps;
|
||||
const t1 = 1 - t;
|
||||
const point = pointFrom<GlobalPoint>(
|
||||
t1 * t1 * p0[0] + 2 * t1 * t * p1[0] + t * t * p2[0],
|
||||
t1 * t1 * p0[1] + 2 * t1 * t * p1[1] + t * t * p2[1],
|
||||
);
|
||||
const tangentX = 2 * (1 - t) * (p1[0] - p0[0]) + 2 * t * (p2[0] - p1[0]);
|
||||
const tangentY = 2 * (1 - t) * (p1[1] - p0[1]) + 2 * t * (p2[1] - p1[1]);
|
||||
const tangent = vectorNormalize(vector(tangentX, tangentY));
|
||||
const normal = vectorNormal(tangent);
|
||||
|
||||
offsetPoints.push(pointFromVector(vectorScale(normal, offsetDist), point));
|
||||
}
|
||||
|
||||
return offsetPoints;
|
||||
}
|
||||
|
@ -1,23 +1,22 @@
|
||||
import oc from "open-color";
|
||||
import {
|
||||
pointFrom,
|
||||
type GlobalPoint,
|
||||
type LocalPoint,
|
||||
type Radians,
|
||||
} from "@excalidraw/math";
|
||||
import oc from "open-color";
|
||||
|
||||
import {
|
||||
arrayToMap,
|
||||
DEFAULT_TRANSFORM_HANDLE_SPACING,
|
||||
FRAME_STYLE,
|
||||
THEME,
|
||||
arrayToMap,
|
||||
invariant,
|
||||
THEME,
|
||||
throttleRAF,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
BINDING_HIGHLIGHT_OFFSET,
|
||||
BINDING_HIGHLIGHT_THICKNESS,
|
||||
FIXED_BINDING_DISTANCE,
|
||||
maxBindingGap,
|
||||
} from "@excalidraw/element/binding";
|
||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||
@ -35,14 +34,12 @@ import {
|
||||
isTextElement,
|
||||
} from "@excalidraw/element/typeChecks";
|
||||
|
||||
import { getCornerRadius } from "@excalidraw/element/shapes";
|
||||
|
||||
import { renderSelectionElement } from "@excalidraw/element/renderElement";
|
||||
|
||||
import {
|
||||
isSelectedViaGroup,
|
||||
getSelectedGroupIds,
|
||||
getElementsInGroup,
|
||||
getSelectedGroupIds,
|
||||
isSelectedViaGroup,
|
||||
selectGroupsFromGivenElements,
|
||||
} from "@excalidraw/element/groups";
|
||||
|
||||
@ -86,8 +83,12 @@ import { getClientColor, renderRemoteCursors } from "../clients";
|
||||
|
||||
import {
|
||||
bootstrapCanvas,
|
||||
drawHighlightForDiamondWithRotation,
|
||||
drawHighlightForRectWithRotation,
|
||||
fillCircle,
|
||||
getNormalizedCanvasDimensions,
|
||||
strokeEllipseWithRotation,
|
||||
strokeRectWithRotation,
|
||||
} from "./helpers";
|
||||
|
||||
import type {
|
||||
@ -160,57 +161,6 @@ const highlightPoint = <Point extends LocalPoint | GlobalPoint>(
|
||||
);
|
||||
};
|
||||
|
||||
const strokeRectWithRotation = (
|
||||
context: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
cx: number,
|
||||
cy: number,
|
||||
angle: number,
|
||||
fill: boolean = false,
|
||||
/** should account for zoom */
|
||||
radius: number = 0,
|
||||
) => {
|
||||
context.save();
|
||||
context.translate(cx, cy);
|
||||
context.rotate(angle);
|
||||
if (fill) {
|
||||
context.fillRect(x - cx, y - cy, width, height);
|
||||
}
|
||||
if (radius && context.roundRect) {
|
||||
context.beginPath();
|
||||
context.roundRect(x - cx, y - cy, width, height, radius);
|
||||
context.stroke();
|
||||
context.closePath();
|
||||
} else {
|
||||
context.strokeRect(x - cx, y - cy, width, height);
|
||||
}
|
||||
context.restore();
|
||||
};
|
||||
|
||||
const strokeDiamondWithRotation = (
|
||||
context: CanvasRenderingContext2D,
|
||||
width: number,
|
||||
height: number,
|
||||
cx: number,
|
||||
cy: number,
|
||||
angle: number,
|
||||
) => {
|
||||
context.save();
|
||||
context.translate(cx, cy);
|
||||
context.rotate(angle);
|
||||
context.beginPath();
|
||||
context.moveTo(0, height / 2);
|
||||
context.lineTo(width / 2, 0);
|
||||
context.lineTo(0, -height / 2);
|
||||
context.lineTo(-width / 2, 0);
|
||||
context.closePath();
|
||||
context.stroke();
|
||||
context.restore();
|
||||
};
|
||||
|
||||
const renderSingleLinearPoint = <Point extends GlobalPoint | LocalPoint>(
|
||||
context: CanvasRenderingContext2D,
|
||||
appState: InteractiveCanvasAppState,
|
||||
@ -237,19 +187,6 @@ const renderSingleLinearPoint = <Point extends GlobalPoint | LocalPoint>(
|
||||
);
|
||||
};
|
||||
|
||||
const strokeEllipseWithRotation = (
|
||||
context: CanvasRenderingContext2D,
|
||||
width: number,
|
||||
height: number,
|
||||
cx: number,
|
||||
cy: number,
|
||||
angle: number,
|
||||
) => {
|
||||
context.beginPath();
|
||||
context.ellipse(cx, cy, width / 2, height / 2, angle, 0, Math.PI * 2);
|
||||
context.stroke();
|
||||
};
|
||||
|
||||
const renderBindingHighlightForBindableElement = (
|
||||
context: CanvasRenderingContext2D,
|
||||
element: ExcalidrawBindableElement,
|
||||
@ -261,16 +198,10 @@ const renderBindingHighlightForBindableElement = (
|
||||
const height = y2 - y1;
|
||||
|
||||
context.strokeStyle = "rgba(0,0,0,.05)";
|
||||
// When zooming out, make line width greater for visibility
|
||||
const zoomValue = zoom.value < 1 ? zoom.value : 1;
|
||||
context.lineWidth = BINDING_HIGHLIGHT_THICKNESS / zoomValue;
|
||||
// To ensure the binding highlight doesn't overlap the element itself
|
||||
const padding = context.lineWidth / 2 + BINDING_HIGHLIGHT_OFFSET;
|
||||
context.fillStyle = "rgba(0,0,0,.05)";
|
||||
|
||||
const radius = getCornerRadius(
|
||||
Math.min(element.width, element.height),
|
||||
element,
|
||||
);
|
||||
// To ensure the binding highlight doesn't overlap the element itself
|
||||
const padding = maxBindingGap(element, element.width, element.height, zoom);
|
||||
|
||||
switch (element.type) {
|
||||
case "rectangle":
|
||||
@ -280,37 +211,20 @@ const renderBindingHighlightForBindableElement = (
|
||||
case "embeddable":
|
||||
case "frame":
|
||||
case "magicframe":
|
||||
strokeRectWithRotation(
|
||||
context,
|
||||
x1 - padding,
|
||||
y1 - padding,
|
||||
width + padding * 2,
|
||||
height + padding * 2,
|
||||
x1 + width / 2,
|
||||
y1 + height / 2,
|
||||
element.angle,
|
||||
undefined,
|
||||
radius,
|
||||
);
|
||||
drawHighlightForRectWithRotation(context, element, padding);
|
||||
break;
|
||||
case "diamond":
|
||||
const side = Math.hypot(width, height);
|
||||
const wPadding = (padding * side) / height;
|
||||
const hPadding = (padding * side) / width;
|
||||
strokeDiamondWithRotation(
|
||||
context,
|
||||
width + wPadding * 2,
|
||||
height + hPadding * 2,
|
||||
x1 + width / 2,
|
||||
y1 + height / 2,
|
||||
element.angle,
|
||||
);
|
||||
drawHighlightForDiamondWithRotation(context, padding, element);
|
||||
break;
|
||||
case "ellipse":
|
||||
context.lineWidth =
|
||||
maxBindingGap(element, element.width, element.height, zoom) -
|
||||
FIXED_BINDING_DISTANCE;
|
||||
|
||||
strokeEllipseWithRotation(
|
||||
context,
|
||||
width + padding * 2,
|
||||
height + padding * 2,
|
||||
width + padding + FIXED_BINDING_DISTANCE,
|
||||
height + padding + FIXED_BINDING_DISTANCE,
|
||||
x1 + width / 2,
|
||||
y1 + height / 2,
|
||||
element.angle,
|
||||
|
@ -2,6 +2,7 @@ import type { Bounds } from "@excalidraw/element/bounds";
|
||||
|
||||
import { isPoint, pointDistance, pointFrom } from "./point";
|
||||
import { rectangle, rectangleIntersectLineSegment } from "./rectangle";
|
||||
import { vector } from "./vector";
|
||||
|
||||
import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types";
|
||||
|
||||
@ -82,7 +83,7 @@ function solve(
|
||||
return [t0, s0];
|
||||
}
|
||||
|
||||
const bezierEquation = <Point extends GlobalPoint | LocalPoint>(
|
||||
export const bezierEquation = <Point extends GlobalPoint | LocalPoint>(
|
||||
c: Curve<Point>,
|
||||
t: number,
|
||||
) =>
|
||||
@ -274,6 +275,26 @@ export function isCurve<P extends GlobalPoint | LocalPoint>(
|
||||
);
|
||||
}
|
||||
|
||||
export function curveTangent<Point extends GlobalPoint | LocalPoint>(
|
||||
[p0, p1, p2, p3]: Curve<Point>,
|
||||
t: number,
|
||||
) {
|
||||
return vector(
|
||||
-3 * (1 - t) * (1 - t) * p0[0] +
|
||||
3 * (1 - t) * (1 - t) * p1[0] -
|
||||
6 * t * (1 - t) * p1[0] -
|
||||
3 * t * t * p2[0] +
|
||||
6 * t * (1 - t) * p2[0] +
|
||||
3 * t * t * p3[0],
|
||||
-3 * (1 - t) * (1 - t) * p0[1] +
|
||||
3 * (1 - t) * (1 - t) * p1[1] -
|
||||
6 * t * (1 - t) * p1[1] -
|
||||
3 * t * t * p2[1] +
|
||||
6 * t * (1 - t) * p2[1] +
|
||||
3 * t * t * p3[1],
|
||||
);
|
||||
}
|
||||
|
||||
function curveBounds<Point extends GlobalPoint | LocalPoint>(
|
||||
c: Curve<Point>,
|
||||
): Bounds {
|
||||
|
@ -143,3 +143,8 @@ export const vectorNormalize = (v: Vector): Vector => {
|
||||
|
||||
return vector(v[0] / m, v[1] / m);
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate the right-hand normal of the vector.
|
||||
*/
|
||||
export const vectorNormal = (v: Vector): Vector => vector(v[1], -v[0]);
|
||||
|
Loading…
x
Reference in New Issue
Block a user