Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4eb1bd8036 | |||
| e19fd1332a | |||
| 6e655cdb24 | |||
| 8d7ffa21d1 | |||
| 192c4e7658 | |||
| 195a743874 | |||
| 82cef23c3d | |||
| 4a60fe3d22 | |||
| 541725ff5a | |||
| 28066034d7 | |||
| 7d0d6aec7a | |||
| e6ade3b627 | |||
| 9a2bd18904 | |||
| c7c6a4c3f1 | |||
| 9c27f936de | |||
| b8fdd7ef23 | |||
| ece841326b | |||
| 2a0d15799c | |||
| a18b139a60 | |||
| 41711af210 | |||
| 230e47fd52 | |||
| 52445aeb68 | |||
| bc9f34e71e | |||
| 22aade07b3 | |||
| c2de1304b7 | |||
| 25fb43f5b7 | |||
| 6dfa5de66c | |||
| 7abbb2afa3 | |||
| aa91a3d610 | |||
| 25d6e517c9 | |||
| d5e33730ab | |||
| c06b78c1b2 | |||
| eaa869620e | |||
| a8338cdb5a | |||
| 1ee3676784 | |||
| f12f7e4b50 |
@@ -32,6 +32,12 @@
|
|||||||
"name": "jotai",
|
"name": "jotai",
|
||||||
"message": "Do not import from \"jotai\" directly. Use our app-specific modules (\"editor-jotai\" or \"app-jotai\")."
|
"message": "Do not import from \"jotai\" directly. Use our app-specific modules (\"editor-jotai\" or \"app-jotai\")."
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"react/jsx-no-target-blank": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"allowReferrer": true
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export const AIComponents = ({
|
|||||||
</br>
|
</br>
|
||||||
<div>You can also try <a href="${
|
<div>You can also try <a href="${
|
||||||
import.meta.env.VITE_APP_PLUS_LP
|
import.meta.env.VITE_APP_PLUS_LP
|
||||||
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=d2c" target="_blank" rel="noreferrer noopener">Excalidraw+</a> to get more requests.</div>
|
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=d2c" target="_blank" rel="noopener">Excalidraw+</a> to get more requests.</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>`,
|
</html>`,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
import { isCurve } from "@excalidraw/math/curve";
|
import { isCurve } from "@excalidraw/math/curve";
|
||||||
|
|
||||||
import type { DebugElement } from "@excalidraw/excalidraw/visualdebug";
|
import type { DebugElement } from "@excalidraw/utils/visualdebug";
|
||||||
|
|
||||||
import type { Curve } from "@excalidraw/math";
|
import type { Curve } from "@excalidraw/math";
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const EncryptedIcon = () => {
|
|||||||
className="encrypted-icon tooltip"
|
className="encrypted-icon tooltip"
|
||||||
href="https://plus.excalidraw.com/blog/end-to-end-encryption"
|
href="https://plus.excalidraw.com/blog/end-to-end-encryption"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener"
|
||||||
aria-label={t("encrypted.link")}
|
aria-label={t("encrypted.link")}
|
||||||
>
|
>
|
||||||
<Tooltip label={t("encrypted.tooltip")} long={true}>
|
<Tooltip label={t("encrypted.tooltip")} long={true}>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const ExcalidrawPlusAppLink = () => {
|
|||||||
import.meta.env.VITE_APP_PLUS_APP
|
import.meta.env.VITE_APP_PLUS_APP
|
||||||
}?utm_source=excalidraw&utm_medium=app&utm_content=signedInUserRedirectButton#excalidraw-redirect`}
|
}?utm_source=excalidraw&utm_medium=app&utm_content=signedInUserRedirectButton#excalidraw-redirect`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noopener"
|
||||||
className="plus-button"
|
className="plus-button"
|
||||||
>
|
>
|
||||||
Go to Excalidraw+
|
Go to Excalidraw+
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ exports[`Test MobileMenu > should initialize with welcome screen and hide once u
|
|||||||
<a
|
<a
|
||||||
class="welcome-screen-menu-item "
|
class="welcome-screen-menu-item "
|
||||||
href="undefined/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest"
|
href="undefined/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest"
|
||||||
rel="noreferrer"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ export const CLASSES = {
|
|||||||
SHAPE_ACTIONS_MENU: "App-menu__left",
|
SHAPE_ACTIONS_MENU: "App-menu__left",
|
||||||
ZOOM_ACTIONS: "zoom-actions",
|
ZOOM_ACTIONS: "zoom-actions",
|
||||||
SEARCH_MENU_INPUT_WRAPPER: "layer-ui__search-inputWrapper",
|
SEARCH_MENU_INPUT_WRAPPER: "layer-ui__search-inputWrapper",
|
||||||
|
CONVERT_ELEMENT_TYPE_POPUP: "ConvertElementTypePopup",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CJK_HAND_DRAWN_FALLBACK_FONT = "Xiaolai";
|
export const CJK_HAND_DRAWN_FALLBACK_FONT = "Xiaolai";
|
||||||
|
|||||||
@@ -43,6 +43,12 @@ import {
|
|||||||
import { intersectElementWithLineSegment } from "./collision";
|
import { intersectElementWithLineSegment } from "./collision";
|
||||||
import { distanceToBindableElement } from "./distance";
|
import { distanceToBindableElement } from "./distance";
|
||||||
import {
|
import {
|
||||||
|
compareHeading,
|
||||||
|
HEADING_DOWN,
|
||||||
|
HEADING_LEFT,
|
||||||
|
HEADING_RIGHT,
|
||||||
|
HEADING_UP,
|
||||||
|
headingForPoint,
|
||||||
headingForPointFromElement,
|
headingForPointFromElement,
|
||||||
headingIsHorizontal,
|
headingIsHorizontal,
|
||||||
vectorToHeading,
|
vectorToHeading,
|
||||||
@@ -81,7 +87,6 @@ import type {
|
|||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
OrderedExcalidrawElement,
|
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
FixedPoint,
|
FixedPoint,
|
||||||
FixedPointBinding,
|
FixedPointBinding,
|
||||||
@@ -276,15 +281,6 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
|||||||
zoom,
|
zoom,
|
||||||
)
|
)
|
||||||
: null // If binding is disabled and start is dragged, break all binds
|
: null // If binding is disabled and start is dragged, break all binds
|
||||||
: !isElbowArrow(selectedElement)
|
|
||||||
? // We have to update the focus and gap of the binding, so let's rebind
|
|
||||||
getElligibleElementForBindingElement(
|
|
||||||
selectedElement,
|
|
||||||
"start",
|
|
||||||
elementsMap,
|
|
||||||
elements,
|
|
||||||
zoom,
|
|
||||||
)
|
|
||||||
: "keep";
|
: "keep";
|
||||||
const end = endDragged
|
const end = endDragged
|
||||||
? isBindingEnabled
|
? isBindingEnabled
|
||||||
@@ -296,15 +292,6 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
|||||||
zoom,
|
zoom,
|
||||||
)
|
)
|
||||||
: null // If binding is disabled and end is dragged, break all binds
|
: null // If binding is disabled and end is dragged, break all binds
|
||||||
: !isElbowArrow(selectedElement)
|
|
||||||
? // We have to update the focus and gap of the binding, so let's rebind
|
|
||||||
getElligibleElementForBindingElement(
|
|
||||||
selectedElement,
|
|
||||||
"end",
|
|
||||||
elementsMap,
|
|
||||||
elements,
|
|
||||||
zoom,
|
|
||||||
)
|
|
||||||
: "keep";
|
: "keep";
|
||||||
|
|
||||||
return [start, end];
|
return [start, end];
|
||||||
@@ -728,29 +715,32 @@ const calculateFocusAndGap = (
|
|||||||
|
|
||||||
// Supports translating, rotating and scaling `changedElement` with bound
|
// Supports translating, rotating and scaling `changedElement` with bound
|
||||||
// linear elements.
|
// linear elements.
|
||||||
// Because scaling involves moving the focus points as well, it is
|
|
||||||
// done before the `changedElement` is updated, and the `newSize` is passed
|
|
||||||
// in explicitly.
|
|
||||||
export const updateBoundElements = (
|
export const updateBoundElements = (
|
||||||
changedElement: NonDeletedExcalidrawElement,
|
changedElement: NonDeletedExcalidrawElement,
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
options?: {
|
options?: {
|
||||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||||
newSize?: { width: number; height: number };
|
newSize?: { width: number; height: number };
|
||||||
changedElements?: Map<string, OrderedExcalidrawElement>;
|
changedElements?: Map<string, ExcalidrawElement>;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
|
if (!isBindableElement(changedElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { newSize, simultaneouslyUpdated } = options ?? {};
|
const { newSize, simultaneouslyUpdated } = options ?? {};
|
||||||
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
|
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
|
||||||
simultaneouslyUpdated,
|
simultaneouslyUpdated,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isBindableElement(changedElement)) {
|
let elementsMap: ElementsMap = scene.getNonDeletedElementsMap();
|
||||||
return;
|
if (options?.changedElements) {
|
||||||
|
elementsMap = new Map(elementsMap) as typeof elementsMap;
|
||||||
|
options.changedElements.forEach((element) => {
|
||||||
|
elementsMap.set(element.id, element);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
|
||||||
|
|
||||||
boundElementsVisitor(elementsMap, changedElement, (element) => {
|
boundElementsVisitor(elementsMap, changedElement, (element) => {
|
||||||
if (!isLinearElement(element) || element.isDeleted) {
|
if (!isLinearElement(element) || element.isDeleted) {
|
||||||
return;
|
return;
|
||||||
@@ -854,6 +844,25 @@ export const updateBoundElements = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updateBindings = (
|
||||||
|
latestElement: ExcalidrawElement,
|
||||||
|
scene: Scene,
|
||||||
|
options?: {
|
||||||
|
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||||
|
newSize?: { width: number; height: number };
|
||||||
|
zoom?: AppState["zoom"];
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
if (isLinearElement(latestElement)) {
|
||||||
|
bindOrUnbindLinearElements([latestElement], true, [], scene, options?.zoom);
|
||||||
|
} else {
|
||||||
|
updateBoundElements(latestElement, scene, {
|
||||||
|
...options,
|
||||||
|
changedElements: new Map([[latestElement.id, latestElement]]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const doesNeedUpdate = (
|
const doesNeedUpdate = (
|
||||||
boundElement: NonDeleted<ExcalidrawLinearElement>,
|
boundElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
changedElement: ExcalidrawBindableElement,
|
changedElement: ExcalidrawBindableElement,
|
||||||
@@ -1022,7 +1031,14 @@ export const avoidRectangularCorner = (
|
|||||||
|
|
||||||
if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) {
|
if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) {
|
||||||
// Top left
|
// Top left
|
||||||
if (nonRotatedPoint[1] - element.y > -FIXED_BINDING_DISTANCE) {
|
const heading = headingForPoint(
|
||||||
|
nonRotatedPoint,
|
||||||
|
pointFrom(element.x, element.y),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
compareHeading(heading, HEADING_DOWN) ||
|
||||||
|
compareHeading(heading, HEADING_LEFT)
|
||||||
|
) {
|
||||||
return pointRotateRads<GlobalPoint>(
|
return pointRotateRads<GlobalPoint>(
|
||||||
pointFrom(element.x - FIXED_BINDING_DISTANCE, element.y),
|
pointFrom(element.x - FIXED_BINDING_DISTANCE, element.y),
|
||||||
center,
|
center,
|
||||||
@@ -1039,7 +1055,14 @@ export const avoidRectangularCorner = (
|
|||||||
nonRotatedPoint[1] > element.y + element.height
|
nonRotatedPoint[1] > element.y + element.height
|
||||||
) {
|
) {
|
||||||
// Bottom left
|
// Bottom left
|
||||||
if (nonRotatedPoint[0] - element.x > -FIXED_BINDING_DISTANCE) {
|
const heading = headingForPoint(
|
||||||
|
nonRotatedPoint,
|
||||||
|
pointFrom(element.x, element.y + element.height),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
compareHeading(heading, HEADING_DOWN) ||
|
||||||
|
compareHeading(heading, HEADING_RIGHT)
|
||||||
|
) {
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
pointFrom(
|
pointFrom(
|
||||||
element.x,
|
element.x,
|
||||||
@@ -1059,9 +1082,13 @@ export const avoidRectangularCorner = (
|
|||||||
nonRotatedPoint[1] > element.y + element.height
|
nonRotatedPoint[1] > element.y + element.height
|
||||||
) {
|
) {
|
||||||
// Bottom right
|
// Bottom right
|
||||||
|
const heading = headingForPoint(
|
||||||
|
nonRotatedPoint,
|
||||||
|
pointFrom(element.x + element.width, element.y + element.height),
|
||||||
|
);
|
||||||
if (
|
if (
|
||||||
nonRotatedPoint[0] - element.x <
|
compareHeading(heading, HEADING_DOWN) ||
|
||||||
element.width + FIXED_BINDING_DISTANCE
|
compareHeading(heading, HEADING_LEFT)
|
||||||
) {
|
) {
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
pointFrom(
|
pointFrom(
|
||||||
@@ -1085,9 +1112,13 @@ export const avoidRectangularCorner = (
|
|||||||
nonRotatedPoint[1] < element.y
|
nonRotatedPoint[1] < element.y
|
||||||
) {
|
) {
|
||||||
// Top right
|
// Top right
|
||||||
|
const heading = headingForPoint(
|
||||||
|
nonRotatedPoint,
|
||||||
|
pointFrom(element.x + element.width, element.y),
|
||||||
|
);
|
||||||
if (
|
if (
|
||||||
nonRotatedPoint[0] - element.x <
|
compareHeading(heading, HEADING_UP) ||
|
||||||
element.width + FIXED_BINDING_DISTANCE
|
compareHeading(heading, HEADING_LEFT)
|
||||||
) {
|
) {
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
pointFrom(
|
pointFrom(
|
||||||
@@ -1105,6 +1136,17 @@ export const avoidRectangularCorner = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Break up explicit border bindings to have better elbow arrow routing
|
||||||
|
if (p[0] === element.x) {
|
||||||
|
return pointFrom(p[0] - FIXED_BINDING_DISTANCE, p[1]);
|
||||||
|
} else if (p[0] === element.x + element.width) {
|
||||||
|
return pointFrom(p[0] + FIXED_BINDING_DISTANCE, p[1]);
|
||||||
|
} else if (p[1] === element.y) {
|
||||||
|
return pointFrom(p[0], p[1] - FIXED_BINDING_DISTANCE);
|
||||||
|
} else if (p[1] === element.y + element.height) {
|
||||||
|
return pointFrom(p[0], p[1] + FIXED_BINDING_DISTANCE);
|
||||||
|
}
|
||||||
|
|
||||||
return p;
|
return p;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
rescalePoints,
|
|
||||||
arrayToMap,
|
arrayToMap,
|
||||||
invariant,
|
invariant,
|
||||||
|
rescalePoints,
|
||||||
sizeOf,
|
sizeOf,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
degreesToRadians,
|
degreesToRadians,
|
||||||
lineSegment,
|
lineSegment,
|
||||||
pointFrom,
|
|
||||||
pointDistance,
|
pointDistance,
|
||||||
|
pointFrom,
|
||||||
pointFromArray,
|
pointFromArray,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
@@ -33,8 +33,8 @@ import type { AppState } from "@excalidraw/excalidraw/types";
|
|||||||
|
|
||||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import { ShapeCache } from "./ShapeCache";
|
|
||||||
import { generateRoughOptions } from "./Shape";
|
import { generateRoughOptions } from "./Shape";
|
||||||
|
import { ShapeCache } from "./ShapeCache";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { getBoundTextElement, getContainerElement } from "./textElement";
|
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||||
import {
|
import {
|
||||||
@@ -52,20 +52,20 @@ import {
|
|||||||
deconstructRectanguloidElement,
|
deconstructRectanguloidElement,
|
||||||
} from "./utils";
|
} 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 { Drawable, Op } from "roughjs/bin/core";
|
||||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
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 = {
|
export type RectangleBox = {
|
||||||
x: number;
|
x: number;
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ import {
|
|||||||
type NonDeletedSceneElementsMap,
|
type NonDeletedSceneElementsMap,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import { aabbForElement, pointInsideBounds } from "./shapes";
|
import { aabbForElement, aabbForPoints, pointInsideBounds } from "./shapes";
|
||||||
|
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
import type { Heading } from "./heading";
|
import type { Heading } from "./heading";
|
||||||
@@ -65,6 +65,8 @@ import type {
|
|||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
|
import { debugDrawBounds } from "@excalidraw/utils/visualdebug";
|
||||||
|
|
||||||
type GridAddress = [number, number] & { _brand: "gridaddress" };
|
type GridAddress = [number, number] & { _brand: "gridaddress" };
|
||||||
|
|
||||||
type Node = {
|
type Node = {
|
||||||
@@ -106,8 +108,32 @@ type ElbowArrowData = {
|
|||||||
hoveredEndElement: ExcalidrawBindableElement | null;
|
hoveredEndElement: ExcalidrawBindableElement | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEDUP_TRESHOLD = 1;
|
const calculateDedupTreshhold = <Point extends GlobalPoint | LocalPoint>(
|
||||||
export const BASE_PADDING = 40;
|
a: Point,
|
||||||
|
b: Point,
|
||||||
|
) => 1 + pointDistance(a, b) / 300;
|
||||||
|
|
||||||
|
const calculatePadding = (
|
||||||
|
aabb: Bounds,
|
||||||
|
startBoundingBox: Bounds,
|
||||||
|
endBoundingBox: Bounds,
|
||||||
|
) => {
|
||||||
|
return Math.max(
|
||||||
|
Math.min(
|
||||||
|
Math.hypot(
|
||||||
|
startBoundingBox[2] - startBoundingBox[0],
|
||||||
|
startBoundingBox[3] - startBoundingBox[1],
|
||||||
|
) / 4,
|
||||||
|
Math.hypot(
|
||||||
|
endBoundingBox[2] - endBoundingBox[0],
|
||||||
|
endBoundingBox[3] - endBoundingBox[1],
|
||||||
|
) / 4,
|
||||||
|
Math.hypot(aabb[2] - aabb[0], aabb[3] - aabb[1]) / 4,
|
||||||
|
40,
|
||||||
|
),
|
||||||
|
30,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSegmentRenormalization = (
|
const handleSegmentRenormalization = (
|
||||||
arrow: ExcalidrawElbowArrowElement,
|
arrow: ExcalidrawElbowArrowElement,
|
||||||
@@ -183,7 +209,11 @@ const handleSegmentRenormalization = (
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
// Remove segments that are too short
|
// Remove segments that are too short
|
||||||
pointDistance(points[i - 2], points[i - 1]) < DEDUP_TRESHOLD
|
pointDistance(points[i - 2], points[i - 1]) <
|
||||||
|
calculateDedupTreshhold(
|
||||||
|
points[i - 3] ?? points[i - 3],
|
||||||
|
points[i] ?? points[i - 1],
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
const prevPrevSegmentIdx =
|
const prevPrevSegmentIdx =
|
||||||
nextFixedSegments?.findIndex((segment) => segment.index === i - 2) ??
|
nextFixedSegments?.findIndex((segment) => segment.index === i - 2) ??
|
||||||
@@ -359,6 +389,10 @@ const handleSegmentRelease = (
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!restoredPoints) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
const nextPoints: GlobalPoint[] = [];
|
const nextPoints: GlobalPoint[] = [];
|
||||||
|
|
||||||
// First part of the arrow are the old points
|
// First part of the arrow are the old points
|
||||||
@@ -463,6 +497,13 @@ const handleSegmentMove = (
|
|||||||
hoveredStartElement: ExcalidrawBindableElement | null,
|
hoveredStartElement: ExcalidrawBindableElement | null,
|
||||||
hoveredEndElement: ExcalidrawBindableElement | null,
|
hoveredEndElement: ExcalidrawBindableElement | null,
|
||||||
): ElementUpdate<ExcalidrawElbowArrowElement> => {
|
): ElementUpdate<ExcalidrawElbowArrowElement> => {
|
||||||
|
const BASE_PADDING = calculatePadding(
|
||||||
|
aabbForElement(arrow),
|
||||||
|
hoveredStartElement
|
||||||
|
? aabbForElement(hoveredStartElement)
|
||||||
|
: [10, 10, 10, 10],
|
||||||
|
hoveredEndElement ? aabbForElement(hoveredEndElement) : [10, 10, 10, 10],
|
||||||
|
);
|
||||||
const activelyModifiedSegmentIdx = fixedSegments
|
const activelyModifiedSegmentIdx = fixedSegments
|
||||||
.map((segment, i) => {
|
.map((segment, i) => {
|
||||||
if (
|
if (
|
||||||
@@ -707,6 +748,13 @@ const handleEndpointDrag = (
|
|||||||
hoveredStartElement: ExcalidrawBindableElement | null,
|
hoveredStartElement: ExcalidrawBindableElement | null,
|
||||||
hoveredEndElement: ExcalidrawBindableElement | null,
|
hoveredEndElement: ExcalidrawBindableElement | null,
|
||||||
) => {
|
) => {
|
||||||
|
const BASE_PADDING = calculatePadding(
|
||||||
|
aabbForPoints([startGlobalPoint, endGlobalPoint]),
|
||||||
|
hoveredStartElement
|
||||||
|
? aabbForElement(hoveredStartElement)
|
||||||
|
: [10, 10, 10, 10],
|
||||||
|
hoveredEndElement ? aabbForElement(hoveredEndElement) : [10, 10, 10, 10],
|
||||||
|
);
|
||||||
let startIsSpecial = arrow.startIsSpecial ?? null;
|
let startIsSpecial = arrow.startIsSpecial ?? null;
|
||||||
let endIsSpecial = arrow.endIsSpecial ?? null;
|
let endIsSpecial = arrow.endIsSpecial ?? null;
|
||||||
const globalUpdatedPoints = updatedPoints.map((p, i) =>
|
const globalUpdatedPoints = updatedPoints.map((p, i) =>
|
||||||
@@ -741,6 +789,7 @@ const handleEndpointDrag = (
|
|||||||
|
|
||||||
// Calculate the moving second point connection and add the start point
|
// Calculate the moving second point connection and add the start point
|
||||||
{
|
{
|
||||||
|
startIsSpecial = arrow.startIsSpecial && globalUpdatedPoints.length > 2;
|
||||||
const secondPoint = globalUpdatedPoints[startIsSpecial ? 2 : 1];
|
const secondPoint = globalUpdatedPoints[startIsSpecial ? 2 : 1];
|
||||||
const thirdPoint = globalUpdatedPoints[startIsSpecial ? 3 : 2];
|
const thirdPoint = globalUpdatedPoints[startIsSpecial ? 3 : 2];
|
||||||
const startIsHorizontal = headingIsHorizontal(startHeading);
|
const startIsHorizontal = headingIsHorizontal(startHeading);
|
||||||
@@ -801,6 +850,7 @@ const handleEndpointDrag = (
|
|||||||
|
|
||||||
// Calculate the moving second to last point connection
|
// Calculate the moving second to last point connection
|
||||||
{
|
{
|
||||||
|
endIsSpecial = arrow.endIsSpecial && globalUpdatedPoints.length > 2;
|
||||||
const secondToLastPoint =
|
const secondToLastPoint =
|
||||||
globalUpdatedPoints[globalUpdatedPoints.length - (endIsSpecial ? 3 : 2)];
|
globalUpdatedPoints[globalUpdatedPoints.length - (endIsSpecial ? 3 : 2)];
|
||||||
const thirdToLastPoint =
|
const thirdToLastPoint =
|
||||||
@@ -1293,29 +1343,28 @@ const getElbowArrowData = (
|
|||||||
endGlobalPoint[0] + 2,
|
endGlobalPoint[0] + 2,
|
||||||
endGlobalPoint[1] + 2,
|
endGlobalPoint[1] + 2,
|
||||||
] as Bounds;
|
] as Bounds;
|
||||||
|
const BASE_PADDING = calculatePadding(
|
||||||
|
aabbForPoints([startGlobalPoint, endGlobalPoint]),
|
||||||
|
hoveredStartElement
|
||||||
|
? aabbForElement(hoveredStartElement)
|
||||||
|
: [10, 10, 10, 10],
|
||||||
|
hoveredEndElement ? aabbForElement(hoveredEndElement) : [10, 10, 10, 10],
|
||||||
|
);
|
||||||
|
const startOffsets = offsetFromHeading(
|
||||||
|
startHeading,
|
||||||
|
arrow.startArrowhead ? FIXED_BINDING_DISTANCE * 4 : FIXED_BINDING_DISTANCE,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
const endOffsets = offsetFromHeading(
|
||||||
|
endHeading,
|
||||||
|
arrow.endArrowhead ? FIXED_BINDING_DISTANCE * 4 : FIXED_BINDING_DISTANCE,
|
||||||
|
1,
|
||||||
|
);
|
||||||
const startElementBounds = hoveredStartElement
|
const startElementBounds = hoveredStartElement
|
||||||
? aabbForElement(
|
? aabbForElement(hoveredStartElement, startOffsets)
|
||||||
hoveredStartElement,
|
|
||||||
offsetFromHeading(
|
|
||||||
startHeading,
|
|
||||||
arrow.startArrowhead
|
|
||||||
? FIXED_BINDING_DISTANCE * 6
|
|
||||||
: FIXED_BINDING_DISTANCE * 2,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: startPointBounds;
|
: startPointBounds;
|
||||||
const endElementBounds = hoveredEndElement
|
const endElementBounds = hoveredEndElement
|
||||||
? aabbForElement(
|
? aabbForElement(hoveredEndElement, endOffsets)
|
||||||
hoveredEndElement,
|
|
||||||
offsetFromHeading(
|
|
||||||
endHeading,
|
|
||||||
arrow.endArrowhead
|
|
||||||
? FIXED_BINDING_DISTANCE * 6
|
|
||||||
: FIXED_BINDING_DISTANCE * 2,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: endPointBounds;
|
: endPointBounds;
|
||||||
const boundsOverlap =
|
const boundsOverlap =
|
||||||
pointInsideBounds(
|
pointInsideBounds(
|
||||||
@@ -1358,7 +1407,7 @@ const getElbowArrowData = (
|
|||||||
: BASE_PADDING -
|
: BASE_PADDING -
|
||||||
(arrow.startArrowhead
|
(arrow.startArrowhead
|
||||||
? FIXED_BINDING_DISTANCE * 6
|
? FIXED_BINDING_DISTANCE * 6
|
||||||
: FIXED_BINDING_DISTANCE * 2),
|
: FIXED_BINDING_DISTANCE),
|
||||||
BASE_PADDING,
|
BASE_PADDING,
|
||||||
),
|
),
|
||||||
boundsOverlap
|
boundsOverlap
|
||||||
@@ -1374,13 +1423,29 @@ const getElbowArrowData = (
|
|||||||
: BASE_PADDING -
|
: BASE_PADDING -
|
||||||
(arrow.endArrowhead
|
(arrow.endArrowhead
|
||||||
? FIXED_BINDING_DISTANCE * 6
|
? FIXED_BINDING_DISTANCE * 6
|
||||||
: FIXED_BINDING_DISTANCE * 2),
|
: FIXED_BINDING_DISTANCE),
|
||||||
BASE_PADDING,
|
BASE_PADDING,
|
||||||
),
|
),
|
||||||
boundsOverlap,
|
boundsOverlap,
|
||||||
hoveredStartElement && aabbForElement(hoveredStartElement),
|
hoveredStartElement
|
||||||
hoveredEndElement && aabbForElement(hoveredEndElement),
|
? aabbForElement(hoveredStartElement)
|
||||||
|
: startPointBounds,
|
||||||
|
hoveredEndElement ? aabbForElement(hoveredEndElement) : endPointBounds,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
debugDrawBounds(startElementBounds, {
|
||||||
|
permanent: false,
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
|
debugDrawBounds(endElementBounds, {
|
||||||
|
permanent: false,
|
||||||
|
color: "green",
|
||||||
|
});
|
||||||
|
debugDrawBounds(dynamicAABBs, {
|
||||||
|
permanent: false,
|
||||||
|
color: "blue",
|
||||||
|
});
|
||||||
|
|
||||||
const startDonglePosition = getDonglePosition(
|
const startDonglePosition = getDonglePosition(
|
||||||
dynamicAABBs[0],
|
dynamicAABBs[0],
|
||||||
startHeading,
|
startHeading,
|
||||||
@@ -1651,11 +1716,11 @@ const generateDynamicAABBs = (
|
|||||||
a: Bounds,
|
a: Bounds,
|
||||||
b: Bounds,
|
b: Bounds,
|
||||||
common: Bounds,
|
common: Bounds,
|
||||||
startDifference?: [number, number, number, number],
|
startDifference: [number, number, number, number],
|
||||||
endDifference?: [number, number, number, number],
|
endDifference: [number, number, number, number],
|
||||||
disableSideHack?: boolean,
|
disableSideHack: boolean,
|
||||||
startElementBounds?: Bounds | null,
|
startElementBounds: Bounds,
|
||||||
endElementBounds?: Bounds | null,
|
endElementBounds: Bounds,
|
||||||
): Bounds[] => {
|
): Bounds[] => {
|
||||||
const startEl = startElementBounds ?? a;
|
const startEl = startElementBounds ?? a;
|
||||||
const endEl = endElementBounds ?? b;
|
const endEl = endElementBounds ?? b;
|
||||||
@@ -1735,15 +1800,24 @@ const generateDynamicAABBs = (
|
|||||||
(second[0] + second[2]) / 2,
|
(second[0] + second[2]) / 2,
|
||||||
(second[1] + second[3]) / 2,
|
(second[1] + second[3]) / 2,
|
||||||
];
|
];
|
||||||
if (b[0] > a[2] && a[1] > b[3]) {
|
if (
|
||||||
|
endElementBounds[0] > startElementBounds[2] &&
|
||||||
|
startElementBounds[1] > endElementBounds[3]
|
||||||
|
) {
|
||||||
// BOTTOM LEFT
|
// BOTTOM LEFT
|
||||||
const cX = first[2] + (second[0] - first[2]) / 2;
|
const cX = first[2] + (second[0] - first[2]) / 2;
|
||||||
const cY = second[3] + (first[1] - second[3]) / 2;
|
const cY = second[3] + (first[1] - second[3]) / 2;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
vectorCross(
|
vectorCross(
|
||||||
vector(a[2] - endCenterX, a[1] - endCenterY),
|
vector(
|
||||||
vector(a[0] - endCenterX, a[3] - endCenterY),
|
startElementBounds[2] - endCenterX,
|
||||||
|
startElementBounds[1] - endCenterY,
|
||||||
|
),
|
||||||
|
vector(
|
||||||
|
startElementBounds[0] - endCenterX,
|
||||||
|
startElementBounds[3] - endCenterY,
|
||||||
|
),
|
||||||
) > 0
|
) > 0
|
||||||
) {
|
) {
|
||||||
return [
|
return [
|
||||||
@@ -1756,15 +1830,24 @@ const generateDynamicAABBs = (
|
|||||||
[first[0], cY, first[2], first[3]],
|
[first[0], cY, first[2], first[3]],
|
||||||
[second[0], second[1], second[2], cY],
|
[second[0], second[1], second[2], cY],
|
||||||
];
|
];
|
||||||
} else if (a[2] < b[0] && a[3] < b[1]) {
|
} else if (
|
||||||
|
startElementBounds[2] < endElementBounds[0] &&
|
||||||
|
startElementBounds[3] < endElementBounds[1]
|
||||||
|
) {
|
||||||
// TOP LEFT
|
// TOP LEFT
|
||||||
const cX = first[2] + (second[0] - first[2]) / 2;
|
const cX = first[2] + (second[0] - first[2]) / 2;
|
||||||
const cY = first[3] + (second[1] - first[3]) / 2;
|
const cY = first[3] + (second[1] - first[3]) / 2;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
vectorCross(
|
vectorCross(
|
||||||
vector(a[0] - endCenterX, a[1] - endCenterY),
|
vector(
|
||||||
vector(a[2] - endCenterX, a[3] - endCenterY),
|
startElementBounds[0] - endCenterX,
|
||||||
|
startElementBounds[1] - endCenterY,
|
||||||
|
),
|
||||||
|
vector(
|
||||||
|
startElementBounds[2] - endCenterX,
|
||||||
|
startElementBounds[3] - endCenterY,
|
||||||
|
),
|
||||||
) > 0
|
) > 0
|
||||||
) {
|
) {
|
||||||
return [
|
return [
|
||||||
@@ -1777,15 +1860,24 @@ const generateDynamicAABBs = (
|
|||||||
[first[0], first[1], cX, first[3]],
|
[first[0], first[1], cX, first[3]],
|
||||||
[cX, second[1], second[2], second[3]],
|
[cX, second[1], second[2], second[3]],
|
||||||
];
|
];
|
||||||
} else if (a[0] > b[2] && a[3] < b[1]) {
|
} else if (
|
||||||
|
startElementBounds[0] > endElementBounds[2] &&
|
||||||
|
startElementBounds[3] < endElementBounds[1]
|
||||||
|
) {
|
||||||
// TOP RIGHT
|
// TOP RIGHT
|
||||||
const cX = second[2] + (first[0] - second[2]) / 2;
|
const cX = second[2] + (first[0] - second[2]) / 2;
|
||||||
const cY = first[3] + (second[1] - first[3]) / 2;
|
const cY = first[3] + (second[1] - first[3]) / 2;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
vectorCross(
|
vectorCross(
|
||||||
vector(a[2] - endCenterX, a[1] - endCenterY),
|
vector(
|
||||||
vector(a[0] - endCenterX, a[3] - endCenterY),
|
startElementBounds[2] - endCenterX,
|
||||||
|
startElementBounds[1] - endCenterY,
|
||||||
|
),
|
||||||
|
vector(
|
||||||
|
startElementBounds[0] - endCenterX,
|
||||||
|
startElementBounds[3] - endCenterY,
|
||||||
|
),
|
||||||
) > 0
|
) > 0
|
||||||
) {
|
) {
|
||||||
return [
|
return [
|
||||||
@@ -1798,15 +1890,24 @@ const generateDynamicAABBs = (
|
|||||||
[first[0], first[1], first[2], cY],
|
[first[0], first[1], first[2], cY],
|
||||||
[second[0], cY, second[2], second[3]],
|
[second[0], cY, second[2], second[3]],
|
||||||
];
|
];
|
||||||
} else if (a[0] > b[2] && a[1] > b[3]) {
|
} else if (
|
||||||
|
startElementBounds[0] > endElementBounds[2] &&
|
||||||
|
startElementBounds[1] > endElementBounds[3]
|
||||||
|
) {
|
||||||
// BOTTOM RIGHT
|
// BOTTOM RIGHT
|
||||||
const cX = second[2] + (first[0] - second[2]) / 2;
|
const cX = second[2] + (first[0] - second[2]) / 2;
|
||||||
const cY = second[3] + (first[1] - second[3]) / 2;
|
const cY = second[3] + (first[1] - second[3]) / 2;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
vectorCross(
|
vectorCross(
|
||||||
vector(a[0] - endCenterX, a[1] - endCenterY),
|
vector(
|
||||||
vector(a[2] - endCenterX, a[3] - endCenterY),
|
startElementBounds[0] - endCenterX,
|
||||||
|
startElementBounds[1] - endCenterY,
|
||||||
|
),
|
||||||
|
vector(
|
||||||
|
startElementBounds[2] - endCenterX,
|
||||||
|
startElementBounds[3] - endCenterY,
|
||||||
|
),
|
||||||
) > 0
|
) > 0
|
||||||
) {
|
) {
|
||||||
return [
|
return [
|
||||||
@@ -2088,16 +2189,11 @@ const normalizeArrowElementUpdate = (
|
|||||||
nextFixedSegments: readonly FixedSegment[] | null,
|
nextFixedSegments: readonly FixedSegment[] | null,
|
||||||
startIsSpecial?: ExcalidrawElbowArrowElement["startIsSpecial"],
|
startIsSpecial?: ExcalidrawElbowArrowElement["startIsSpecial"],
|
||||||
endIsSpecial?: ExcalidrawElbowArrowElement["startIsSpecial"],
|
endIsSpecial?: ExcalidrawElbowArrowElement["startIsSpecial"],
|
||||||
): {
|
): ElementUpdate<ExcalidrawElbowArrowElement> => {
|
||||||
points: LocalPoint[];
|
if (global.length === 0) {
|
||||||
x: number;
|
return {};
|
||||||
y: number;
|
}
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
fixedSegments: readonly FixedSegment[] | null;
|
|
||||||
startIsSpecial?: ExcalidrawElbowArrowElement["startIsSpecial"];
|
|
||||||
endIsSpecial?: ExcalidrawElbowArrowElement["startIsSpecial"];
|
|
||||||
} => {
|
|
||||||
const offsetX = global[0][0];
|
const offsetX = global[0][0];
|
||||||
const offsetY = global[0][1];
|
const offsetY = global[0][1];
|
||||||
let points = global.map((p) =>
|
let points = global.map((p) =>
|
||||||
@@ -2185,7 +2281,10 @@ const removeElbowArrowShortSegments = (
|
|||||||
|
|
||||||
const prev = points[idx - 1];
|
const prev = points[idx - 1];
|
||||||
const prevDist = pointDistance(prev, p);
|
const prevDist = pointDistance(prev, p);
|
||||||
return prevDist > DEDUP_TRESHOLD;
|
return (
|
||||||
|
prevDist >
|
||||||
|
calculateDedupTreshhold(points[idx - 2] ?? prev, points[idx + 1] ?? p)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2288,13 +2387,16 @@ const gridAddressesEqual = (a: GridAddress, b: GridAddress): boolean =>
|
|||||||
|
|
||||||
export const validateElbowPoints = <P extends GlobalPoint | LocalPoint>(
|
export const validateElbowPoints = <P extends GlobalPoint | LocalPoint>(
|
||||||
points: readonly P[],
|
points: readonly P[],
|
||||||
tolerance: number = DEDUP_TRESHOLD,
|
tolerance?: number,
|
||||||
) =>
|
) =>
|
||||||
points
|
points
|
||||||
.slice(1)
|
.slice(1)
|
||||||
.map(
|
.map((p, i) => {
|
||||||
(p, i) =>
|
const t =
|
||||||
Math.abs(p[0] - points[i][0]) < tolerance ||
|
tolerance ??
|
||||||
Math.abs(p[1] - points[i][1]) < tolerance,
|
calculateDedupTreshhold(points[i - 1] ?? points[i], points[i + 2] ?? p);
|
||||||
)
|
return (
|
||||||
|
Math.abs(p[0] - points[i][0]) < t || Math.abs(p[1] - points[i][1]) < t
|
||||||
|
);
|
||||||
|
})
|
||||||
.every(Boolean);
|
.every(Boolean);
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ import type {
|
|||||||
ExcalidrawIframeElement,
|
ExcalidrawIframeElement,
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
FixedSegment,
|
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@@ -478,7 +477,7 @@ export const newArrowElement = <T extends boolean>(
|
|||||||
endArrowhead?: Arrowhead | null;
|
endArrowhead?: Arrowhead | null;
|
||||||
points?: ExcalidrawArrowElement["points"];
|
points?: ExcalidrawArrowElement["points"];
|
||||||
elbowed?: T;
|
elbowed?: T;
|
||||||
fixedSegments?: FixedSegment[] | null;
|
fixedSegments?: ExcalidrawElbowArrowElement["fixedSegments"] | null;
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): T extends true
|
): T extends true
|
||||||
? NonDeleted<ExcalidrawElbowArrowElement>
|
? NonDeleted<ExcalidrawElbowArrowElement>
|
||||||
|
|||||||
@@ -282,6 +282,15 @@ export const mapIntervalToBezierT = <P extends GlobalPoint | LocalPoint>(
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const aabbForPoints = <Point extends GlobalPoint | LocalPoint>(
|
||||||
|
points: Point[],
|
||||||
|
): Bounds => [
|
||||||
|
Math.min(...points.map((point) => point[0])),
|
||||||
|
Math.min(...points.map((point) => point[1])),
|
||||||
|
Math.max(...points.map((point) => point[0])),
|
||||||
|
Math.max(...points.map((point) => point[1])),
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the axis-aligned bounding box for a given element
|
* Get the axis-aligned bounding box for a given element
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -119,6 +119,20 @@ export const isElbowArrow = (
|
|||||||
return isArrowElement(element) && element.elbowed;
|
return isArrowElement(element) && element.elbowed;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isSharpArrow = (
|
||||||
|
element?: ExcalidrawElement,
|
||||||
|
): element is ExcalidrawArrowElement => {
|
||||||
|
return isArrowElement(element) && !element.elbowed && !element.roundness;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isCurvedArrow = (
|
||||||
|
element?: ExcalidrawElement,
|
||||||
|
): element is ExcalidrawArrowElement => {
|
||||||
|
return (
|
||||||
|
isArrowElement(element) && !element.elbowed && element.roundness !== null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const isLinearElementType = (
|
export const isLinearElementType = (
|
||||||
elementType: ElementOrToolType,
|
elementType: ElementOrToolType,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
@@ -271,6 +285,10 @@ export const isBoundToContainer = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isArrowBoundToElement = (element: ExcalidrawArrowElement) => {
|
||||||
|
return !!element.startBinding || !!element.endBinding;
|
||||||
|
};
|
||||||
|
|
||||||
export const isUsingAdaptiveRadius = (type: string) =>
|
export const isUsingAdaptiveRadius = (type: string) =>
|
||||||
type === "rectangle" ||
|
type === "rectangle" ||
|
||||||
type === "embeddable" ||
|
type === "embeddable" ||
|
||||||
|
|||||||
@@ -412,3 +412,11 @@ export type NonDeletedSceneElementsMap = Map<
|
|||||||
export type ElementsMapOrArray =
|
export type ElementsMapOrArray =
|
||||||
| readonly ExcalidrawElement[]
|
| readonly ExcalidrawElement[]
|
||||||
| Readonly<ElementsMap>;
|
| Readonly<ElementsMap>;
|
||||||
|
|
||||||
|
export type ConvertibleGenericTypes = "rectangle" | "diamond" | "ellipse";
|
||||||
|
export type ConvertibleLinearTypes =
|
||||||
|
| "line"
|
||||||
|
| "sharpArrow"
|
||||||
|
| "curvedArrow"
|
||||||
|
| "elbowArrow";
|
||||||
|
export type ConvertibleTypes = ConvertibleGenericTypes | ConvertibleLinearTypes;
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ describe("elbow arrow routing", () => {
|
|||||||
expect(arrow.startBinding).not.toBe(null);
|
expect(arrow.startBinding).not.toBe(null);
|
||||||
expect(arrow.endBinding).not.toBe(null);
|
expect(arrow.endBinding).not.toBe(null);
|
||||||
|
|
||||||
h.app.scene.mutateElement(arrow, {
|
scene.mutateElement(arrow, {
|
||||||
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)],
|
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -295,11 +295,11 @@ describe("elbow arrow ui", () => {
|
|||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
UI.updateInput(inputAngle, String("40"));
|
UI.updateInput(inputAngle, String("40"));
|
||||||
|
|
||||||
expect(arrow.points.map((point) => point.map(Math.round))).toEqual([
|
expect(arrow.points).toCloselyEqualPoints([
|
||||||
[0, 0],
|
[0, 0],
|
||||||
[35, 0],
|
[34.7791, 0],
|
||||||
[35, 165],
|
[34.7791, 164.67],
|
||||||
[103, 165],
|
[102.931, 164.67],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -510,12 +510,12 @@ describe("arrow element", () => {
|
|||||||
h.state,
|
h.state,
|
||||||
)[0] as ExcalidrawElbowArrowElement;
|
)[0] as ExcalidrawElbowArrowElement;
|
||||||
|
|
||||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
|
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
||||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||||
|
|
||||||
UI.resize(rectangle, "se", [-200, -150]);
|
UI.resize(rectangle, "se", [-200, -150]);
|
||||||
|
|
||||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
|
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
||||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -538,11 +538,11 @@ describe("arrow element", () => {
|
|||||||
h.state,
|
h.state,
|
||||||
)[0] as ExcalidrawElbowArrowElement;
|
)[0] as ExcalidrawElbowArrowElement;
|
||||||
|
|
||||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
|
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
||||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||||
|
|
||||||
UI.resize([rectangle, arrow], "nw", [300, 350]);
|
UI.resize([rectangle, arrow], "nw", [300, 350]);
|
||||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(0);
|
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(-0.05);
|
||||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.25);
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.25);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -73,12 +73,12 @@ describe("flipping re-centers selection", () => {
|
|||||||
API.executeAction(actionFlipHorizontal);
|
API.executeAction(actionFlipHorizontal);
|
||||||
|
|
||||||
const rec1 = h.elements.find((el) => el.id === "rec1")!;
|
const rec1 = h.elements.find((el) => el.id === "rec1")!;
|
||||||
expect(rec1.x).toBeCloseTo(100, 0);
|
expect(rec1.x).toBeCloseTo(97.8678, 0);
|
||||||
expect(rec1.y).toBeCloseTo(100, 0);
|
expect(rec1.y).toBeCloseTo(97.444, 0);
|
||||||
|
|
||||||
const rec2 = h.elements.find((el) => el.id === "rec2")!;
|
const rec2 = h.elements.find((el) => el.id === "rec2")!;
|
||||||
expect(rec2.x).toBeCloseTo(220, 0);
|
expect(rec2.x).toBeCloseTo(218, 0);
|
||||||
expect(rec2.y).toBeCloseTo(250, 0);
|
expect(rec2.y).toBeCloseTo(247, 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getConversionTypeFromElements,
|
||||||
|
convertElementTypePopupAtom,
|
||||||
|
} from "../components/ConvertElementTypePopup";
|
||||||
|
import { editorJotaiStore } from "../editor-jotai";
|
||||||
|
import { CaptureUpdateAction } from "../store";
|
||||||
|
|
||||||
|
import { register } from "./register";
|
||||||
|
|
||||||
|
export const actionToggleShapeSwitch = register({
|
||||||
|
name: "toggleShapeSwitch",
|
||||||
|
label: "labels.shapeSwitch",
|
||||||
|
icon: () => null,
|
||||||
|
viewMode: true,
|
||||||
|
trackEvent: {
|
||||||
|
category: "shape_switch",
|
||||||
|
action: "toggle",
|
||||||
|
},
|
||||||
|
keywords: ["change", "switch", "swap"],
|
||||||
|
perform(elements, appState, _, app) {
|
||||||
|
editorJotaiStore.set(convertElementTypePopupAtom, {
|
||||||
|
type: "panel",
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
captureUpdate: CaptureUpdateAction.NEVER,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
checked: (appState) => appState.gridModeEnabled,
|
||||||
|
predicate: (elements, appState, props) =>
|
||||||
|
getConversionTypeFromElements(elements as ExcalidrawElement[]) !== null,
|
||||||
|
});
|
||||||
@@ -140,7 +140,8 @@ export type ActionName =
|
|||||||
| "linkToElement"
|
| "linkToElement"
|
||||||
| "cropEditor"
|
| "cropEditor"
|
||||||
| "wrapSelectionInFrame"
|
| "wrapSelectionInFrame"
|
||||||
| "toggleLassoTool";
|
| "toggleLassoTool"
|
||||||
|
| "toggleShapeSwitch";
|
||||||
|
|
||||||
export type PanelComponentProps = {
|
export type PanelComponentProps = {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
@@ -195,7 +196,8 @@ export interface Action {
|
|||||||
| "menu"
|
| "menu"
|
||||||
| "collab"
|
| "collab"
|
||||||
| "hyperlink"
|
| "hyperlink"
|
||||||
| "search_menu";
|
| "search_menu"
|
||||||
|
| "shape_switch";
|
||||||
action?: string;
|
action?: string;
|
||||||
predicate?: (
|
predicate?: (
|
||||||
appState: Readonly<AppState>,
|
appState: Readonly<AppState>,
|
||||||
|
|||||||
@@ -129,7 +129,6 @@ export class AnimatedTrail implements Trail {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private update() {
|
private update() {
|
||||||
this.pastTrails = [];
|
|
||||||
this.start();
|
this.start();
|
||||||
if (this.trailAnimation) {
|
if (this.trailAnimation) {
|
||||||
this.trailAnimation.setAttribute("begin", "indefinite");
|
this.trailAnimation.setAttribute("begin", "indefinite");
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ import {
|
|||||||
arrayToMap,
|
arrayToMap,
|
||||||
type EXPORT_IMAGE_TYPES,
|
type EXPORT_IMAGE_TYPES,
|
||||||
randomInteger,
|
randomInteger,
|
||||||
|
CLASSES,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -431,7 +432,7 @@ import {
|
|||||||
} from "../components/hyperlink/Hyperlink";
|
} from "../components/hyperlink/Hyperlink";
|
||||||
|
|
||||||
import { Fonts } from "../fonts";
|
import { Fonts } from "../fonts";
|
||||||
import { editorJotaiStore } from "../editor-jotai";
|
import { editorJotaiStore, type WritableAtom } from "../editor-jotai";
|
||||||
import { ImageSceneDataError } from "../errors";
|
import { ImageSceneDataError } from "../errors";
|
||||||
import {
|
import {
|
||||||
getSnapLinesAtPointer,
|
getSnapLinesAtPointer,
|
||||||
@@ -467,6 +468,12 @@ import { LassoTrail } from "../lasso";
|
|||||||
|
|
||||||
import { EraserTrail } from "../eraser";
|
import { EraserTrail } from "../eraser";
|
||||||
|
|
||||||
|
import ConvertElementTypePopup, {
|
||||||
|
getConversionTypeFromElements,
|
||||||
|
convertElementTypePopupAtom,
|
||||||
|
convertElementTypes,
|
||||||
|
} from "./ConvertElementTypePopup";
|
||||||
|
|
||||||
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
||||||
import BraveMeasureTextError from "./BraveMeasureTextError";
|
import BraveMeasureTextError from "./BraveMeasureTextError";
|
||||||
import { ContextMenu, CONTEXT_MENU_SEPARATOR } from "./ContextMenu";
|
import { ContextMenu, CONTEXT_MENU_SEPARATOR } from "./ContextMenu";
|
||||||
@@ -498,7 +505,6 @@ import type { ExportedElements } from "../data";
|
|||||||
import type { ContextMenuItems } from "./ContextMenu";
|
import type { ContextMenuItems } from "./ContextMenu";
|
||||||
import type { FileSystemHandle } from "../data/filesystem";
|
import type { FileSystemHandle } from "../data/filesystem";
|
||||||
import type { ExcalidrawElementSkeleton } from "../data/transform";
|
import type { ExcalidrawElementSkeleton } from "../data/transform";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AppClassProperties,
|
AppClassProperties,
|
||||||
AppProps,
|
AppProps,
|
||||||
@@ -815,6 +821,15 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateEditorAtom = <Value, Args extends unknown[], Result>(
|
||||||
|
atom: WritableAtom<Value, Args, Result>,
|
||||||
|
...args: Args
|
||||||
|
): Result => {
|
||||||
|
const result = editorJotaiStore.set(atom, ...args);
|
||||||
|
this.triggerRender();
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
private onWindowMessage(event: MessageEvent) {
|
private onWindowMessage(event: MessageEvent) {
|
||||||
if (
|
if (
|
||||||
event.origin !== "https://player.vimeo.com" &&
|
event.origin !== "https://player.vimeo.com" &&
|
||||||
@@ -1583,6 +1598,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
const firstSelectedElement = selectedElements[0];
|
const firstSelectedElement = selectedElements[0];
|
||||||
|
|
||||||
|
const showShapeSwitchPanel =
|
||||||
|
editorJotaiStore.get(convertElementTypePopupAtom)?.type === "panel";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx("excalidraw excalidraw-container", {
|
className={clsx("excalidraw excalidraw-container", {
|
||||||
@@ -1857,6 +1875,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{this.renderFrameNames()}
|
{this.renderFrameNames()}
|
||||||
|
{showShapeSwitchPanel && (
|
||||||
|
<ConvertElementTypePopup app={this} />
|
||||||
|
)}
|
||||||
</ExcalidrawActionManagerContext.Provider>
|
</ExcalidrawActionManagerContext.Provider>
|
||||||
{this.renderEmbeddables()}
|
{this.renderEmbeddables()}
|
||||||
</ExcalidrawElementsContext.Provider>
|
</ExcalidrawElementsContext.Provider>
|
||||||
@@ -2138,7 +2159,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private openEyeDropper = ({ type }: { type: "stroke" | "background" }) => {
|
private openEyeDropper = ({ type }: { type: "stroke" | "background" }) => {
|
||||||
editorJotaiStore.set(activeEyeDropperAtom, {
|
this.updateEditorAtom(activeEyeDropperAtom, {
|
||||||
swapPreviewOnAlt: true,
|
swapPreviewOnAlt: true,
|
||||||
colorPickerType:
|
colorPickerType:
|
||||||
type === "stroke" ? "elementStroke" : "elementBackground",
|
type === "stroke" ? "elementStroke" : "elementBackground",
|
||||||
@@ -4157,6 +4178,40 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shape switching
|
||||||
|
if (event.key === KEYS.ESCAPE) {
|
||||||
|
this.updateEditorAtom(convertElementTypePopupAtom, null);
|
||||||
|
} else if (
|
||||||
|
event.key === KEYS.TAB &&
|
||||||
|
(document.activeElement === this.excalidrawContainerRef?.current ||
|
||||||
|
document.activeElement?.classList.contains(
|
||||||
|
CLASSES.CONVERT_ELEMENT_TYPE_POPUP,
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const conversionType =
|
||||||
|
getConversionTypeFromElements(selectedElements);
|
||||||
|
|
||||||
|
if (
|
||||||
|
editorJotaiStore.get(convertElementTypePopupAtom)?.type === "panel"
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
convertElementTypes(this, {
|
||||||
|
conversionType,
|
||||||
|
direction: event.shiftKey ? "left" : "right",
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
this.store.shouldCaptureIncrement();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (conversionType) {
|
||||||
|
this.updateEditorAtom(convertElementTypePopupAtom, {
|
||||||
|
type: "panel",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
event.key === KEYS.ESCAPE &&
|
event.key === KEYS.ESCAPE &&
|
||||||
this.flowChartCreator.isCreatingChart
|
this.flowChartCreator.isCreatingChart
|
||||||
@@ -4615,7 +4670,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
event[KEYS.CTRL_OR_CMD] &&
|
event[KEYS.CTRL_OR_CMD] &&
|
||||||
(event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE)
|
(event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE)
|
||||||
) {
|
) {
|
||||||
editorJotaiStore.set(activeConfirmDialogAtom, "clearCanvas");
|
this.updateEditorAtom(activeConfirmDialogAtom, "clearCanvas");
|
||||||
}
|
}
|
||||||
|
|
||||||
// eye dropper
|
// eye dropper
|
||||||
@@ -6364,7 +6419,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
focus: false,
|
focus: false,
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
editorJotaiStore.set(searchItemInFocusAtom, null);
|
this.updateEditorAtom(searchItemInFocusAtom, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editorJotaiStore.get(convertElementTypePopupAtom)) {
|
||||||
|
this.updateEditorAtom(convertElementTypePopupAtom, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// since contextMenu options are potentially evaluated on each render,
|
// since contextMenu options are potentially evaluated on each render,
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
isWritableElement,
|
isWritableElement,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
|
import { actionToggleShapeSwitch } from "@excalidraw/excalidraw/actions/actionToggleShapeSwitch";
|
||||||
|
|
||||||
import type { MarkRequired } from "@excalidraw/common/utility-types";
|
import type { MarkRequired } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -410,6 +412,14 @@ function CommandPaletteInner({
|
|||||||
actionManager.executeAction(actionToggleSearchMenu);
|
actionManager.executeAction(actionToggleSearchMenu);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: t("labels.shapeSwitch"),
|
||||||
|
category: DEFAULT_CATEGORIES.elements,
|
||||||
|
icon: boltIcon,
|
||||||
|
perform: () => {
|
||||||
|
actionManager.executeAction(actionToggleShapeSwitch);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: t("labels.changeStroke"),
|
label: t("labels.changeStroke"),
|
||||||
keywords: ["color", "outline"],
|
keywords: ["color", "outline"],
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
@import "../css//variables.module.scss";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
.ConvertElementTypePopup {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.2rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: var(--island-bg-color);
|
||||||
|
box-shadow: var(--shadow-island);
|
||||||
|
padding: 0.5rem;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,7 @@ const Header = () => (
|
|||||||
className="HelpDialog__btn"
|
className="HelpDialog__btn"
|
||||||
href="https://docs.excalidraw.com"
|
href="https://docs.excalidraw.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener"
|
||||||
>
|
>
|
||||||
<div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
|
<div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
|
||||||
{t("helpDialog.documentation")}
|
{t("helpDialog.documentation")}
|
||||||
@@ -30,7 +30,7 @@ const Header = () => (
|
|||||||
className="HelpDialog__btn"
|
className="HelpDialog__btn"
|
||||||
href="https://plus.excalidraw.com/blog"
|
href="https://plus.excalidraw.com/blog"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener"
|
||||||
>
|
>
|
||||||
<div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
|
<div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
|
||||||
{t("helpDialog.blog")}
|
{t("helpDialog.blog")}
|
||||||
@@ -247,6 +247,11 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
label={t("toolBar.link")}
|
label={t("toolBar.link")}
|
||||||
shortcuts={[getShortcutKey("CtrlOrCmd+K")]}
|
shortcuts={[getShortcutKey("CtrlOrCmd+K")]}
|
||||||
/>
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("toolBar.convertElementType")}
|
||||||
|
shortcuts={["Tab", "Shift+Tab"]}
|
||||||
|
isOr={true}
|
||||||
|
/>
|
||||||
</ShortcutIsland>
|
</ShortcutIsland>
|
||||||
<ShortcutIsland
|
<ShortcutIsland
|
||||||
className="HelpDialog__island--view"
|
className="HelpDialog__island--view"
|
||||||
|
|||||||
@@ -389,7 +389,7 @@ const PublishLibrary = ({
|
|||||||
<a
|
<a
|
||||||
href="https://libraries.excalidraw.com"
|
href="https://libraries.excalidraw.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener"
|
||||||
>
|
>
|
||||||
{el}
|
{el}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ import type Scene from "@excalidraw/element/Scene";
|
|||||||
|
|
||||||
import { angleIcon } from "../icons";
|
import { angleIcon } from "../icons";
|
||||||
|
|
||||||
|
import { updateBindings } from "../../../element/src/binding";
|
||||||
|
|
||||||
import DragInput from "./DragInput";
|
import DragInput from "./DragInput";
|
||||||
import { getStepSizedValue, isPropertyEditable, updateBindings } from "./utils";
|
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
||||||
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
|||||||
import type Scene from "@excalidraw/element/Scene";
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import StatsDragInput from "./DragInput";
|
import StatsDragInput from "./DragInput";
|
||||||
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
import {
|
||||||
|
getAtomicUnits,
|
||||||
|
getStepSizedValue,
|
||||||
|
isPropertyEditable,
|
||||||
|
STEP_SIZE,
|
||||||
|
} from "./utils";
|
||||||
import { getElementsInAtomicUnit, moveElement } from "./utils";
|
import { getElementsInAtomicUnit, moveElement } from "./utils";
|
||||||
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
@@ -26,8 +31,6 @@ interface MultiPositionProps {
|
|||||||
appState: AppState;
|
appState: AppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const STEP_SIZE = 10;
|
|
||||||
|
|
||||||
const moveElements = (
|
const moveElements = (
|
||||||
property: MultiPositionProps["property"],
|
property: MultiPositionProps["property"],
|
||||||
changeInTopX: number,
|
changeInTopX: number,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
|||||||
import type Scene from "@excalidraw/element/Scene";
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import StatsDragInput from "./DragInput";
|
import StatsDragInput from "./DragInput";
|
||||||
import { getStepSizedValue, moveElement } from "./utils";
|
import { getStepSizedValue, moveElement, STEP_SIZE } from "./utils";
|
||||||
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
@@ -24,8 +24,6 @@ interface PositionProps {
|
|||||||
appState: AppState;
|
appState: AppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const STEP_SIZE = 10;
|
|
||||||
|
|
||||||
const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||||
accumulatedChange,
|
accumulatedChange,
|
||||||
instantChange,
|
instantChange,
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
import { pointFrom, pointRotateRads } from "@excalidraw/math";
|
import { pointFrom, pointRotateRads } from "@excalidraw/math";
|
||||||
|
|
||||||
import {
|
|
||||||
bindOrUnbindLinearElements,
|
|
||||||
updateBoundElements,
|
|
||||||
} from "@excalidraw/element/binding";
|
|
||||||
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
||||||
import {
|
import {
|
||||||
isFrameLikeElement,
|
isFrameLikeElement,
|
||||||
isLinearElement,
|
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "@excalidraw/element/typeChecks";
|
} from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
@@ -17,6 +12,8 @@ import {
|
|||||||
isInGroup,
|
isInGroup,
|
||||||
} from "@excalidraw/element/groups";
|
} from "@excalidraw/element/groups";
|
||||||
|
|
||||||
|
import { getFrameChildren } from "@excalidraw/element/frame";
|
||||||
|
|
||||||
import type { Radians } from "@excalidraw/math";
|
import type { Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -27,6 +24,8 @@ import type {
|
|||||||
|
|
||||||
import type Scene from "@excalidraw/element/Scene";
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
|
import { updateBindings } from "../../../element/src/binding";
|
||||||
|
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
export type StatsInputProperty =
|
export type StatsInputProperty =
|
||||||
@@ -39,6 +38,7 @@ export type StatsInputProperty =
|
|||||||
| "gridStep";
|
| "gridStep";
|
||||||
|
|
||||||
export const SMALLEST_DELTA = 0.01;
|
export const SMALLEST_DELTA = 0.01;
|
||||||
|
export const STEP_SIZE = 10;
|
||||||
|
|
||||||
export const isPropertyEditable = (
|
export const isPropertyEditable = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
@@ -172,6 +172,68 @@ export const moveElement = (
|
|||||||
{ informMutation: shouldInformMutation, isDragging: false },
|
{ 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 = (
|
export const getAtomicUnits = (
|
||||||
@@ -194,19 +256,3 @@ export const getAtomicUnits = (
|
|||||||
});
|
});
|
||||||
return _atomicUnits;
|
return _atomicUnits;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateBindings = (
|
|
||||||
latestElement: ExcalidrawElement,
|
|
||||||
scene: Scene,
|
|
||||||
options?: {
|
|
||||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
|
||||||
newSize?: { width: number; height: number };
|
|
||||||
zoom?: AppState["zoom"];
|
|
||||||
},
|
|
||||||
) => {
|
|
||||||
if (isLinearElement(latestElement)) {
|
|
||||||
bindOrUnbindLinearElements([latestElement], true, [], scene, options?.zoom);
|
|
||||||
} else {
|
|
||||||
updateBoundElements(latestElement, scene, options);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const DropdownMenuItemLink = ({
|
|||||||
onSelect,
|
onSelect,
|
||||||
className = "",
|
className = "",
|
||||||
selected,
|
selected,
|
||||||
rel = "noreferrer",
|
rel = "noopener",
|
||||||
...rest
|
...rest
|
||||||
}: {
|
}: {
|
||||||
href: string;
|
href: string;
|
||||||
@@ -31,11 +31,12 @@ const DropdownMenuItemLink = ({
|
|||||||
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
// eslint-disable-next-line react/jsx-no-target-blank
|
||||||
<a
|
<a
|
||||||
{...rest}
|
{...rest}
|
||||||
href={href}
|
href={href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel={rel || "noopener"}
|
||||||
className={getDropdownMenuItemClassName(className, selected)}
|
className={getDropdownMenuItemClassName(className, selected)}
|
||||||
title={rest.title ?? rest["aria-label"]}
|
title={rest.title ?? rest["aria-label"]}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ const WelcomeScreenMenuItemLink = ({
|
|||||||
className={`welcome-screen-menu-item ${className}`}
|
className={`welcome-screen-menu-item ${className}`}
|
||||||
href={href}
|
href={href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noopener"
|
||||||
>
|
>
|
||||||
<WelcomeScreenMenuItemContent icon={icon} shortcut={shortcut}>
|
<WelcomeScreenMenuItemContent icon={icon} shortcut={shortcut}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { bumpVersion } from "@excalidraw/element/mutateElement";
|
|||||||
import { getContainerElement } from "@excalidraw/element/textElement";
|
import { getContainerElement } from "@excalidraw/element/textElement";
|
||||||
import { detectLineHeight } from "@excalidraw/element/textMeasurements";
|
import { detectLineHeight } from "@excalidraw/element/textMeasurements";
|
||||||
import {
|
import {
|
||||||
|
isArrowBoundToElement,
|
||||||
isArrowElement,
|
isArrowElement,
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
isFixedPointBinding,
|
isFixedPointBinding,
|
||||||
@@ -594,8 +595,7 @@ export const restoreElements = (
|
|||||||
return restoredElements.map((element) => {
|
return restoredElements.map((element) => {
|
||||||
if (
|
if (
|
||||||
isElbowArrow(element) &&
|
isElbowArrow(element) &&
|
||||||
element.startBinding == null &&
|
!isArrowBoundToElement(element) &&
|
||||||
element.endBinding == null &&
|
|
||||||
!validateElbowPoints(element.points)
|
!validateElbowPoints(element.points)
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { atom, createStore, type PrimitiveAtom } from "jotai";
|
import {
|
||||||
|
atom,
|
||||||
|
createStore,
|
||||||
|
type PrimitiveAtom,
|
||||||
|
type WritableAtom,
|
||||||
|
} from "jotai";
|
||||||
import { createIsolation } from "jotai-scope";
|
import { createIsolation } from "jotai-scope";
|
||||||
|
|
||||||
const jotai = createIsolation();
|
const jotai = createIsolation();
|
||||||
|
|
||||||
export { atom, PrimitiveAtom };
|
export { atom, PrimitiveAtom, WritableAtom };
|
||||||
export const { useAtom, useSetAtom, useAtomValue, useStore } = jotai;
|
export const { useAtom, useSetAtom, useAtomValue, useStore } = jotai;
|
||||||
export const EditorJotaiProvider: ReturnType<
|
export const EditorJotaiProvider: ReturnType<
|
||||||
typeof createIsolation
|
typeof createIsolation
|
||||||
|
|||||||
@@ -165,7 +165,9 @@
|
|||||||
"unCroppedDimension": "Uncropped dimension",
|
"unCroppedDimension": "Uncropped dimension",
|
||||||
"copyElementLink": "Copy link to object",
|
"copyElementLink": "Copy link to object",
|
||||||
"linkToElement": "Link to object",
|
"linkToElement": "Link to object",
|
||||||
"wrapSelectionInFrame": "Wrap selection in frame"
|
"wrapSelectionInFrame": "Wrap selection in frame",
|
||||||
|
"tab": "Tab",
|
||||||
|
"shapeSwitch": "Switch shape"
|
||||||
},
|
},
|
||||||
"elementLink": {
|
"elementLink": {
|
||||||
"title": "Link to object",
|
"title": "Link to object",
|
||||||
@@ -296,7 +298,8 @@
|
|||||||
"laser": "Laser pointer",
|
"laser": "Laser pointer",
|
||||||
"hand": "Hand (panning tool)",
|
"hand": "Hand (panning tool)",
|
||||||
"extraTools": "More tools",
|
"extraTools": "More tools",
|
||||||
"mermaidToExcalidraw": "Mermaid to Excalidraw"
|
"mermaidToExcalidraw": "Mermaid to Excalidraw",
|
||||||
|
"convertElementType": "Toggle shape type"
|
||||||
},
|
},
|
||||||
"element": {
|
"element": {
|
||||||
"rectangle": "Rectangle",
|
"rectangle": "Rectangle",
|
||||||
|
|||||||
@@ -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 { StaticCanvasRenderConfig } from "../scene/types";
|
||||||
import type { StaticCanvasAppState, AppState } from "../types";
|
import type { AppState, StaticCanvasAppState } from "../types";
|
||||||
|
|
||||||
export const fillCircle = (
|
export const fillCircle = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
@@ -72,3 +95,399 @@ export const bootstrapCanvas = ({
|
|||||||
|
|
||||||
return context;
|
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 {
|
import {
|
||||||
pointFrom,
|
pointFrom,
|
||||||
type GlobalPoint,
|
type GlobalPoint,
|
||||||
type LocalPoint,
|
type LocalPoint,
|
||||||
type Radians,
|
type Radians,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
import oc from "open-color";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
arrayToMap,
|
||||||
DEFAULT_TRANSFORM_HANDLE_SPACING,
|
DEFAULT_TRANSFORM_HANDLE_SPACING,
|
||||||
FRAME_STYLE,
|
FRAME_STYLE,
|
||||||
THEME,
|
|
||||||
arrayToMap,
|
|
||||||
invariant,
|
invariant,
|
||||||
|
THEME,
|
||||||
throttleRAF,
|
throttleRAF,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BINDING_HIGHLIGHT_OFFSET,
|
FIXED_BINDING_DISTANCE,
|
||||||
BINDING_HIGHLIGHT_THICKNESS,
|
|
||||||
maxBindingGap,
|
maxBindingGap,
|
||||||
} from "@excalidraw/element/binding";
|
} from "@excalidraw/element/binding";
|
||||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||||
@@ -35,14 +34,12 @@ import {
|
|||||||
isTextElement,
|
isTextElement,
|
||||||
} from "@excalidraw/element/typeChecks";
|
} from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
import { getCornerRadius } from "@excalidraw/element/shapes";
|
|
||||||
|
|
||||||
import { renderSelectionElement } from "@excalidraw/element/renderElement";
|
import { renderSelectionElement } from "@excalidraw/element/renderElement";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isSelectedViaGroup,
|
|
||||||
getSelectedGroupIds,
|
|
||||||
getElementsInGroup,
|
getElementsInGroup,
|
||||||
|
getSelectedGroupIds,
|
||||||
|
isSelectedViaGroup,
|
||||||
selectGroupsFromGivenElements,
|
selectGroupsFromGivenElements,
|
||||||
} from "@excalidraw/element/groups";
|
} from "@excalidraw/element/groups";
|
||||||
|
|
||||||
@@ -86,8 +83,12 @@ import { getClientColor, renderRemoteCursors } from "../clients";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
bootstrapCanvas,
|
bootstrapCanvas,
|
||||||
|
drawHighlightForDiamondWithRotation,
|
||||||
|
drawHighlightForRectWithRotation,
|
||||||
fillCircle,
|
fillCircle,
|
||||||
getNormalizedCanvasDimensions,
|
getNormalizedCanvasDimensions,
|
||||||
|
strokeEllipseWithRotation,
|
||||||
|
strokeRectWithRotation,
|
||||||
} from "./helpers";
|
} from "./helpers";
|
||||||
|
|
||||||
import type {
|
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>(
|
const renderSingleLinearPoint = <Point extends GlobalPoint | LocalPoint>(
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
appState: InteractiveCanvasAppState,
|
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 = (
|
const renderBindingHighlightForBindableElement = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
element: ExcalidrawBindableElement,
|
element: ExcalidrawBindableElement,
|
||||||
@@ -261,16 +198,10 @@ const renderBindingHighlightForBindableElement = (
|
|||||||
const height = y2 - y1;
|
const height = y2 - y1;
|
||||||
|
|
||||||
context.strokeStyle = "rgba(0,0,0,.05)";
|
context.strokeStyle = "rgba(0,0,0,.05)";
|
||||||
// When zooming out, make line width greater for visibility
|
context.fillStyle = "rgba(0,0,0,.05)";
|
||||||
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;
|
|
||||||
|
|
||||||
const radius = getCornerRadius(
|
// To ensure the binding highlight doesn't overlap the element itself
|
||||||
Math.min(element.width, element.height),
|
const padding = maxBindingGap(element, element.width, element.height, zoom);
|
||||||
element,
|
|
||||||
);
|
|
||||||
|
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
@@ -280,37 +211,20 @@ const renderBindingHighlightForBindableElement = (
|
|||||||
case "embeddable":
|
case "embeddable":
|
||||||
case "frame":
|
case "frame":
|
||||||
case "magicframe":
|
case "magicframe":
|
||||||
strokeRectWithRotation(
|
drawHighlightForRectWithRotation(context, element, padding);
|
||||||
context,
|
|
||||||
x1 - padding,
|
|
||||||
y1 - padding,
|
|
||||||
width + padding * 2,
|
|
||||||
height + padding * 2,
|
|
||||||
x1 + width / 2,
|
|
||||||
y1 + height / 2,
|
|
||||||
element.angle,
|
|
||||||
undefined,
|
|
||||||
radius,
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case "diamond":
|
case "diamond":
|
||||||
const side = Math.hypot(width, height);
|
drawHighlightForDiamondWithRotation(context, padding, element);
|
||||||
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,
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case "ellipse":
|
case "ellipse":
|
||||||
|
context.lineWidth =
|
||||||
|
maxBindingGap(element, element.width, element.height, zoom) -
|
||||||
|
FIXED_BINDING_DISTANCE;
|
||||||
|
|
||||||
strokeEllipseWithRotation(
|
strokeEllipseWithRotation(
|
||||||
context,
|
context,
|
||||||
width + padding * 2,
|
width + padding + FIXED_BINDING_DISTANCE,
|
||||||
height + padding * 2,
|
height + padding + FIXED_BINDING_DISTANCE,
|
||||||
x1 + width / 2,
|
x1 + width / 2,
|
||||||
y1 + height / 2,
|
y1 + height / 2,
|
||||||
element.angle,
|
element.angle,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
|
|||||||
<a
|
<a
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
class="dropdown-menu-item dropdown-menu-item-base"
|
||||||
href="blog.excalidaw.com"
|
href="blog.excalidaw.com"
|
||||||
rel="noreferrer"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -392,7 +392,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||||||
aria-label="GitHub"
|
aria-label="GitHub"
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
class="dropdown-menu-item dropdown-menu-item-base"
|
||||||
href="https://github.com/excalidraw/excalidraw"
|
href="https://github.com/excalidraw/excalidraw"
|
||||||
rel="noreferrer"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
title="GitHub"
|
title="GitHub"
|
||||||
>
|
>
|
||||||
@@ -426,7 +426,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||||||
aria-label="X"
|
aria-label="X"
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
class="dropdown-menu-item dropdown-menu-item-base"
|
||||||
href="https://x.com/excalidraw"
|
href="https://x.com/excalidraw"
|
||||||
rel="noreferrer"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
title="X"
|
title="X"
|
||||||
>
|
>
|
||||||
@@ -472,7 +472,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||||||
aria-label="Discord"
|
aria-label="Discord"
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
class="dropdown-menu-item dropdown-menu-item-base"
|
||||||
href="https://discord.gg/UexuTaE"
|
href="https://discord.gg/UexuTaE"
|
||||||
rel="noreferrer"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
title="Discord"
|
title="Discord"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 19,
|
"version": 9,
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 100,
|
"x": 100,
|
||||||
"y": -50,
|
"y": -50,
|
||||||
@@ -198,7 +198,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": "102.35417",
|
"height": "102.45605",
|
||||||
"id": "id172",
|
"id": "id172",
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@@ -212,8 +212,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"101.77517",
|
"102.80179",
|
||||||
"102.35417",
|
"102.45605",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@@ -227,9 +227,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 40,
|
"version": 37,
|
||||||
"width": "101.77517",
|
"width": "102.80179",
|
||||||
"x": "0.70711",
|
"x": "-0.42182",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -264,7 +264,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 6,
|
"version": 14,
|
||||||
"width": 50,
|
"width": 50,
|
||||||
"x": 100,
|
"x": 100,
|
||||||
"y": 100,
|
"y": 100,
|
||||||
@@ -291,22 +291,39 @@ History {
|
|||||||
"added": Map {},
|
"added": Map {},
|
||||||
"removed": Map {},
|
"removed": Map {},
|
||||||
"updated": Map {
|
"updated": Map {
|
||||||
|
"id171" => Delta {
|
||||||
|
"deleted": {
|
||||||
|
"boundElements": [],
|
||||||
|
},
|
||||||
|
"inserted": {
|
||||||
|
"boundElements": [
|
||||||
|
{
|
||||||
|
"id": "id172",
|
||||||
|
"type": "arrow",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
"id172" => Delta {
|
"id172" => Delta {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id171",
|
"elementId": "id175",
|
||||||
"focus": "0.00990",
|
"fixedPoint": [
|
||||||
|
"0.50000",
|
||||||
|
1,
|
||||||
|
],
|
||||||
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"height": "0.98586",
|
"height": "70.45017",
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.58579",
|
"100.70774",
|
||||||
"-0.98586",
|
"70.45017",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
@@ -321,7 +338,7 @@ History {
|
|||||||
"focus": "-0.02000",
|
"focus": "-0.02000",
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"height": "0.00000",
|
"height": "0.09250",
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
@@ -329,7 +346,7 @@ History {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.58579",
|
"98.58579",
|
||||||
"0.00000",
|
"0.09250",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
@@ -339,6 +356,19 @@ History {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"id175" => Delta {
|
||||||
|
"deleted": {
|
||||||
|
"boundElements": [
|
||||||
|
{
|
||||||
|
"id": "id172",
|
||||||
|
"type": "arrow",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"inserted": {
|
||||||
|
"boundElements": [],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -366,59 +396,32 @@ History {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"id171" => Delta {
|
|
||||||
"deleted": {
|
|
||||||
"boundElements": [],
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"boundElements": [
|
|
||||||
{
|
|
||||||
"id": "id172",
|
|
||||||
"type": "arrow",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"id172" => Delta {
|
"id172" => Delta {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"endBinding": {
|
"height": "102.45584",
|
||||||
"elementId": "id175",
|
|
||||||
"fixedPoint": [
|
|
||||||
"0.50000",
|
|
||||||
1,
|
|
||||||
],
|
|
||||||
"focus": 0,
|
|
||||||
"gap": 1,
|
|
||||||
},
|
|
||||||
"height": "102.35417",
|
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"101.77517",
|
"102.79971",
|
||||||
"102.35417",
|
"102.45584",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"endBinding": {
|
"height": "70.33521",
|
||||||
"elementId": "id171",
|
|
||||||
"focus": "0.00990",
|
|
||||||
"gap": 1,
|
|
||||||
},
|
|
||||||
"height": "0.98586",
|
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.58579",
|
"100.78887",
|
||||||
"-0.98586",
|
"70.33521",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
@@ -426,20 +429,7 @@ History {
|
|||||||
"focus": "0.02970",
|
"focus": "0.02970",
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"y": "0.99364",
|
"y": "35.20327",
|
||||||
},
|
|
||||||
},
|
|
||||||
"id175" => Delta {
|
|
||||||
"deleted": {
|
|
||||||
"boundElements": [
|
|
||||||
{
|
|
||||||
"id": "id172",
|
|
||||||
"type": "arrow",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"boundElements": [],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -739,7 +729,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 9,
|
"version": 19,
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 150,
|
"x": 150,
|
||||||
"y": -50,
|
"y": -50,
|
||||||
@@ -819,8 +809,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 30,
|
"version": 33,
|
||||||
"width": 0,
|
"width": 100,
|
||||||
"x": "149.29289",
|
"x": "149.29289",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
@@ -846,20 +836,22 @@ History {
|
|||||||
"added": Map {},
|
"added": Map {},
|
||||||
"removed": Map {},
|
"removed": Map {},
|
||||||
"updated": Map {
|
"updated": Map {
|
||||||
"id167" => Delta {
|
"id166" => Delta {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"points": [
|
"boundElements": [],
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
|
"boundElements": [
|
||||||
|
{
|
||||||
|
"id": "id167",
|
||||||
|
"type": "arrow",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"id167" => Delta {
|
||||||
|
"deleted": {
|
||||||
|
"endBinding": null,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
@@ -871,6 +863,23 @@ History {
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
"inserted": {
|
||||||
|
"endBinding": {
|
||||||
|
"elementId": "id166",
|
||||||
|
"focus": -0,
|
||||||
|
"gap": 1,
|
||||||
|
},
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -899,22 +908,8 @@ History {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"id166" => Delta {
|
|
||||||
"deleted": {
|
|
||||||
"boundElements": [],
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"boundElements": [
|
|
||||||
{
|
|
||||||
"id": "id167",
|
|
||||||
"type": "arrow",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"id167" => Delta {
|
"id167" => Delta {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"endBinding": null,
|
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
@@ -928,18 +923,13 @@ History {
|
|||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"endBinding": {
|
|
||||||
"elementId": "id166",
|
|
||||||
"focus": -0,
|
|
||||||
"gap": 1,
|
|
||||||
},
|
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
0,
|
100,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ export type AppClassProperties = {
|
|||||||
excalidrawContainerValue: App["excalidrawContainerValue"];
|
excalidrawContainerValue: App["excalidrawContainerValue"];
|
||||||
|
|
||||||
onPointerUpEmitter: App["onPointerUpEmitter"];
|
onPointerUpEmitter: App["onPointerUpEmitter"];
|
||||||
|
updateEditorAtom: App["updateEditorAtom"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PointerDownState = Readonly<{
|
export type PointerDownState = Readonly<{
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ const getTransform = (
|
|||||||
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
|
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SubmitHandler = () => void;
|
||||||
|
|
||||||
export const textWysiwyg = ({
|
export const textWysiwyg = ({
|
||||||
id,
|
id,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -106,7 +108,7 @@ export const textWysiwyg = ({
|
|||||||
excalidrawContainer: HTMLDivElement | null;
|
excalidrawContainer: HTMLDivElement | null;
|
||||||
app: App;
|
app: App;
|
||||||
autoSelect?: boolean;
|
autoSelect?: boolean;
|
||||||
}) => {
|
}): SubmitHandler => {
|
||||||
const textPropertiesUpdated = (
|
const textPropertiesUpdated = (
|
||||||
updatedTextElement: ExcalidrawTextElement,
|
updatedTextElement: ExcalidrawTextElement,
|
||||||
editable: HTMLTextAreaElement,
|
editable: HTMLTextAreaElement,
|
||||||
@@ -186,7 +188,6 @@ export const textWysiwyg = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
maxWidth = getBoundTextMaxWidth(container, updatedTextElement);
|
maxWidth = getBoundTextMaxWidth(container, updatedTextElement);
|
||||||
|
|
||||||
maxHeight = getBoundTextMaxHeight(
|
maxHeight = getBoundTextMaxHeight(
|
||||||
container,
|
container,
|
||||||
updatedTextElement as ExcalidrawTextElementWithContainer,
|
updatedTextElement as ExcalidrawTextElementWithContainer,
|
||||||
@@ -735,4 +736,6 @@ export const textWysiwyg = ({
|
|||||||
excalidrawContainer
|
excalidrawContainer
|
||||||
?.querySelector(".excalidraw-textEditorContainer")!
|
?.querySelector(".excalidraw-textEditorContainer")!
|
||||||
.appendChild(editable);
|
.appendChild(editable);
|
||||||
|
|
||||||
|
return handleSubmit;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { Bounds } from "@excalidraw/element/bounds";
|
|||||||
|
|
||||||
import { isPoint, pointDistance, pointFrom } from "./point";
|
import { isPoint, pointDistance, pointFrom } from "./point";
|
||||||
import { rectangle, rectangleIntersectLineSegment } from "./rectangle";
|
import { rectangle, rectangleIntersectLineSegment } from "./rectangle";
|
||||||
|
import { vector } from "./vector";
|
||||||
|
|
||||||
import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types";
|
import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types";
|
||||||
|
|
||||||
@@ -82,7 +83,7 @@ function solve(
|
|||||||
return [t0, s0];
|
return [t0, s0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const bezierEquation = <Point extends GlobalPoint | LocalPoint>(
|
export const bezierEquation = <Point extends GlobalPoint | LocalPoint>(
|
||||||
c: Curve<Point>,
|
c: Curve<Point>,
|
||||||
t: number,
|
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>(
|
function curveBounds<Point extends GlobalPoint | LocalPoint>(
|
||||||
c: Curve<Point>,
|
c: Curve<Point>,
|
||||||
): Bounds {
|
): Bounds {
|
||||||
|
|||||||
@@ -143,3 +143,8 @@ export const vectorNormalize = (v: Vector): Vector => {
|
|||||||
|
|
||||||
return vector(v[0] / m, v[1] / m);
|
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]);
|
||||||
|
|||||||
Reference in New Issue
Block a user