Compare commits

..

10 Commits

Author SHA1 Message Date
Ryan Di be5d0bd925 refactor 2025-12-31 14:53:20 +11:00
Ryan Di 292aa1cddb fix: to support wrapping with autoResize and given dimensions 2025-12-29 17:35:00 +11:00
Ryan Di e95222ed32 fix: add constants and side methods to packages (#10418)
* fix: add constants and side methods to packages

* add transform to the element package

* lint

* remove dead code

* put transform types back to transform.ts

* fix imports

* fix imports in test

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2025-12-28 21:53:25 +01:00
Márk Tolmács d87620b239 fix: Circular reference (#10544)
* fix: Circular reference

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Lint

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* Trigger CI

---------

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
2025-12-21 22:14:21 +01:00
Viczián András 7cc31ac64a fix: Context menu paste adding image twice #10542 (#10543)
removed line that was adding image file twice to paste
2025-12-20 06:31:02 +01:00
zsviczian 071b17a217 fix: Embeddables lost stroke color option in element properties after #9996 (#10541)
Add 'embeddable' type to comparisons
2025-12-19 18:23:09 +01:00
Márk Tolmács 859207b8bc fix: Broken bindings during collab (#10537)
* fix: Broken bindings during collab

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* move repair of non-legacy binding outside the migration branch

---------

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2025-12-17 16:22:24 +01:00
David Luzar becaabfa0f chore: bump node@20 in ci workflows (#10531) 2025-12-16 19:08:42 +01:00
Márk Tolmács f06484c6ab fix: Angle snapping around bindable objects incorrectly resolves (#10501)
Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
Co-authored-by: zsviczian <viczian.zsolt@gmail.com>
2025-12-15 09:49:46 +00:00
Márk Tolmács bf4c65f483 fix: Turn into inside bind when angle locked (#10479)
Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
2025-12-09 19:36:28 +01:00
48 changed files with 726 additions and 189 deletions
+2 -2
View File
@@ -12,10 +12,10 @@ jobs:
- uses: actions/checkout@v2
with:
fetch-depth: 2
- name: Setup Node.js 18.x
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.x
node-version: 20.x
- name: Set up publish access
run: |
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
+2 -2
View File
@@ -9,10 +9,10 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Setup Node.js 18.x
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.x
node-version: 20.x
- name: Install and lint
run: |
+2 -2
View File
@@ -14,10 +14,10 @@ jobs:
with:
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
- name: Setup Node.js 18.x
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.x
node-version: 20.x
- name: Create report file
run: |
+2 -2
View File
@@ -10,10 +10,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js 18.x
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.x
node-version: 20.x
- name: Install and build
run: |
yarn --frozen-lockfile
+2 -2
View File
@@ -11,10 +11,10 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup Node.js 18.x
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18.x
node-version: 20.x
- name: Install in packages/excalidraw
run: yarn
working-directory: packages/excalidraw
+4 -1
View File
@@ -746,7 +746,10 @@ class Collab extends PureComponent<CollabProps, CollabState> {
): ReconciledExcalidrawElement[] => {
const localElements = this.getSceneElementsIncludingDeleted();
const appState = this.excalidrawAPI.getAppState();
const restoredRemoteElements = restoreElements(remoteElements, null);
const restoredRemoteElements = restoreElements(
remoteElements,
this.excalidrawAPI.getSceneElementsMapIncludingDeleted(),
);
const reconciledElements = reconcileElements(
localElements,
restoredRemoteElements as RemoteExcalidrawElement[],
+17
View File
@@ -0,0 +1,17 @@
/**
* x and y position of top left corner, x and y position of bottom right corner
*/
export type Bounds = readonly [
minX: number,
minY: number,
maxX: number,
maxY: number,
];
export const isBounds = (box: unknown): box is Bounds =>
Array.isArray(box) &&
box.length === 4 &&
typeof box[0] === "number" &&
typeof box[1] === "number" &&
typeof box[2] === "number" &&
typeof box[3] === "number";
+7
View File
@@ -108,6 +108,13 @@ export const CLASSES = {
FRAME_NAME: "frame-name",
};
export const FONT_SIZES = {
sm: 16,
md: 20,
lg: 28,
xl: 36,
} as const;
export const CJK_HAND_DRAWN_FALLBACK_FONT = "Xiaolai";
export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
+4 -7
View File
@@ -1,5 +1,3 @@
import mobile from "is-mobile";
export type StylesPanelMode = "compact" | "full" | "mobile";
export type EditorInterface = Readonly<{
@@ -141,13 +139,12 @@ export const getFormFactor = (
editorWidth: number,
editorHeight: number,
): EditorInterface["formFactor"] => {
if (mobile()) {
if (isMobileBreakpoint(editorWidth, editorHeight)) {
return "phone";
} else if (mobile({ tablet: true })) {
}
if (isTabletBreakpoint(editorWidth, editorHeight)) {
return "tablet";
} else if (isMobileBreakpoint(editorWidth, editorHeight)) {
// NOTE: Very small editor sizes should be treated as phone
return "phone";
}
return "desktop";
+1
View File
@@ -1,4 +1,5 @@
export * from "./binary-heap";
export * from "./bounds";
export * from "./colors";
export * from "./constants";
export * from "./font-metadata";
+1 -3
View File
@@ -6,12 +6,10 @@ import {
type LocalPoint,
} from "@excalidraw/math";
import { isBounds } from "@excalidraw/element";
import type { Curve } from "@excalidraw/math";
import type { LineSegment } from "@excalidraw/utils";
import type { Bounds } from "@excalidraw/element";
import { type Bounds, isBounds } from "./bounds";
// The global data holder to collect the debug operations
declare global {
@@ -1,11 +1,12 @@
import { pointFrom } from "@excalidraw/math";
import { vi } from "vitest";
import type { ExcalidrawArrowElement } from "@excalidraw/element/types";
import {
convertToExcalidrawElements,
type ExcalidrawElementSkeleton,
} from "../transform";
import { convertToExcalidrawElements } from "./transform";
import type { ExcalidrawElementSkeleton } from "./transform";
import type { ExcalidrawArrowElement } from "../types";
const opts = { regenerateIds: false };
+490 -8
View File
@@ -22,10 +22,9 @@ import {
} from "@excalidraw/math";
import type { LineSegment, LocalPoint, Radians } from "@excalidraw/math";
import type { AppState } from "@excalidraw/excalidraw/types";
import type { MapEntry, Mutable } from "@excalidraw/common/utility-types";
import type { Bounds } from "@excalidraw/common";
import {
doBoundsIntersect,
@@ -54,17 +53,21 @@ import {
isBindableElement,
isBoundToContainer,
isElbowArrow,
isRectangularElement,
isRectanguloidElement,
isTextElement,
} from "./typeChecks";
import { aabbForElement, elementCenterPoint } from "./bounds";
import { updateElbowArrowPoints } from "./elbowArrow";
import { projectFixedPointOntoDiagonal } from "./utils";
import {
deconstructDiamondElement,
deconstructRectanguloidElement,
projectFixedPointOntoDiagonal,
} from "./utils";
import type { Scene } from "./Scene";
import type { Bounds } from "./bounds";
import type { ElementUpdate } from "./mutateElement";
import type {
BindMode,
@@ -73,6 +76,7 @@ import type {
ExcalidrawBindableElement,
ExcalidrawElbowArrowElement,
ExcalidrawElement,
ExcalidrawRectanguloidElement,
ExcalidrawTextElement,
FixedPoint,
FixedPointBinding,
@@ -146,17 +150,22 @@ export const isBindingEnabled = (appState: AppState): boolean => {
export const bindOrUnbindBindingElement = (
arrow: NonDeleted<ExcalidrawArrowElement>,
draggingPoints: PointsPositionUpdates,
scenePointerX: number,
scenePointerY: number,
scene: Scene,
appState: AppState,
opts?: {
newArrow?: boolean;
altKey?: boolean;
angleLocked?: boolean;
initialBinding?: boolean;
},
) => {
const { start, end } = getBindingStrategyForDraggingBindingElementEndpoints(
arrow,
draggingPoints,
scenePointerX,
scenePointerY,
scene.getNonDeletedElementsMap(),
scene.getNonDeletedElements(),
appState,
@@ -556,12 +565,14 @@ const bindingStrategyForSimpleArrowEndpointDragging_complex = (
export const getBindingStrategyForDraggingBindingElementEndpoints = (
arrow: NonDeleted<ExcalidrawArrowElement>,
draggingPoints: PointsPositionUpdates,
screenPointerX: number,
screenPointerY: number,
elementsMap: NonDeletedSceneElementsMap,
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
appState: AppState,
opts?: {
newArrow?: boolean;
shiftKey?: boolean;
angleLocked?: boolean;
altKey?: boolean;
finalize?: boolean;
initialBinding?: boolean;
@@ -582,6 +593,8 @@ export const getBindingStrategyForDraggingBindingElementEndpoints = (
return getBindingStrategyForDraggingBindingElementEndpoints_simple(
arrow,
draggingPoints,
screenPointerX,
screenPointerY,
elementsMap,
elements,
appState,
@@ -592,12 +605,14 @@ export const getBindingStrategyForDraggingBindingElementEndpoints = (
const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
arrow: NonDeleted<ExcalidrawArrowElement>,
draggingPoints: PointsPositionUpdates,
scenePointerX: number,
scenePointerY: number,
elementsMap: NonDeletedSceneElementsMap,
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
appState: AppState,
opts?: {
newArrow?: boolean;
shiftKey?: boolean;
angleLocked?: boolean;
altKey?: boolean;
finalize?: boolean;
initialBinding?: boolean;
@@ -669,7 +684,15 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
elementsMap,
(e) => maxBindingDistance_simple(appState.zoom),
);
const pointInElement = hit && isPointInElement(globalPoint, hit, elementsMap);
const pointInElement =
hit &&
(opts?.angleLocked
? isPointInElement(
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
hit,
elementsMap,
)
: isPointInElement(globalPoint, hit, elementsMap));
const otherBindableElement = otherBinding
? (elementsMap.get(
otherBinding.elementId,
@@ -770,6 +793,12 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
}
: { mode: null };
const otherEndpoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
arrow,
startDragged ? -1 : 0,
elementsMap,
);
const other: BindingStrategy =
otherBindableElement &&
!otherFocusPointIsInElement &&
@@ -779,6 +808,19 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
element: otherBindableElement,
focusPoint: appState.selectedLinearElement.initialState.altFocusPoint,
}
: opts?.angleLocked && otherBindableElement
? {
mode: "orbit",
element: otherBindableElement,
focusPoint:
projectFixedPointOntoDiagonal(
arrow,
otherEndpoint,
otherBindableElement,
startDragged ? "end" : "start",
elementsMap,
) || otherEndpoint,
}
: { mode: undefined };
return {
@@ -924,6 +966,8 @@ export const bindOrUnbindBindingElements = (
bindOrUnbindBindingElement(
arrow,
new Map(), // No dragging points in this case
Infinity,
Infinity,
scene,
appState,
);
@@ -1126,7 +1170,14 @@ export const updateBindings = (
},
) => {
if (isArrowElement(latestElement)) {
bindOrUnbindBindingElement(latestElement, new Map(), scene, appState);
bindOrUnbindBindingElement(
latestElement,
new Map(),
Infinity,
Infinity,
scene,
appState,
);
} else {
updateBoundElements(latestElement, scene, {
...options,
@@ -2289,3 +2340,434 @@ export const normalizeFixedPoint = <T extends FixedPoint | null>(
}
return fixedPoint as any as T extends null ? null : FixedPoint;
};
type Side =
| "top"
| "top-right"
| "right"
| "bottom-right"
| "bottom"
| "bottom-left"
| "left"
| "top-left";
type ShapeType = "rectangle" | "ellipse" | "diamond";
const getShapeType = (element: ExcalidrawBindableElement): ShapeType => {
if (element.type === "ellipse" || element.type === "diamond") {
return element.type;
}
return "rectangle";
};
interface SectorConfig {
// center angle of the sector in degrees
centerAngle: number;
// width of the sector in degrees
sectorWidth: number;
side: Side;
}
// Define sector configurations for different shape types
const SHAPE_CONFIGS: Record<ShapeType, SectorConfig[]> = {
// rectangle: 15° corners, 75° edges
rectangle: [
{ centerAngle: 0, sectorWidth: 75, side: "right" },
{ centerAngle: 45, sectorWidth: 15, side: "bottom-right" },
{ centerAngle: 90, sectorWidth: 75, side: "bottom" },
{ centerAngle: 135, sectorWidth: 15, side: "bottom-left" },
{ centerAngle: 180, sectorWidth: 75, side: "left" },
{ centerAngle: 225, sectorWidth: 15, side: "top-left" },
{ centerAngle: 270, sectorWidth: 75, side: "top" },
{ centerAngle: 315, sectorWidth: 15, side: "top-right" },
],
// diamond: 15° vertices, 75° edges
diamond: [
{ centerAngle: 0, sectorWidth: 15, side: "right" },
{ centerAngle: 45, sectorWidth: 75, side: "bottom-right" },
{ centerAngle: 90, sectorWidth: 15, side: "bottom" },
{ centerAngle: 135, sectorWidth: 75, side: "bottom-left" },
{ centerAngle: 180, sectorWidth: 15, side: "left" },
{ centerAngle: 225, sectorWidth: 75, side: "top-left" },
{ centerAngle: 270, sectorWidth: 15, side: "top" },
{ centerAngle: 315, sectorWidth: 75, side: "top-right" },
],
// ellipse: 15° cardinal points, 75° diagonals
ellipse: [
{ centerAngle: 0, sectorWidth: 15, side: "right" },
{ centerAngle: 45, sectorWidth: 75, side: "bottom-right" },
{ centerAngle: 90, sectorWidth: 15, side: "bottom" },
{ centerAngle: 135, sectorWidth: 75, side: "bottom-left" },
{ centerAngle: 180, sectorWidth: 15, side: "left" },
{ centerAngle: 225, sectorWidth: 75, side: "top-left" },
{ centerAngle: 270, sectorWidth: 15, side: "top" },
{ centerAngle: 315, sectorWidth: 75, side: "top-right" },
],
};
const getSectorBoundaries = (
config: SectorConfig[],
): Array<{ start: number; end: number; side: Side }> => {
return config.map((sector, index) => {
const halfWidth = sector.sectorWidth / 2;
let start = sector.centerAngle - halfWidth;
let end = sector.centerAngle + halfWidth;
// normalize angles to [0, 360) range
start = ((start % 360) + 360) % 360;
end = ((end % 360) + 360) % 360;
return { start, end, side: sector.side };
});
};
// determine which side a point falls into using adaptive sectors
const getShapeSideAdaptive = (
fixedPoint: FixedPoint,
shapeType: ShapeType,
): Side => {
const [x, y] = fixedPoint;
// convert to centered coordinates
const centerX = x - 0.5;
const centerY = y - 0.5;
// calculate angle
let angle = Math.atan2(centerY, centerX);
if (angle < 0) {
angle += 2 * Math.PI;
}
const degrees = (angle * 180) / Math.PI;
// get sector configuration for this shape type
const config = SHAPE_CONFIGS[shapeType];
const boundaries = getSectorBoundaries(config);
// find which sector the angle falls into
for (const boundary of boundaries) {
if (boundary.start <= boundary.end) {
// Normal case: sector doesn't cross 0°
if (degrees >= boundary.start && degrees <= boundary.end) {
return boundary.side;
}
} else if (degrees >= boundary.start || degrees <= boundary.end) {
return boundary.side;
}
}
// fallback - find nearest sector center
let minDiff = Infinity;
let nearestSide = config[0].side;
for (const sector of config) {
let diff = Math.abs(degrees - sector.centerAngle);
// handle wraparound
if (diff > 180) {
diff = 360 - diff;
}
if (diff < minDiff) {
minDiff = diff;
nearestSide = sector.side;
}
}
return nearestSide;
};
export const getBindingSideMidPoint = (
binding: FixedPointBinding,
elementsMap: ElementsMap,
) => {
const bindableElement = elementsMap.get(binding.elementId);
if (
!bindableElement ||
bindableElement.isDeleted ||
!isBindableElement(bindableElement)
) {
return null;
}
const center = elementCenterPoint(bindableElement, elementsMap);
const shapeType = getShapeType(bindableElement);
const side = getShapeSideAdaptive(
normalizeFixedPoint(binding.fixedPoint),
shapeType,
);
// small offset to avoid precision issues in elbow
const OFFSET = 0.01;
if (bindableElement.type === "diamond") {
const [sides, corners] = deconstructDiamondElement(bindableElement);
const [bottomRight, bottomLeft, topLeft, topRight] = sides;
let x: number;
let y: number;
switch (side) {
case "left": {
// left vertex - use the center of the left corner curve
if (corners.length >= 3) {
const leftCorner = corners[2];
const midPoint = leftCorner[1];
x = midPoint[0] - OFFSET;
y = midPoint[1];
} else {
// fallback for non-rounded diamond
const midPoint = getMidPoint(bottomLeft[1], topLeft[0]);
x = midPoint[0] - OFFSET;
y = midPoint[1];
}
break;
}
case "right": {
if (corners.length >= 1) {
const rightCorner = corners[0];
const midPoint = rightCorner[1];
x = midPoint[0] + OFFSET;
y = midPoint[1];
} else {
const midPoint = getMidPoint(topRight[1], bottomRight[0]);
x = midPoint[0] + OFFSET;
y = midPoint[1];
}
break;
}
case "top": {
if (corners.length >= 4) {
const topCorner = corners[3];
const midPoint = topCorner[1];
x = midPoint[0];
y = midPoint[1] - OFFSET;
} else {
const midPoint = getMidPoint(topLeft[1], topRight[0]);
x = midPoint[0];
y = midPoint[1] - OFFSET;
}
break;
}
case "bottom": {
if (corners.length >= 2) {
const bottomCorner = corners[1];
const midPoint = bottomCorner[1];
x = midPoint[0];
y = midPoint[1] + OFFSET;
} else {
const midPoint = getMidPoint(bottomRight[1], bottomLeft[0]);
x = midPoint[0];
y = midPoint[1] + OFFSET;
}
break;
}
case "top-right": {
const midPoint = getMidPoint(topRight[0], topRight[1]);
x = midPoint[0] + OFFSET * 0.707;
y = midPoint[1] - OFFSET * 0.707;
break;
}
case "bottom-right": {
const midPoint = getMidPoint(bottomRight[0], bottomRight[1]);
x = midPoint[0] + OFFSET * 0.707;
y = midPoint[1] + OFFSET * 0.707;
break;
}
case "bottom-left": {
const midPoint = getMidPoint(bottomLeft[0], bottomLeft[1]);
x = midPoint[0] - OFFSET * 0.707;
y = midPoint[1] + OFFSET * 0.707;
break;
}
case "top-left": {
const midPoint = getMidPoint(topLeft[0], topLeft[1]);
x = midPoint[0] - OFFSET * 0.707;
y = midPoint[1] - OFFSET * 0.707;
break;
}
default: {
return null;
}
}
return pointRotateRads(pointFrom(x, y), center, bindableElement.angle);
}
if (bindableElement.type === "ellipse") {
const ellipseCenterX = bindableElement.x + bindableElement.width / 2;
const ellipseCenterY = bindableElement.y + bindableElement.height / 2;
const radiusX = bindableElement.width / 2;
const radiusY = bindableElement.height / 2;
let x: number;
let y: number;
switch (side) {
case "top": {
x = ellipseCenterX;
y = ellipseCenterY - radiusY - OFFSET;
break;
}
case "right": {
x = ellipseCenterX + radiusX + OFFSET;
y = ellipseCenterY;
break;
}
case "bottom": {
x = ellipseCenterX;
y = ellipseCenterY + radiusY + OFFSET;
break;
}
case "left": {
x = ellipseCenterX - radiusX - OFFSET;
y = ellipseCenterY;
break;
}
case "top-right": {
const angle = -Math.PI / 4;
const ellipseX = radiusX * Math.cos(angle);
const ellipseY = radiusY * Math.sin(angle);
x = ellipseCenterX + ellipseX + OFFSET * 0.707;
y = ellipseCenterY + ellipseY - OFFSET * 0.707;
break;
}
case "bottom-right": {
const angle = Math.PI / 4;
const ellipseX = radiusX * Math.cos(angle);
const ellipseY = radiusY * Math.sin(angle);
x = ellipseCenterX + ellipseX + OFFSET * 0.707;
y = ellipseCenterY + ellipseY + OFFSET * 0.707;
break;
}
case "bottom-left": {
const angle = (3 * Math.PI) / 4;
const ellipseX = radiusX * Math.cos(angle);
const ellipseY = radiusY * Math.sin(angle);
x = ellipseCenterX + ellipseX - OFFSET * 0.707;
y = ellipseCenterY + ellipseY + OFFSET * 0.707;
break;
}
case "top-left": {
const angle = (-3 * Math.PI) / 4;
const ellipseX = radiusX * Math.cos(angle);
const ellipseY = radiusY * Math.sin(angle);
x = ellipseCenterX + ellipseX - OFFSET * 0.707;
y = ellipseCenterY + ellipseY - OFFSET * 0.707;
break;
}
default: {
return null;
}
}
return pointRotateRads(pointFrom(x, y), center, bindableElement.angle);
}
if (isRectangularElement(bindableElement)) {
const [sides, corners] = deconstructRectanguloidElement(
bindableElement as ExcalidrawRectanguloidElement,
);
const [top, right, bottom, left] = sides;
let x: number;
let y: number;
switch (side) {
case "top": {
const midPoint = getMidPoint(top[0], top[1]);
x = midPoint[0];
y = midPoint[1] - OFFSET;
break;
}
case "right": {
const midPoint = getMidPoint(right[0], right[1]);
x = midPoint[0] + OFFSET;
y = midPoint[1];
break;
}
case "bottom": {
const midPoint = getMidPoint(bottom[0], bottom[1]);
x = midPoint[0];
y = midPoint[1] + OFFSET;
break;
}
case "left": {
const midPoint = getMidPoint(left[0], left[1]);
x = midPoint[0] - OFFSET;
y = midPoint[1];
break;
}
case "top-left": {
if (corners.length >= 1) {
const corner = corners[0];
const p1 = corner[0];
const p2 = corner[3];
const midPoint = getMidPoint(p1, p2);
x = midPoint[0] - OFFSET * 0.707;
y = midPoint[1] - OFFSET * 0.707;
} else {
x = bindableElement.x - OFFSET;
y = bindableElement.y - OFFSET;
}
break;
}
case "top-right": {
if (corners.length >= 2) {
const corner = corners[1];
const p1 = corner[0];
const p2 = corner[3];
const midPoint = getMidPoint(p1, p2);
x = midPoint[0] + OFFSET * 0.707;
y = midPoint[1] - OFFSET * 0.707;
} else {
x = bindableElement.x + bindableElement.width + OFFSET;
y = bindableElement.y - OFFSET;
}
break;
}
case "bottom-right": {
if (corners.length >= 3) {
const corner = corners[2];
const p1 = corner[0];
const p2 = corner[3];
const midPoint = getMidPoint(p1, p2);
x = midPoint[0] + OFFSET * 0.707;
y = midPoint[1] + OFFSET * 0.707;
} else {
x = bindableElement.x + bindableElement.width + OFFSET;
y = bindableElement.y + bindableElement.height + OFFSET;
}
break;
}
case "bottom-left": {
if (corners.length >= 4) {
const corner = corners[3];
const p1 = corner[0];
const p2 = corner[3];
const midPoint = getMidPoint(p1, p2);
x = midPoint[0] - OFFSET * 0.707;
y = midPoint[1] + OFFSET * 0.707;
} else {
x = bindableElement.x - OFFSET;
y = bindableElement.y + bindableElement.height + OFFSET;
}
break;
}
default: {
return null;
}
}
return pointRotateRads(pointFrom(x, y), center, bindableElement.angle);
}
return null;
};
const getMidPoint = (p1: GlobalPoint, p2: GlobalPoint): GlobalPoint => {
return pointFrom((p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2);
};
+1 -10
View File
@@ -2,6 +2,7 @@ import rough from "roughjs/bin/rough";
import {
arrayToMap,
type Bounds,
invariant,
rescalePoints,
sizeOf,
@@ -78,16 +79,6 @@ export type RectangleBox = {
type MaybeQuadraticSolution = [number | null, number | null] | false;
/**
* x and y position of top left corner, x and y position of bottom right corner
*/
export type Bounds = readonly [
minX: number,
minY: number,
maxX: number,
maxY: number,
];
export type SceneBounds = readonly [
sceneX: number,
sceneY: number,
+1 -2
View File
@@ -1,4 +1,4 @@
import { invariant, isTransparent } from "@excalidraw/common";
import { invariant, isTransparent, type Bounds } from "@excalidraw/common";
import {
curveIntersectLineSegment,
isPointWithinBounds,
@@ -29,7 +29,6 @@ import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
import { isPathALoop } from "./utils";
import {
type Bounds,
doBoundsIntersect,
elementCenterPoint,
getCenterForBounds,
+2 -1
View File
@@ -16,7 +16,8 @@ export const hasStrokeColor = (type: ElementOrToolType) =>
type === "freedraw" ||
type === "arrow" ||
type === "line" ||
type === "text";
type === "text" ||
type === "embeddable";
export const hasStrokeWidth = (type: ElementOrToolType) =>
type === "rectangle" ||
+1 -1
View File
@@ -1,4 +1,5 @@
import {
type Bounds,
TEXT_AUTOWRAP_THRESHOLD,
getGridPoint,
getFontString,
@@ -29,7 +30,6 @@ import {
import type { Scene } from "./Scene";
import type { Bounds } from "./bounds";
import type { ExcalidrawElement } from "./types";
export const dragSelectedElements = (
+1 -1
View File
@@ -14,6 +14,7 @@ import {
} from "@excalidraw/math";
import {
type Bounds,
BinaryHeap,
invariant,
isAnyTrue,
@@ -54,7 +55,6 @@ import {
import { aabbForElement, pointInsideBounds } from "./bounds";
import { getHoveredElementForBinding } from "./collision";
import type { Bounds } from "./bounds";
import type { Heading } from "./heading";
import type {
Arrowhead,
+7 -2
View File
@@ -1,4 +1,9 @@
import { invariant, isDevEnv, isTestEnv } from "@excalidraw/common";
import {
invariant,
isDevEnv,
isTestEnv,
type Bounds,
} from "@excalidraw/common";
import {
pointFrom,
@@ -19,7 +24,7 @@ import type {
Vector,
} from "@excalidraw/math";
import { getCenterForBounds, type Bounds } from "./bounds";
import { getCenterForBounds } from "./bounds";
import type { ExcalidrawBindableElement } from "./types";
+1
View File
@@ -92,6 +92,7 @@ export * from "./store";
export * from "./textElement";
export * from "./textMeasurements";
export * from "./textWrapping";
export * from "./transform";
export * from "./transformHandles";
export * from "./typeChecks";
export * from "./utils";
+23 -33
View File
@@ -26,7 +26,6 @@ import {
import {
deconstructLinearOrFreeDrawElement,
getHoveredElementForBinding,
isPathALoop,
moveArrowAboveBindable,
projectFixedPointOntoDiagonal,
@@ -43,6 +42,7 @@ import type {
NullableGridSize,
Zoom,
} from "@excalidraw/excalidraw/types";
import type { Bounds } from "@excalidraw/common";
import {
calculateFixedPointForNonElbowArrowBinding,
@@ -69,7 +69,6 @@ import { isLineElement } from "./typeChecks";
import type { Scene } from "./Scene";
import type { Bounds } from "./bounds";
import type {
NonDeleted,
ExcalidrawLinearElement,
@@ -306,21 +305,11 @@ export class LinearElementEditor {
const customLineAngle =
linearElementEditor.customLineAngle ??
determineCustomLinearAngle(pivotPoint, element.points[idx]);
const hoveredElement = getHoveredElementForBinding(
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
elements,
elementsMap,
);
// Determine if point movement should happen and how much
let deltaX = 0;
let deltaY = 0;
if (
shouldRotateWithDiscreteAngle(event) &&
!hoveredElement &&
!element.startBinding &&
!element.endBinding
) {
if (shouldRotateWithDiscreteAngle(event)) {
const [width, height] = LinearElementEditor._getShiftLockedDelta(
element,
elementsMap,
@@ -354,11 +343,13 @@ export class LinearElementEditor {
[idx],
deltaX,
deltaY,
scenePointerX,
scenePointerY,
elementsMap,
element,
elements,
app,
event.shiftKey,
shouldRotateWithDiscreteAngle(event),
event.altKey,
);
@@ -492,22 +483,11 @@ export class LinearElementEditor {
const endIsSelected = selectedPointsIndices.includes(
element.points.length - 1,
);
const hoveredElement = getHoveredElementForBinding(
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
elements,
elementsMap,
);
// Determine if point movement should happen and how much
let deltaX = 0;
let deltaY = 0;
if (
shouldRotateWithDiscreteAngle(event) &&
singlePointDragged &&
!hoveredElement &&
!element.startBinding &&
!element.endBinding
) {
if (shouldRotateWithDiscreteAngle(event) && singlePointDragged) {
const [width, height] = LinearElementEditor._getShiftLockedDelta(
element,
elementsMap,
@@ -520,7 +500,6 @@ export class LinearElementEditor {
width + pivotPoint[0],
height + pivotPoint[1],
);
deltaX = target[0] - draggingPoint[0];
deltaY = target[1] - draggingPoint[1];
} else {
@@ -541,11 +520,13 @@ export class LinearElementEditor {
selectedPointsIndices,
deltaX,
deltaY,
scenePointerX,
scenePointerY,
elementsMap,
element,
elements,
app,
event.shiftKey,
shouldRotateWithDiscreteAngle(event) && singlePointDragged,
event.altKey,
);
@@ -2088,11 +2069,13 @@ const pointDraggingUpdates = (
selectedPointsIndices: readonly number[],
deltaX: number,
deltaY: number,
scenePointerX: number,
scenePointerY: number,
elementsMap: NonDeletedSceneElementsMap,
element: NonDeleted<ExcalidrawLinearElement>,
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
app: AppClassProperties,
shiftKey: boolean,
angleLocked: boolean,
altKey: boolean,
): {
positions: PointsPositionUpdates;
@@ -2128,12 +2111,14 @@ const pointDraggingUpdates = (
const { start, end } = getBindingStrategyForDraggingBindingElementEndpoints(
element,
naiveDraggingPoints,
scenePointerX,
scenePointerY,
elementsMap,
elements,
app.state,
{
newArrow: !!app.state.newElement,
shiftKey,
angleLocked,
altKey,
},
);
@@ -2250,10 +2235,15 @@ const pointDraggingUpdates = (
// We need to use a custom intersector to ensure that if there is a big "jump"
// in the arrow's position, we can position it with outline avoidance
// pixel-perfectly and avoid "dancing" arrows.
const customIntersector =
// NOTE: Direction matters here, so we create two intersectors
const startCustomIntersector =
start.focusPoint && end.focusPoint
? lineSegment(start.focusPoint, end.focusPoint)
: undefined;
const endCustomIntersector =
start.focusPoint && end.focusPoint
? lineSegment(end.focusPoint, start.focusPoint)
: undefined;
// Needed to handle a special case where an existing arrow is dragged over
// the same element it is bound to on the other side
@@ -2290,7 +2280,7 @@ const pointDraggingUpdates = (
nextArrow.endBinding,
endBindable,
elementsMap,
customIntersector,
endCustomIntersector,
) || nextArrow.points[nextArrow.points.length - 1]
: nextArrow.points[nextArrow.points.length - 1];
@@ -2321,7 +2311,7 @@ const pointDraggingUpdates = (
nextArrow.startBinding,
startBindable,
elementsMap,
customIntersector,
startCustomIntersector,
) || nextArrow.points[0]
: nextArrow.points[0];
+22 -19
View File
@@ -21,7 +21,7 @@ import {
getResizedElementAbsoluteCoords,
} from "./bounds";
import { newElementWith } from "./mutateElement";
import { getBoundTextMaxWidth } from "./textElement";
import { getBoundTextMaxWidth, getInitialTextMetrics } from "./textElement";
import { normalizeText, measureText } from "./textMeasurements";
import { wrapText } from "./textWrapping";
@@ -236,27 +236,30 @@ const getTextElementPositionOffsets = (
};
};
export type NewTextElementOptions = {
text: string;
originalText?: string;
fontSize?: number;
fontFamily?: FontFamilyValues;
textAlign?: TextAlign;
verticalAlign?: VerticalAlign;
containerId?: ExcalidrawTextContainer["id"] | null;
lineHeight?: ExcalidrawTextElement["lineHeight"];
autoResize?: ExcalidrawTextElement["autoResize"];
} & ElementConstructorOpts;
export const newTextElement = (
opts: {
text: string;
originalText?: string;
fontSize?: number;
fontFamily?: FontFamilyValues;
textAlign?: TextAlign;
verticalAlign?: VerticalAlign;
containerId?: ExcalidrawTextContainer["id"] | null;
lineHeight?: ExcalidrawTextElement["lineHeight"];
autoResize?: ExcalidrawTextElement["autoResize"];
} & ElementConstructorOpts,
opts: NewTextElementOptions,
): NonDeleted<ExcalidrawTextElement> => {
const fontFamily = opts.fontFamily || DEFAULT_FONT_FAMILY;
const fontSize = opts.fontSize || DEFAULT_FONT_SIZE;
const lineHeight = opts.lineHeight || getLineHeight(fontFamily);
const text = normalizeText(opts.text);
const metrics = measureText(
text,
getFontString({ fontFamily, fontSize }),
lineHeight,
const normalizedText = normalizeText(opts.text);
const originalText = opts.originalText ?? normalizedText;
const metrics = getInitialTextMetrics(
{ ...opts, text: normalizedText },
fontFamily,
fontSize,
);
const textAlign = opts.textAlign || DEFAULT_TEXT_ALIGN;
const verticalAlign = opts.verticalAlign || DEFAULT_VERTICAL_ALIGN;
@@ -267,7 +270,7 @@ export const newTextElement = (
const textElementProps: ExcalidrawTextElement = {
..._newElementBase<ExcalidrawTextElement>("text", opts),
text,
text: normalizedText,
fontSize,
fontFamily,
textAlign,
@@ -277,7 +280,7 @@ export const newTextElement = (
width: metrics.width,
height: metrics.height,
containerId: opts.containerId || null,
originalText: opts.originalText ?? text,
originalText,
autoResize: opts.autoResize ?? true,
lineHeight,
};
+1 -1
View File
@@ -13,6 +13,7 @@ import {
import type { GlobalPoint, LineSegment, LocalPoint } from "@excalidraw/math";
import type { AppState, Zoom } from "@excalidraw/excalidraw/types";
import type { Bounds } from "@excalidraw/common";
import { getElementAbsoluteCoords } from "./bounds";
import {
@@ -23,7 +24,6 @@ import {
} from "./transformHandles";
import { isImageElement, isLinearElement } from "./typeChecks";
import type { Bounds } from "./bounds";
import type {
TransformHandleType,
TransformHandle,
+26
View File
@@ -8,6 +8,8 @@ import {
getFontString,
isProdEnv,
invariant,
DEFAULT_FONT_FAMILY,
getLineHeight,
} from "@excalidraw/common";
import { pointFrom, pointRotateRads, type Radians } from "@excalidraw/math";
@@ -30,6 +32,8 @@ import {
isTextElement,
} from "./typeChecks";
import type { NewTextElementOptions } from "./newElement";
import type { Scene } from "./Scene";
import type { MaybeTransformHandleType } from "./transformHandles";
@@ -40,6 +44,7 @@ import type {
ExcalidrawTextContainer,
ExcalidrawTextElement,
ExcalidrawTextElementWithContainer,
FontFamilyValues,
NonDeletedExcalidrawElement,
} from "./types";
@@ -528,3 +533,24 @@ export const getTextFromElements = (
.join(separator);
return text;
};
/** When text is already measured and wrapped, we want to respect those dimensions */
export const getInitialTextMetrics = (
text: NewTextElementOptions,
fontFamily: FontFamilyValues = DEFAULT_FONT_FAMILY,
fontSize: number = DEFAULT_FONT_SIZE,
) => {
const shouldUseProvidedDimensions =
text.autoResize === false && text.width && text.height;
return shouldUseProvidedDimensions
? {
width: text.width,
height: text.height,
}
: measureText(
text.text,
getFontString({ fontFamily, fontSize }),
text.lineHeight ?? getLineHeight(fontFamily),
);
};
@@ -10,13 +10,13 @@ import {
arrayToMap,
assertNever,
cloneJSON,
getFontString,
isDevEnv,
toBrandedType,
getLineHeight,
} from "@excalidraw/common";
import { bindBindingElement } from "@excalidraw/element";
import type { MarkOptional } from "@excalidraw/common/utility-types";
import { bindBindingElement } from "./binding";
import {
newArrowElement,
newElement,
@@ -25,21 +25,19 @@ import {
newLinearElement,
newMagicFrameElement,
newTextElement,
} from "@excalidraw/element";
import { measureText, normalizeText } from "@excalidraw/element";
import { isArrowElement } from "@excalidraw/element";
type ElementConstructorOpts,
} from "./newElement";
import { isArrowElement } from "./typeChecks";
import { syncInvalidIndices } from "@excalidraw/element";
import { syncInvalidIndices } from "./fractionalIndex";
import { redrawTextBoundingBox } from "@excalidraw/element";
import { getInitialTextMetrics, redrawTextBoundingBox } from "./textElement";
import { LinearElementEditor } from "@excalidraw/element";
import { LinearElementEditor } from "./linearElementEditor";
import { getCommonBounds } from "@excalidraw/element";
import { getCommonBounds } from "./bounds";
import { Scene } from "@excalidraw/element";
import type { ElementConstructorOpts } from "@excalidraw/element";
import { Scene } from "./Scene";
import type {
ExcalidrawArrowElement,
@@ -59,9 +57,7 @@ import type {
NonDeletedSceneElementsMap,
TextAlign,
VerticalAlign,
} from "@excalidraw/element/types";
import type { MarkOptional } from "@excalidraw/common/utility-types";
} from "./types";
export type ValidLinearElement = {
type: "arrow" | "line";
@@ -580,14 +576,7 @@ export const convertToExcalidrawElements = (
case "text": {
const fontFamily = element?.fontFamily || DEFAULT_FONT_FAMILY;
const fontSize = element?.fontSize || DEFAULT_FONT_SIZE;
const lineHeight = element?.lineHeight || getLineHeight(fontFamily);
const text = element.text ?? "";
const normalizedText = normalizeText(text);
const metrics = measureText(
normalizedText,
getFontString({ fontFamily, fontSize }),
lineHeight,
);
const metrics = getInitialTextMetrics(element, fontFamily, fontSize);
excalidrawElement = newTextElement({
width: metrics.width,
+1 -1
View File
@@ -11,6 +11,7 @@ import type {
InteractiveCanvasAppState,
Zoom,
} from "@excalidraw/excalidraw/types";
import type { Bounds } from "@excalidraw/common";
import { getElementAbsoluteCoords } from "./bounds";
import {
@@ -20,7 +21,6 @@ import {
isLinearElement,
} from "./typeChecks";
import type { Bounds } from "./bounds";
import type {
ElementsMap,
ExcalidrawElement,
-10
View File
@@ -6,7 +6,6 @@ import type { ElementOrToolType } from "@excalidraw/excalidraw/types";
import type { MarkNonNullable } from "@excalidraw/common/utility-types";
import type { Bounds } from "./bounds";
import type {
ExcalidrawElement,
ExcalidrawTextElement,
@@ -356,15 +355,6 @@ export const getDefaultRoundnessTypeForElement = (
return null;
};
// TODO: Move this to @excalidraw/math
export const isBounds = (box: unknown): box is Bounds =>
Array.isArray(box) &&
box.length === 4 &&
typeof box[0] === "number" &&
typeof box[1] === "number" &&
typeof box[2] === "number" &&
typeof box[3] === "number";
export const getLinearElementSubType = (
element: ExcalidrawLinearElement,
): ExcalidrawLinearElementSubType => {
+1 -1
View File
@@ -2,6 +2,7 @@ import { pointFrom } from "@excalidraw/math";
import { Excalidraw } from "@excalidraw/excalidraw";
import {
type Bounds,
KEYS,
getSizeFromPoints,
reseed,
@@ -22,7 +23,6 @@ import { resizeSingleElement } from "../src/resizeElements";
import { LinearElementEditor } from "../src/linearElementEditor";
import { getElementPointsCoords } from "../src/bounds";
import type { Bounds } from "../src/bounds";
import type {
ExcalidrawElbowArrowElement,
ExcalidrawFreeDrawElement,
+14 -4
View File
@@ -18,6 +18,7 @@ import {
KEYS,
arrayToMap,
invariant,
shouldRotateWithDiscreteAngle,
updateActiveTool,
} from "@excalidraw/common";
import { isPathALoop } from "@excalidraw/element";
@@ -102,10 +103,19 @@ export const actionFinalize = register<FormData>({
return map;
}, new Map()) ?? new Map();
bindOrUnbindBindingElement(element, draggedPoints, scene, appState, {
newArrow,
altKey: event.altKey,
});
bindOrUnbindBindingElement(
element,
draggedPoints,
sceneCoords.x,
sceneCoords.y,
scene,
appState,
{
newArrow,
altKey: event.altKey,
angleLocked: shouldRotateWithDiscreteAngle(event),
},
);
} else if (isLineElement(element)) {
if (
appState.selectedLinearElement?.isEditing &&
@@ -22,6 +22,7 @@ import {
isTransparent,
reduceToCommonValue,
invariant,
FONT_SIZES,
} from "@excalidraw/common";
import { canBecomePolygon, getNonDeletedElements } from "@excalidraw/element";
@@ -758,25 +759,25 @@ export const actionChangeFontSize = register<ExcalidrawTextElement["fontSize"]>(
group="font-size"
options={[
{
value: 16,
value: FONT_SIZES.sm,
text: t("labels.small"),
icon: FontSizeSmallIcon,
testId: "fontSize-small",
},
{
value: 20,
value: FONT_SIZES.md,
text: t("labels.medium"),
icon: FontSizeMediumIcon,
testId: "fontSize-medium",
},
{
value: 28,
value: FONT_SIZES.lg,
text: t("labels.large"),
icon: FontSizeLargeIcon,
testId: "fontSize-large",
},
{
value: 36,
value: FONT_SIZES.xl,
text: t("labels.veryLarge"),
icon: FontSizeExtraLargeIcon,
testId: "fontSize-veryLarge",
+2 -1
View File
@@ -9,6 +9,7 @@ import {
VERTICAL_ALIGN,
randomId,
isDevEnv,
FONT_SIZES,
} from "@excalidraw/common";
import {
@@ -213,7 +214,7 @@ const chartXLabels = (
y: y + BAR_GAP / 2,
width: BAR_WIDTH,
angle: 5.87 as Radians,
fontSize: 16,
fontSize: FONT_SIZES.sm,
textAlign: "center",
verticalAlign: "top",
});
-1
View File
@@ -113,7 +113,6 @@ export const createPasteEvent = ({
if (typeof value !== "string") {
files = files || [];
files.push(value);
event.clipboardData?.items.add(value);
continue;
}
try {
+11 -3
View File
@@ -248,6 +248,8 @@ import {
doBoundsIntersect,
isPointInElement,
maxBindingDistance_simple,
convertToExcalidrawElements,
type ExcalidrawElementSkeleton,
} from "@excalidraw/element";
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
@@ -395,7 +397,6 @@ import {
SnapCache,
isGridModeEnabled,
} from "../snapping";
import { convertToExcalidrawElements } from "../data/transform";
import { Renderer } from "../scene/Renderer";
import {
setEraserCursor,
@@ -457,7 +458,7 @@ import type { ClipboardData, PastedMixedContent } from "../clipboard";
import type { ExportedElements } from "../data";
import type { ContextMenuItems } from "./ContextMenu";
import type { FileSystemHandle } from "../data/filesystem";
import type { ExcalidrawElementSkeleton } from "../data/transform";
import type {
AppClassProperties,
AppProps,
@@ -8617,9 +8618,16 @@ class App extends React.Component<AppProps, AppState> {
},
],
]),
point[0],
point[1],
this.scene,
this.state,
{ newArrow: true, altKey: event.altKey, initialBinding: true },
{
newArrow: true,
altKey: event.altKey,
initialBinding: true,
angleLocked: shouldRotateWithDiscreteAngle(event.nativeEvent),
},
);
}
@@ -6,7 +6,7 @@ import { hitElementBoundingBox } from "@excalidraw/element";
import type { GlobalPoint, Radians } from "@excalidraw/math";
import type { Bounds } from "@excalidraw/element";
import type { Bounds } from "@excalidraw/common";
import type {
ElementsMap,
NonDeletedExcalidrawElement,
+35 -18
View File
@@ -56,6 +56,7 @@ import type { LocalPoint, Radians } from "@excalidraw/math";
import type {
ElementsMap,
ElementsMapOrArray,
ExcalidrawArrowElement,
ExcalidrawBindableElement,
ExcalidrawElbowArrowElement,
@@ -129,7 +130,8 @@ const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => {
const repairBinding = <T extends ExcalidrawArrowElement>(
element: T,
binding: FixedPointBinding | null,
elementsMap: Readonly<ElementsMap>,
targetElementsMap: Readonly<ElementsMap>,
localElementsMap: Readonly<ElementsMap> | null | undefined,
startOrEnd: "start" | "end",
): FixedPointBinding | null => {
if (!binding) {
@@ -148,18 +150,27 @@ const repairBinding = <T extends ExcalidrawArrowElement>(
return fixedPointBinding;
}
const boundElement =
(elementsMap.get(binding.elementId) as ExcalidrawBindableElement) ||
undefined;
if (boundElement) {
if (binding.mode) {
return {
elementId: binding.elementId,
mode: binding.mode || "orbit",
fixedPoint: normalizeFixedPoint(binding.fixedPoint || [0.5, 0.5]),
} as FixedPointBinding | null;
}
// Fallback if the bound element is missing but the binding is at least
// looking like a valid one shape-wise
if (binding.mode && binding.fixedPoint && binding.elementId) {
return {
elementId: binding.elementId,
mode: binding.mode,
fixedPoint: normalizeFixedPoint(binding.fixedPoint || [0.5, 0.5]),
} as FixedPointBinding | null;
}
const targetBoundElement =
(targetElementsMap.get(binding.elementId) as ExcalidrawBindableElement) ||
undefined;
const boundElement =
targetBoundElement ||
(localElementsMap?.get(binding.elementId) as ExcalidrawBindableElement) ||
undefined;
const elementsMap = targetBoundElement ? targetElementsMap : localElementsMap;
// migrating legacy focus point bindings
if (boundElement && elementsMap) {
const p = LinearElementEditor.getPointAtIndexGlobalCoordinates(
element,
startOrEnd === "start" ? 0 : element.points.length - 1,
@@ -193,6 +204,8 @@ const repairBinding = <T extends ExcalidrawArrowElement>(
};
}
console.error(`could not repair binding for element`);
return null;
};
@@ -284,7 +297,8 @@ const restoreElementWithProperties = <
export const restoreElement = (
element: Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
elementsMap: Readonly<ElementsMap>,
targetElementsMap: Readonly<ElementsMap>,
localElementsMap: Readonly<ElementsMap> | null | undefined,
opts?: {
deleteInvisibleElements?: boolean;
},
@@ -405,13 +419,15 @@ export const restoreElement = (
startBinding: repairBinding(
element as ExcalidrawArrowElement,
element.startBinding,
elementsMap,
targetElementsMap,
localElementsMap,
"start",
),
endBinding: repairBinding(
element as ExcalidrawArrowElement,
element.endBinding,
elementsMap,
targetElementsMap,
localElementsMap,
"end",
),
startArrowhead,
@@ -575,7 +591,7 @@ const repairFrameMembership = (
export const restoreElements = (
targetElements: ImportedDataState["elements"],
/** NOTE doesn't serve for reconciliation */
localElements: readonly ExcalidrawElement[] | null | undefined,
localElements: Readonly<ElementsMapOrArray> | null | undefined,
opts?:
| {
refreshDimensions?: boolean;
@@ -586,7 +602,7 @@ export const restoreElements = (
): OrderedExcalidrawElement[] => {
// used to detect duplicate top-level element ids
const existingIds = new Set<string>();
const elementsMap = arrayToMap(targetElements || []);
const targetElementsMap = arrayToMap(targetElements || []);
const localElementsMap = localElements ? arrayToMap(localElements) : null;
const restoredElements = syncInvalidIndices(
(targetElements || []).reduce((elements, element) => {
@@ -598,7 +614,8 @@ export const restoreElements = (
let migratedElement: ExcalidrawElement | null = restoreElement(
element,
elementsMap,
targetElementsMap,
localElementsMap,
{
deleteInvisibleElements: opts?.deleteInvisibleElements,
},
+1 -1
View File
@@ -28,7 +28,7 @@ import { shouldTestInside } from "@excalidraw/element";
import { hasBoundTextElement, isBoundToContainer } from "@excalidraw/element";
import { getBoundTextElementId } from "@excalidraw/element";
import type { Bounds } from "@excalidraw/element";
import type { Bounds } from "@excalidraw/common";
import type { GlobalPoint, LineSegment } from "@excalidraw/math/types";
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
+2 -1
View File
@@ -4,6 +4,7 @@ import {
CJK_HAND_DRAWN_FALLBACK_FONT,
WINDOWS_EMOJI_FALLBACK_FONT,
getFontFamilyFallbacks,
FONT_SIZES,
} from "@excalidraw/common";
import { getContainerElement } from "@excalidraw/element";
import { charWidth } from "@excalidraw/element";
@@ -240,7 +241,7 @@ export class Fonts {
for (const [index, fontFamily] of fontFamilies.entries()) {
const font = getFontString({
fontFamily,
fontSize: 16,
fontSize: FONT_SIZES.sm,
});
// WARN: without "text" param it does not have to mean that all font faces are loaded as it could be just one irrelevant font face!
+5 -2
View File
@@ -293,8 +293,11 @@ export { TTDDialog } from "./components/TTDDialog/TTDDialog";
export { TTDDialogTrigger } from "./components/TTDDialog/TTDDialogTrigger";
export { zoomToFitBounds } from "./actions/actionCanvas";
export { convertToExcalidrawElements } from "./data/transform";
export { getCommonBounds, getVisibleSceneBounds } from "@excalidraw/element";
export {
getCommonBounds,
getVisibleSceneBounds,
convertToExcalidrawElements,
} from "@excalidraw/element";
export {
elementsOverlappingBBox,
+2 -1
View File
@@ -6,8 +6,9 @@ import {
polygonIncludesPointNonZero,
} from "@excalidraw/math";
import { type Bounds } from "@excalidraw/common";
import {
type Bounds,
computeBoundTextPosition,
doBoundsIntersect,
getBoundTextElement,
-1
View File
@@ -95,7 +95,6 @@
"fractional-indexing": "3.2.0",
"fuzzy": "0.1.3",
"image-blob-reduce": "3.0.1",
"is-mobile": "5.0.0",
"jotai": "2.11.0",
"jotai-scope": "0.7.2",
"lodash.debounce": "4.0.8",
+1 -1
View File
@@ -39,7 +39,7 @@ import { type Mutable } from "@excalidraw/common/utility-types";
import { newTextElement } from "@excalidraw/element";
import type { Bounds } from "@excalidraw/element";
import type { Bounds } from "@excalidraw/common";
import type {
ExcalidrawElement,
+1 -1
View File
@@ -24,7 +24,7 @@ import {
import type { InclusiveRange } from "@excalidraw/math";
import type { Bounds } from "@excalidraw/element";
import type { Bounds } from "@excalidraw/common";
import type { MaybeTransformHandleType } from "@excalidraw/element";
import type {
ElementsMap,
+3 -1
View File
@@ -23,6 +23,8 @@ import { isLinearElementType } from "@excalidraw/element";
import { getSelectedElements } from "@excalidraw/element";
import { selectGroupsForSelectedElements } from "@excalidraw/element";
import { FONT_SIZES } from "@excalidraw/common";
import type {
ExcalidrawElement,
ExcalidrawGenericElement,
@@ -406,7 +408,7 @@ export class API {
text: opts?.label?.text || "sample-text",
width: 50,
height: 20,
fontSize: 16,
fontSize: FONT_SIZES.sm,
containerId: rectangle.id,
frameId:
opts?.label?.frameId === undefined
+1 -1
View File
@@ -5,7 +5,7 @@ import {
type LocalPoint,
} from "@excalidraw/math";
import type { Bounds } from "@excalidraw/element";
import type { Bounds } from "@excalidraw/common";
export type LineSegment<P extends LocalPoint | GlobalPoint> = [P, P];
+1 -2
View File
@@ -1,4 +1,4 @@
import { arrayToMap } from "@excalidraw/common";
import { arrayToMap, type Bounds } from "@excalidraw/common";
import { getElementBounds } from "@excalidraw/element";
import {
isArrowElement,
@@ -14,7 +14,6 @@ import {
rangeInclusive,
} from "@excalidraw/math";
import type { Bounds } from "@excalidraw/element";
import type {
ExcalidrawElement,
ExcalidrawFreeDrawElement,
+1 -1
View File
@@ -1,6 +1,6 @@
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
import type { Bounds } from "@excalidraw/element";
import type { Bounds } from "@excalidraw/common";
import {
elementPartiallyOverlapsWithOrContainsBBox,
-5
View File
@@ -6398,11 +6398,6 @@ is-map@^2.0.3:
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e"
integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==
is-mobile@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/is-mobile/-/is-mobile-5.0.0.tgz#1e08a0ef2c38a67bff84a52af68d67bcef445333"
integrity sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==
is-module@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"