mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-04-14 16:40:58 -04:00
Compare commits
5 commits
c72f66d8bd
...
bb9768b267
Author | SHA1 | Date | |
---|---|---|---|
|
bb9768b267 | ||
|
01304aac49 | ||
|
dff69e9191 | ||
|
6fc85022ae | ||
|
67595e0f5a |
18 changed files with 261 additions and 143 deletions
32
README.md
32
README.md
|
@ -52,24 +52,24 @@
|
|||
</figure>
|
||||
</div>
|
||||
|
||||
## Features
|
||||
|
||||
The Excalidraw editor (npm package) supports:
|
||||
##Features
|
||||
The Excalidraw editor (npm package) offers an array of powerful features:
|
||||
|
||||
- 💯 Free & open-source.
|
||||
- 🎨 Infinite, canvas-based whiteboard.
|
||||
- ✍️ Hand-drawn like style.
|
||||
- 🌓 Dark mode.
|
||||
- 🏗️ Customizable.
|
||||
- 📷 Image support.
|
||||
- 😀 Shape libraries support.
|
||||
- 👅 Localization (i18n) support.
|
||||
- 🖼️ Export to PNG, SVG & clipboard.
|
||||
- 💾 Open format - export drawings as an `.excalidraw` json file.
|
||||
- ⚒️ Wide range of tools - rectangle, circle, diamond, arrow, line, free-draw, eraser...
|
||||
- ➡️ Arrow-binding & labeled arrows.
|
||||
- 🔙 Undo / Redo.
|
||||
- 🔍 Zoom and panning support.
|
||||
💯 Free & Open-Source: Enjoy the freedom of an open-source tool.
|
||||
🎨 Infinite Canvas: Let your ideas flow freely on an infinite canvas.
|
||||
✍️ Hand-Drawn Style: Embrace a hand-drawn-like style for your creations.
|
||||
🌓 Dark Mode: Switch seamlessly between light and dark modes.
|
||||
🏗️ Customizable: Tailor the editor to fit your needs.
|
||||
📷 Image Support: Easily incorporate images into your drawings.
|
||||
😀 Shape Libraries: Access a wide range of shapes to enhance your designs.
|
||||
👅 Localization (i18n) Support: Use the editor in your preferred language.
|
||||
🖼️ Export Options: Export your creations to PNG, SVG, or clipboard with ease.
|
||||
💾 Open Format: Save and share your drawings as .excalidraw json files.
|
||||
⚒️ Wide Range of Tools: Choose from various tools including rectangle, circle, diamond, arrow, line, free-draw, and eraser.
|
||||
➡️ Arrow Binding & Labeled Arrows: Create arrows with labels for clear communication.
|
||||
🔙 Undo / Redo: Easily correct mistakes or backtrack as needed.
|
||||
🔍 Zoom and Panning: Get a closer look and navigate your canvas effortlessly.
|
||||
|
||||
## Excalidraw.com
|
||||
|
||||
|
|
|
@ -112,6 +112,7 @@ export const YOUTUBE_STATES = {
|
|||
export const ENV = {
|
||||
TEST: "test",
|
||||
DEVELOPMENT: "development",
|
||||
PRODUCTION: "production",
|
||||
};
|
||||
|
||||
export const CLASSES = {
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { average } from "@excalidraw/math";
|
||||
import { average, pointFrom, type GlobalPoint } from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
ExcalidrawBindableElement,
|
||||
FontFamilyValues,
|
||||
FontString,
|
||||
ExcalidrawElement,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type {
|
||||
|
@ -738,6 +739,8 @@ export const isTestEnv = () => import.meta.env.MODE === ENV.TEST;
|
|||
|
||||
export const isDevEnv = () => import.meta.env.MODE === ENV.DEVELOPMENT;
|
||||
|
||||
export const isProdEnv = () => import.meta.env.MODE === ENV.PRODUCTION;
|
||||
|
||||
export const isServerEnv = () =>
|
||||
typeof process !== "undefined" && !!process?.env?.NODE_ENV;
|
||||
|
||||
|
@ -1201,3 +1204,17 @@ export const escapeDoubleQuotes = (str: string) => {
|
|||
|
||||
export const castArray = <T>(value: T | T[]): T[] =>
|
||||
Array.isArray(value) ? value : [value];
|
||||
|
||||
export const elementCenterPoint = (
|
||||
element: ExcalidrawElement,
|
||||
xOffset: number = 0,
|
||||
yOffset: number = 0,
|
||||
) => {
|
||||
const { x, y, width, height } = element;
|
||||
|
||||
const centerXPoint = x + width / 2 + xOffset;
|
||||
|
||||
const centerYPoint = y + height / 2 + yOffset;
|
||||
|
||||
return pointFrom<GlobalPoint>(centerXPoint, centerYPoint);
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
invariant,
|
||||
isDevEnv,
|
||||
isTestEnv,
|
||||
elementCenterPoint,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
|
@ -904,13 +905,7 @@ export const getHeadingForElbowArrowSnap = (
|
|||
|
||||
if (!distance) {
|
||||
return vectorToHeading(
|
||||
vectorFromPoint(
|
||||
p,
|
||||
pointFrom<GlobalPoint>(
|
||||
bindableElement.x + bindableElement.width / 2,
|
||||
bindableElement.y + bindableElement.height / 2,
|
||||
),
|
||||
),
|
||||
vectorFromPoint(p, elementCenterPoint(bindableElement)),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1040,10 +1035,7 @@ export const avoidRectangularCorner = (
|
|||
element: ExcalidrawBindableElement,
|
||||
p: GlobalPoint,
|
||||
): GlobalPoint => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
const nonRotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
||||
|
||||
if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) {
|
||||
|
@ -1140,10 +1132,9 @@ export const snapToMid = (
|
|||
tolerance: number = 0.05,
|
||||
): GlobalPoint => {
|
||||
const { x, y, width, height, angle } = element;
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
x + width / 2 - 0.1,
|
||||
y + height / 2 - 0.1,
|
||||
);
|
||||
|
||||
const center = elementCenterPoint(element, -0.1, -0.1);
|
||||
|
||||
const nonRotated = pointRotateRads(p, center, -angle as Radians);
|
||||
|
||||
// snap-to-center point is adaptive to element size, but we don't want to go
|
||||
|
@ -1228,10 +1219,7 @@ const updateBoundPoint = (
|
|||
startOrEnd === "startBinding" ? "start" : "end",
|
||||
elementsMap,
|
||||
).fixedPoint;
|
||||
const globalMidPoint = pointFrom<GlobalPoint>(
|
||||
bindableElement.x + bindableElement.width / 2,
|
||||
bindableElement.y + bindableElement.height / 2,
|
||||
);
|
||||
const globalMidPoint = elementCenterPoint(bindableElement);
|
||||
const global = pointFrom<GlobalPoint>(
|
||||
bindableElement.x + fixedPoint[0] * bindableElement.width,
|
||||
bindableElement.y + fixedPoint[1] * bindableElement.height,
|
||||
|
@ -1275,10 +1263,7 @@ const updateBoundPoint = (
|
|||
elementsMap,
|
||||
);
|
||||
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
bindableElement.x + bindableElement.width / 2,
|
||||
bindableElement.y + bindableElement.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(bindableElement);
|
||||
const interceptorLength =
|
||||
pointDistance(adjacentPoint, edgePointAbsolute) +
|
||||
pointDistance(adjacentPoint, center) +
|
||||
|
@ -1771,10 +1756,7 @@ const determineFocusDistance = (
|
|||
// Another point on the line, in absolute coordinates (closer to element)
|
||||
b: GlobalPoint,
|
||||
): number => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
|
||||
if (pointsEqual(a, b)) {
|
||||
return 0;
|
||||
|
@ -1904,10 +1886,7 @@ const determineFocusPoint = (
|
|||
focus: number,
|
||||
adjacentPoint: GlobalPoint,
|
||||
): GlobalPoint => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
|
||||
if (focus === 0) {
|
||||
return center;
|
||||
|
@ -2338,10 +2317,7 @@ export const getGlobalFixedPointForBindableElement = (
|
|||
element.x + element.width * fixedX,
|
||||
element.y + element.height * fixedY,
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
),
|
||||
elementCenterPoint(element),
|
||||
element.angle,
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { isTransparent } from "@excalidraw/common";
|
||||
import { isTransparent, elementCenterPoint } from "@excalidraw/common";
|
||||
import {
|
||||
curveIntersectLineSegment,
|
||||
isPointWithinBounds,
|
||||
|
@ -16,7 +16,7 @@ import {
|
|||
} from "@excalidraw/math/ellipse";
|
||||
|
||||
import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision";
|
||||
import { getPolygonShape } from "@excalidraw/utils/shape";
|
||||
import { type GeometricShape, getPolygonShape } from "@excalidraw/utils/shape";
|
||||
|
||||
import type {
|
||||
GlobalPoint,
|
||||
|
@ -26,8 +26,6 @@ import type {
|
|||
Radians,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import type { GeometricShape } from "@excalidraw/utils/shape";
|
||||
|
||||
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { getBoundTextShape, isPathALoop } from "./shapes";
|
||||
|
@ -191,10 +189,7 @@ const intersectRectanguloidWithLineSegment = (
|
|||
l: LineSegment<GlobalPoint>,
|
||||
offset: number = 0,
|
||||
): GlobalPoint[] => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
||||
// instead. It's all the same distance-wise.
|
||||
const rotatedA = pointRotateRads<GlobalPoint>(
|
||||
|
@ -253,10 +248,7 @@ const intersectDiamondWithLineSegment = (
|
|||
l: LineSegment<GlobalPoint>,
|
||||
offset: number = 0,
|
||||
): GlobalPoint[] => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
|
||||
// Rotate the point to the inverse direction to simulate the rotated diamond
|
||||
// points. It's all the same distance-wise.
|
||||
|
@ -304,10 +296,7 @@ const intersectEllipseWithLineSegment = (
|
|||
l: LineSegment<GlobalPoint>,
|
||||
offset: number = 0,
|
||||
): GlobalPoint[] => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
|
||||
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
|
||||
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
|
||||
|
|
|
@ -14,6 +14,8 @@ import {
|
|||
} from "@excalidraw/math";
|
||||
import { type Point } from "points-on-curve";
|
||||
|
||||
import { elementCenterPoint } from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
getElementAbsoluteCoords,
|
||||
getResizedElementAbsoluteCoords,
|
||||
|
@ -61,7 +63,7 @@ export const cropElement = (
|
|||
|
||||
const rotatedPointer = pointRotateRads(
|
||||
pointFrom(pointerX, pointerY),
|
||||
pointFrom(element.x + element.width / 2, element.y + element.height / 2),
|
||||
elementCenterPoint(element),
|
||||
-element.angle as Radians,
|
||||
);
|
||||
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import {
|
||||
curvePointDistance,
|
||||
distanceToLineSegment,
|
||||
pointFrom,
|
||||
pointRotateRads,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import { ellipse, ellipseDistanceFromPoint } from "@excalidraw/math/ellipse";
|
||||
|
||||
import { elementCenterPoint } from "@excalidraw/common";
|
||||
|
||||
import type { GlobalPoint, Radians } from "@excalidraw/math";
|
||||
|
||||
import {
|
||||
|
@ -53,10 +54,7 @@ const distanceToRectanguloidElement = (
|
|||
element: ExcalidrawRectanguloidElement,
|
||||
p: GlobalPoint,
|
||||
) => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
||||
// instead. It's all the same distance-wise.
|
||||
const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
||||
|
@ -84,10 +82,7 @@ const distanceToDiamondElement = (
|
|||
element: ExcalidrawDiamondElement,
|
||||
p: GlobalPoint,
|
||||
): number => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
|
||||
// Rotate the point to the inverse direction to simulate the rotated diamond
|
||||
// points. It's all the same distance-wise.
|
||||
|
@ -115,10 +110,7 @@ const distanceToEllipseElement = (
|
|||
element: ExcalidrawEllipseElement,
|
||||
p: GlobalPoint,
|
||||
): number => {
|
||||
const center = pointFrom(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
return ellipseDistanceFromPoint(
|
||||
// Instead of rotating the ellipse, rotate the point to the inverse angle
|
||||
pointRotateRads(p, center, -element.angle as Radians),
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
LINE_CONFIRM_THRESHOLD,
|
||||
ROUNDNESS,
|
||||
invariant,
|
||||
elementCenterPoint,
|
||||
} from "@excalidraw/common";
|
||||
import {
|
||||
isPoint,
|
||||
|
@ -297,7 +298,7 @@ export const aabbForElement = (
|
|||
midY: element.y + element.height / 2,
|
||||
};
|
||||
|
||||
const center = pointFrom(bbox.midX, bbox.midY);
|
||||
const center = elementCenterPoint(element);
|
||||
const [topLeftX, topLeftY] = pointRotateRads(
|
||||
pointFrom(bbox.minX, bbox.minY),
|
||||
center,
|
||||
|
|
|
@ -6,6 +6,8 @@ import {
|
|||
TEXT_ALIGN,
|
||||
VERTICAL_ALIGN,
|
||||
getFontString,
|
||||
isProdEnv,
|
||||
invariant,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
|
@ -26,6 +28,8 @@ import {
|
|||
isTextElement,
|
||||
} from "./typeChecks";
|
||||
|
||||
import type { Radians } from "../../math/src";
|
||||
|
||||
import type { MaybeTransformHandleType } from "./transformHandles";
|
||||
import type {
|
||||
ElementsMap,
|
||||
|
@ -44,13 +48,25 @@ export const redrawTextBoundingBox = (
|
|||
informMutation = true,
|
||||
) => {
|
||||
let maxWidth = undefined;
|
||||
|
||||
if (!isProdEnv()) {
|
||||
invariant(
|
||||
!container || !isArrowElement(container) || textElement.angle === 0,
|
||||
"text element angle must be 0 if bound to arrow container",
|
||||
);
|
||||
}
|
||||
|
||||
const boundTextUpdates = {
|
||||
x: textElement.x,
|
||||
y: textElement.y,
|
||||
text: textElement.text,
|
||||
width: textElement.width,
|
||||
height: textElement.height,
|
||||
angle: container?.angle ?? textElement.angle,
|
||||
angle: (container
|
||||
? isArrowElement(container)
|
||||
? 0
|
||||
: container.angle
|
||||
: textElement.angle) as Radians,
|
||||
};
|
||||
|
||||
boundTextUpdates.text = textElement.text;
|
||||
|
@ -335,7 +351,10 @@ export const getTextElementAngle = (
|
|||
textElement: ExcalidrawTextElement,
|
||||
container: ExcalidrawTextContainer | null,
|
||||
) => {
|
||||
if (!container || isArrowElement(container)) {
|
||||
if (isArrowElement(container)) {
|
||||
return 0;
|
||||
}
|
||||
if (!container) {
|
||||
return textElement.angle;
|
||||
}
|
||||
return container.angle;
|
||||
|
|
|
@ -10,6 +10,8 @@ import {
|
|||
type GlobalPoint,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import { elementCenterPoint } from "@excalidraw/common";
|
||||
|
||||
import type { Curve, LineSegment } from "@excalidraw/math";
|
||||
|
||||
import { getCornerRadius } from "./shapes";
|
||||
|
@ -68,10 +70,7 @@ export function deconstructRectanguloidElement(
|
|||
return [sides, []];
|
||||
}
|
||||
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
|
||||
const r = rectangle(
|
||||
pointFrom(element.x, element.y),
|
||||
|
@ -254,10 +253,7 @@ export function deconstructDiamondElement(
|
|||
return [[topRight, bottomRight, bottomLeft, topLeft], []];
|
||||
}
|
||||
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
|
||||
const [top, right, bottom, left]: GlobalPoint[] = [
|
||||
pointFrom(element.x + topX, element.y + topY),
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
|
||||
import {
|
||||
hasBoundTextElement,
|
||||
isArrowElement,
|
||||
isTextBindableContainer,
|
||||
isTextElement,
|
||||
isUsingAdaptiveRadius,
|
||||
|
@ -46,6 +47,8 @@ import { CaptureUpdateAction } from "../store";
|
|||
|
||||
import { register } from "./register";
|
||||
|
||||
import type { Radians } from "../../math/src";
|
||||
|
||||
import type { AppState } from "../types";
|
||||
|
||||
export const actionUnbindText = register({
|
||||
|
@ -155,6 +158,7 @@ export const actionBindText = register({
|
|||
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||
textAlign: TEXT_ALIGN.CENTER,
|
||||
autoResize: true,
|
||||
angle: (isArrowElement(container) ? 0 : container?.angle ?? 0) as Radians,
|
||||
});
|
||||
mutateElement(container, {
|
||||
boundElements: (container.boundElements || []).concat({
|
||||
|
|
|
@ -191,11 +191,7 @@ export class AnimatedTrail implements Trail {
|
|||
});
|
||||
|
||||
const stroke = this.trailAnimation
|
||||
? _stroke.slice(
|
||||
// slicing from 6th point to get rid of the initial notch type of thing
|
||||
Math.min(_stroke.length, 6),
|
||||
_stroke.length / 2,
|
||||
)
|
||||
? _stroke.slice(0, _stroke.length / 2)
|
||||
: _stroke;
|
||||
|
||||
return getSvgPathFromStroke(stroke, true);
|
||||
|
|
|
@ -5352,37 +5352,37 @@ class App extends React.Component<AppProps, AppState> {
|
|||
y: sceneY,
|
||||
});
|
||||
|
||||
const element = existingTextElement
|
||||
? existingTextElement
|
||||
: newTextElement({
|
||||
x: parentCenterPosition
|
||||
? parentCenterPosition.elementCenterX
|
||||
: sceneX,
|
||||
y: parentCenterPosition
|
||||
? parentCenterPosition.elementCenterY
|
||||
: sceneY,
|
||||
strokeColor: this.state.currentItemStrokeColor,
|
||||
backgroundColor: this.state.currentItemBackgroundColor,
|
||||
fillStyle: this.state.currentItemFillStyle,
|
||||
strokeWidth: this.state.currentItemStrokeWidth,
|
||||
strokeStyle: this.state.currentItemStrokeStyle,
|
||||
roughness: this.state.currentItemRoughness,
|
||||
opacity: this.state.currentItemOpacity,
|
||||
text: "",
|
||||
fontSize,
|
||||
fontFamily,
|
||||
textAlign: parentCenterPosition
|
||||
? "center"
|
||||
: this.state.currentItemTextAlign,
|
||||
verticalAlign: parentCenterPosition
|
||||
? VERTICAL_ALIGN.MIDDLE
|
||||
: DEFAULT_VERTICAL_ALIGN,
|
||||
containerId: shouldBindToContainer ? container?.id : undefined,
|
||||
groupIds: container?.groupIds ?? [],
|
||||
lineHeight,
|
||||
angle: container?.angle ?? (0 as Radians),
|
||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||
});
|
||||
const element =
|
||||
existingTextElement ||
|
||||
newTextElement({
|
||||
x: parentCenterPosition ? parentCenterPosition.elementCenterX : sceneX,
|
||||
y: parentCenterPosition ? parentCenterPosition.elementCenterY : sceneY,
|
||||
strokeColor: this.state.currentItemStrokeColor,
|
||||
backgroundColor: this.state.currentItemBackgroundColor,
|
||||
fillStyle: this.state.currentItemFillStyle,
|
||||
strokeWidth: this.state.currentItemStrokeWidth,
|
||||
strokeStyle: this.state.currentItemStrokeStyle,
|
||||
roughness: this.state.currentItemRoughness,
|
||||
opacity: this.state.currentItemOpacity,
|
||||
text: "",
|
||||
fontSize,
|
||||
fontFamily,
|
||||
textAlign: parentCenterPosition
|
||||
? "center"
|
||||
: this.state.currentItemTextAlign,
|
||||
verticalAlign: parentCenterPosition
|
||||
? VERTICAL_ALIGN.MIDDLE
|
||||
: DEFAULT_VERTICAL_ALIGN,
|
||||
containerId: shouldBindToContainer ? container?.id : undefined,
|
||||
groupIds: container?.groupIds ?? [],
|
||||
lineHeight,
|
||||
angle: container
|
||||
? isArrowElement(container)
|
||||
? (0 as Radians)
|
||||
: container.angle
|
||||
: (0 as Radians),
|
||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||
});
|
||||
|
||||
if (!existingTextElement && shouldBindToContainer && container) {
|
||||
mutateElement(container, {
|
||||
|
|
|
@ -439,7 +439,7 @@ const repairContainerElement = (
|
|||
// if defined, lest boundElements is stale
|
||||
!boundElement.containerId
|
||||
) {
|
||||
(boundElement as Mutable<ExcalidrawTextElement>).containerId =
|
||||
(boundElement as Mutable<typeof boundElement>).containerId =
|
||||
container.id;
|
||||
}
|
||||
}
|
||||
|
@ -464,6 +464,10 @@ const repairBoundElement = (
|
|||
? elementsMap.get(boundElement.containerId)
|
||||
: null;
|
||||
|
||||
(boundElement as Mutable<typeof boundElement>).angle = (
|
||||
isArrowElement(container) ? 0 : container?.angle ?? 0
|
||||
) as Radians;
|
||||
|
||||
if (!container) {
|
||||
boundElement.containerId = null;
|
||||
return;
|
||||
|
|
|
@ -2,9 +2,9 @@ import { simplify } from "points-on-curve";
|
|||
|
||||
import {
|
||||
polygonFromPoints,
|
||||
polygonIncludesPoint,
|
||||
lineSegment,
|
||||
lineSegmentIntersectionPoints,
|
||||
polygonIncludesPointNonZero,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import type { GlobalPoint, LineSegment } from "@excalidraw/math/types";
|
||||
|
@ -35,8 +35,6 @@ export const getLassoSelectedElementIds = (input: {
|
|||
if (simplifyDistance) {
|
||||
path = simplify(lassoPath, simplifyDistance) as GlobalPoint[];
|
||||
}
|
||||
// close the path to form a polygon for enclosure check
|
||||
const closedPath = polygonFromPoints(path);
|
||||
// as the path might not enclose a shape anymore, clear before checking
|
||||
enclosedElements.clear();
|
||||
for (const element of elements) {
|
||||
|
@ -44,15 +42,11 @@ export const getLassoSelectedElementIds = (input: {
|
|||
!intersectedElements.has(element.id) &&
|
||||
!enclosedElements.has(element.id)
|
||||
) {
|
||||
const enclosed = enclosureTest(closedPath, element, elementsSegments);
|
||||
const enclosed = enclosureTest(path, element, elementsSegments);
|
||||
if (enclosed) {
|
||||
enclosedElements.add(element.id);
|
||||
} else {
|
||||
const intersects = intersectionTest(
|
||||
closedPath,
|
||||
element,
|
||||
elementsSegments,
|
||||
);
|
||||
const intersects = intersectionTest(path, element, elementsSegments);
|
||||
if (intersects) {
|
||||
intersectedElements.add(element.id);
|
||||
}
|
||||
|
@ -79,7 +73,9 @@ const enclosureTest = (
|
|||
}
|
||||
|
||||
return segments.some((segment) => {
|
||||
return segment.some((point) => polygonIncludesPoint(point, lassoPolygon));
|
||||
return segment.some((point) =>
|
||||
polygonIncludesPointNonZero(point, lassoPolygon),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
isTextElement,
|
||||
isFrameLikeElement,
|
||||
} from "@excalidraw/element/typeChecks";
|
||||
import { KEYS, arrayToMap } from "@excalidraw/common";
|
||||
import { KEYS, arrayToMap, elementCenterPoint } from "@excalidraw/common";
|
||||
|
||||
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
|
||||
|
||||
|
@ -151,7 +151,7 @@ export class Keyboard {
|
|||
const getElementPointForSelection = (
|
||||
element: ExcalidrawElement,
|
||||
): GlobalPoint => {
|
||||
const { x, y, width, height, angle } = element;
|
||||
const { x, y, width, angle } = element;
|
||||
const target = pointFrom<GlobalPoint>(
|
||||
x +
|
||||
(isLinearElement(element) || isFreeDrawElement(element) ? 0 : width / 2),
|
||||
|
@ -166,7 +166,7 @@ const getElementPointForSelection = (
|
|||
(bounds[1] + bounds[3]) / 2,
|
||||
);
|
||||
} else {
|
||||
center = pointFrom(x + width / 2, y + height / 2);
|
||||
center = elementCenterPoint(element);
|
||||
}
|
||||
|
||||
if (isTextElement(element)) {
|
||||
|
|
|
@ -31,6 +31,7 @@ import {
|
|||
mockBoundingClientRect,
|
||||
restoreOriginalGetBoundingClientRect,
|
||||
} from "../tests/test-utils";
|
||||
import { actionBindText } from "../actions";
|
||||
|
||||
unmountComponent();
|
||||
|
||||
|
@ -1568,5 +1569,101 @@ describe("textWysiwyg", () => {
|
|||
expect(text.containerId).toBe(null);
|
||||
expect(text.text).toBe("Excalidraw");
|
||||
});
|
||||
|
||||
it("should reset the text element angle to the container's when binding to rotated non-arrow container", async () => {
|
||||
const text = API.createElement({
|
||||
type: "text",
|
||||
text: "Hello World!",
|
||||
angle: 45,
|
||||
});
|
||||
const rectangle = API.createElement({
|
||||
type: "rectangle",
|
||||
width: 90,
|
||||
height: 75,
|
||||
angle: 30,
|
||||
});
|
||||
|
||||
API.setElements([rectangle, text]);
|
||||
|
||||
API.setSelectedElements([rectangle, text]);
|
||||
|
||||
h.app.actionManager.executeAction(actionBindText);
|
||||
|
||||
expect(text.angle).toBe(30);
|
||||
expect(rectangle.angle).toBe(30);
|
||||
});
|
||||
|
||||
it("should reset the text element angle to 0 when binding to rotated arrow container", async () => {
|
||||
const text = API.createElement({
|
||||
type: "text",
|
||||
text: "Hello World!",
|
||||
angle: 45,
|
||||
});
|
||||
const arrow = API.createElement({
|
||||
type: "arrow",
|
||||
width: 90,
|
||||
height: 75,
|
||||
angle: 30,
|
||||
});
|
||||
|
||||
API.setElements([arrow, text]);
|
||||
|
||||
API.setSelectedElements([arrow, text]);
|
||||
|
||||
h.app.actionManager.executeAction(actionBindText);
|
||||
|
||||
expect(text.angle).toBe(0);
|
||||
expect(arrow.angle).toBe(30);
|
||||
});
|
||||
|
||||
it("should keep the text label at 0 degrees when used as an arrow label", async () => {
|
||||
const arrow = API.createElement({
|
||||
type: "arrow",
|
||||
width: 90,
|
||||
height: 75,
|
||||
angle: 30,
|
||||
});
|
||||
|
||||
API.setElements([arrow]);
|
||||
API.setSelectedElements([arrow]);
|
||||
|
||||
mouse.doubleClickAt(
|
||||
arrow.x + arrow.width / 2,
|
||||
arrow.y + arrow.height / 2,
|
||||
);
|
||||
|
||||
const editor = await getTextEditor(textEditorSelector, true);
|
||||
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
|
||||
Keyboard.exitTextEditor(editor);
|
||||
|
||||
expect(h.elements[1].angle).toBe(0);
|
||||
});
|
||||
|
||||
it("should keep the text label at the same degrees when used as a non-arrow label", async () => {
|
||||
const rectangle = API.createElement({
|
||||
type: "rectangle",
|
||||
width: 90,
|
||||
height: 75,
|
||||
angle: 30,
|
||||
});
|
||||
|
||||
API.setElements([rectangle]);
|
||||
API.setSelectedElements([rectangle]);
|
||||
|
||||
mouse.doubleClickAt(
|
||||
rectangle.x + rectangle.width / 2,
|
||||
rectangle.y + rectangle.height / 2,
|
||||
);
|
||||
|
||||
const editor = await getTextEditor(textEditorSelector, true);
|
||||
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
|
||||
Keyboard.exitTextEditor(editor);
|
||||
|
||||
expect(h.elements[1].angle).toBe(30);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -41,6 +41,34 @@ export const polygonIncludesPoint = <Point extends LocalPoint | GlobalPoint>(
|
|||
return inside;
|
||||
};
|
||||
|
||||
export const polygonIncludesPointNonZero = <Point extends [number, number]>(
|
||||
point: Point,
|
||||
polygon: Point[],
|
||||
): boolean => {
|
||||
const [x, y] = point;
|
||||
let windingNumber = 0;
|
||||
|
||||
for (let i = 0; i < polygon.length; i++) {
|
||||
const j = (i + 1) % polygon.length;
|
||||
const [xi, yi] = polygon[i];
|
||||
const [xj, yj] = polygon[j];
|
||||
|
||||
if (yi <= y) {
|
||||
if (yj > y) {
|
||||
if ((xj - xi) * (y - yi) - (x - xi) * (yj - yi) > 0) {
|
||||
windingNumber++;
|
||||
}
|
||||
}
|
||||
} else if (yj <= y) {
|
||||
if ((xj - xi) * (y - yi) - (x - xi) * (yj - yi) < 0) {
|
||||
windingNumber--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return windingNumber !== 0;
|
||||
};
|
||||
|
||||
export const pointOnPolygon = <Point extends LocalPoint | GlobalPoint>(
|
||||
p: Point,
|
||||
poly: Polygon<Point>,
|
||||
|
|
Loading…
Add table
Reference in a new issue