Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| be5d0bd925 | |||
| 292aa1cddb | |||
| e95222ed32 | |||
| d87620b239 | |||
| 7cc31ac64a | |||
| 071b17a217 | |||
| 859207b8bc | |||
| becaabfa0f | |||
| f06484c6ab | |||
| bf4c65f483 | |||
| 8d18078f5c | |||
| d080833f4d | |||
| 451bcac0b7 |
@@ -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}
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -441,7 +441,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
||||
};
|
||||
|
||||
private decryptPayload = async (
|
||||
iv: Uint8Array,
|
||||
iv: Uint8Array<ArrayBuffer>,
|
||||
encryptedData: ArrayBuffer,
|
||||
decryptionKey: string,
|
||||
): Promise<ValueOf<SocketUpdateDataSource>> => {
|
||||
@@ -562,7 +562,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
||||
// All socket listeners are moving to Portal
|
||||
this.portal.socket.on(
|
||||
"client-broadcast",
|
||||
async (encryptedData: ArrayBuffer, iv: Uint8Array) => {
|
||||
async (encryptedData: ArrayBuffer, iv: Uint8Array<ArrayBuffer>) => {
|
||||
if (!this.portal.roomKey) {
|
||||
return;
|
||||
}
|
||||
@@ -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[],
|
||||
|
||||
@@ -105,8 +105,8 @@ const decryptElements = async (
|
||||
data: FirebaseStoredScene,
|
||||
roomKey: string,
|
||||
): Promise<readonly ExcalidrawElement[]> => {
|
||||
const ciphertext = data.ciphertext.toUint8Array();
|
||||
const iv = data.iv.toUint8Array();
|
||||
const ciphertext = data.ciphertext.toUint8Array() as Uint8Array<ArrayBuffer>;
|
||||
const iv = data.iv.toUint8Array() as Uint8Array<ArrayBuffer>;
|
||||
|
||||
const decrypted = await decryptData(iv, ciphertext, roomKey);
|
||||
const decodedData = new TextDecoder("utf-8").decode(
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@
|
||||
"prettier": "2.6.2",
|
||||
"rewire": "6.0.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"typescript": "4.9.4",
|
||||
"typescript": "5.9.3",
|
||||
"vite": "5.0.12",
|
||||
"vite-plugin-checker": "0.7.2",
|
||||
"vite-plugin-ejs": "1.7.0",
|
||||
|
||||
@@ -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";
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "./binary-heap";
|
||||
export * from "./bounds";
|
||||
export * from "./colors";
|
||||
export * from "./constants";
|
||||
export * from "./font-metadata";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+5
-4
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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,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 = (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -493,6 +493,7 @@ describe("binding for simple arrows", () => {
|
||||
expect(arrow.endBinding?.elementId).toBe(rectRight.id);
|
||||
|
||||
mouse.downAt(-100, -100);
|
||||
mouse.moveTo(0, 0);
|
||||
mouse.moveTo(650, 750);
|
||||
mouse.up(0, 0);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -113,7 +113,6 @@ export const createPasteEvent = ({
|
||||
if (typeof value !== "string") {
|
||||
files = files || [];
|
||||
files.push(value);
|
||||
event.clipboardData?.items.add(value);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11463,7 +11471,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
): void => {
|
||||
const selectionElement = this.state.selectionElement;
|
||||
const pointerCoords = pointerDownState.lastCoords;
|
||||
if (selectionElement && this.state.activeTool.type !== "eraser") {
|
||||
if (
|
||||
selectionElement &&
|
||||
pointerDownState.boxSelection.hasOccurred &&
|
||||
this.state.activeTool.type !== "eraser"
|
||||
) {
|
||||
dragNewElement({
|
||||
newElement: selectionElement,
|
||||
elementType: this.state.activeTool.type,
|
||||
@@ -11527,25 +11539,27 @@ class App extends React.Component<AppProps, AppState> {
|
||||
snapLines,
|
||||
});
|
||||
|
||||
dragNewElement({
|
||||
newElement,
|
||||
elementType: this.state.activeTool.type,
|
||||
originX: pointerDownState.originInGrid.x,
|
||||
originY: pointerDownState.originInGrid.y,
|
||||
x: gridX,
|
||||
y: gridY,
|
||||
width: distance(pointerDownState.originInGrid.x, gridX),
|
||||
height: distance(pointerDownState.originInGrid.y, gridY),
|
||||
shouldMaintainAspectRatio: isImageElement(newElement)
|
||||
? !shouldMaintainAspectRatio(event)
|
||||
: shouldMaintainAspectRatio(event),
|
||||
shouldResizeFromCenter: shouldResizeFromCenter(event),
|
||||
zoom: this.state.zoom.value,
|
||||
scene: this.scene,
|
||||
widthAspectRatio: aspectRatio,
|
||||
originOffset: this.state.originSnapOffset,
|
||||
informMutation,
|
||||
});
|
||||
if (!isBindingElement(newElement)) {
|
||||
dragNewElement({
|
||||
newElement,
|
||||
elementType: this.state.activeTool.type,
|
||||
originX: pointerDownState.originInGrid.x,
|
||||
originY: pointerDownState.originInGrid.y,
|
||||
x: gridX,
|
||||
y: gridY,
|
||||
width: distance(pointerDownState.originInGrid.x, gridX),
|
||||
height: distance(pointerDownState.originInGrid.y, gridY),
|
||||
shouldMaintainAspectRatio: isImageElement(newElement)
|
||||
? !shouldMaintainAspectRatio(event)
|
||||
: shouldMaintainAspectRatio(event),
|
||||
shouldResizeFromCenter: shouldResizeFromCenter(event),
|
||||
zoom: this.state.zoom.value,
|
||||
scene: this.scene,
|
||||
widthAspectRatio: aspectRatio,
|
||||
originOffset: this.state.originSnapOffset,
|
||||
informMutation,
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
newElement,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -222,7 +222,7 @@ function dataView(
|
||||
*
|
||||
* @param buffers each buffer (chunk) must be at most 2^32 bits large (~4GB)
|
||||
*/
|
||||
const concatBuffers = (...buffers: Uint8Array[]) => {
|
||||
const concatBuffers = (...buffers: Uint8Array[]): Uint8Array<ArrayBuffer> => {
|
||||
const bufferView = new Uint8Array(
|
||||
VERSION_DATAVIEW_BYTES +
|
||||
NEXT_CHUNK_SIZE_DATAVIEW_BYTES * buffers.length +
|
||||
@@ -295,12 +295,12 @@ const splitBuffers = (concatenatedBuffer: Uint8Array) => {
|
||||
|
||||
/** @private */
|
||||
const _encryptAndCompress = async (
|
||||
data: Uint8Array | string,
|
||||
data: Uint8Array<ArrayBuffer> | string,
|
||||
encryptionKey: string,
|
||||
) => {
|
||||
const { encryptedBuffer, iv } = await encryptData(
|
||||
encryptionKey,
|
||||
deflate(data),
|
||||
deflate(data) as Uint8Array<ArrayBuffer>,
|
||||
);
|
||||
|
||||
return { iv, buffer: new Uint8Array(encryptedBuffer) };
|
||||
@@ -330,7 +330,7 @@ export const compressData = async <T extends Record<string, any> = never>(
|
||||
: {
|
||||
metadata: T;
|
||||
}),
|
||||
): Promise<Uint8Array> => {
|
||||
): Promise<Uint8Array<ArrayBuffer>> => {
|
||||
const fileInfo: FileEncodingInfo = {
|
||||
version: 2,
|
||||
compression: "pako@1",
|
||||
@@ -355,8 +355,8 @@ export const compressData = async <T extends Record<string, any> = never>(
|
||||
|
||||
/** @private */
|
||||
const _decryptAndDecompress = async (
|
||||
iv: Uint8Array,
|
||||
decryptedBuffer: Uint8Array,
|
||||
iv: Uint8Array<ArrayBuffer>,
|
||||
decryptedBuffer: Uint8Array<ArrayBuffer>,
|
||||
decryptionKey: string,
|
||||
isCompressed: boolean,
|
||||
) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { blobToArrayBuffer } from "./blob";
|
||||
|
||||
export const IV_LENGTH_BYTES = 12;
|
||||
|
||||
export const createIV = () => {
|
||||
export const createIV = (): Uint8Array<ArrayBuffer> => {
|
||||
const arr = new Uint8Array(IV_LENGTH_BYTES);
|
||||
return window.crypto.getRandomValues(arr);
|
||||
};
|
||||
@@ -49,12 +49,12 @@ export const getCryptoKey = (key: string, usage: KeyUsage) =>
|
||||
|
||||
export const encryptData = async (
|
||||
key: string | CryptoKey,
|
||||
data: Uint8Array | ArrayBuffer | Blob | File | string,
|
||||
): Promise<{ encryptedBuffer: ArrayBuffer; iv: Uint8Array }> => {
|
||||
data: Uint8Array<ArrayBuffer> | ArrayBuffer | Blob | File | string,
|
||||
): Promise<{ encryptedBuffer: ArrayBuffer; iv: Uint8Array<ArrayBuffer> }> => {
|
||||
const importedKey =
|
||||
typeof key === "string" ? await getCryptoKey(key, "encrypt") : key;
|
||||
const iv = createIV();
|
||||
const buffer: ArrayBuffer | Uint8Array =
|
||||
const buffer: ArrayBuffer | Uint8Array<ArrayBuffer> =
|
||||
typeof data === "string"
|
||||
? new TextEncoder().encode(data)
|
||||
: data instanceof Uint8Array
|
||||
@@ -71,15 +71,15 @@ export const encryptData = async (
|
||||
iv,
|
||||
},
|
||||
importedKey,
|
||||
buffer as ArrayBuffer | Uint8Array,
|
||||
buffer,
|
||||
);
|
||||
|
||||
return { encryptedBuffer, iv };
|
||||
};
|
||||
|
||||
export const decryptData = async (
|
||||
iv: Uint8Array,
|
||||
encrypted: Uint8Array | ArrayBuffer,
|
||||
iv: Uint8Array<ArrayBuffer>,
|
||||
encrypted: Uint8Array<ArrayBuffer> | ArrayBuffer,
|
||||
privateKey: string,
|
||||
): Promise<ArrayBuffer> => {
|
||||
const key = await getCryptoKey(privateKey, "decrypt");
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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!
|
||||
|
||||
Vendored
+1
-1
@@ -42,7 +42,7 @@ declare module "png-chunk-text" {
|
||||
function decode(data: Uint8Array): { keyword: string; text: string };
|
||||
}
|
||||
declare module "png-chunks-encode" {
|
||||
function encode(chunks: TEXtChunk[]): Uint8Array;
|
||||
function encode(chunks: TEXtChunk[]): Uint8Array<ArrayBuffer>;
|
||||
export = encode;
|
||||
}
|
||||
declare module "png-chunks-extract" {
|
||||
|
||||
@@ -18,7 +18,7 @@ export function useOutsideClick<T extends HTMLElement>(
|
||||
* Returning `undefined` will fallback to the default behavior.
|
||||
*/
|
||||
isInside?: (
|
||||
event: Event & { target: HTMLElement },
|
||||
event: Event & { target: T },
|
||||
/** the element of the passed ref */
|
||||
container: T,
|
||||
) => boolean | undefined,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -6,8 +6,9 @@ import {
|
||||
polygonIncludesPointNonZero,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import { type Bounds } from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
type Bounds,
|
||||
computeBoundTextPosition,
|
||||
doBoundsIntersect,
|
||||
getBoundTextElement,
|
||||
|
||||
@@ -133,6 +133,6 @@
|
||||
"fonteditor-core": "2.4.1",
|
||||
"harfbuzzjs": "0.3.6",
|
||||
"jest-diff": "29.7.0",
|
||||
"typescript": "4.9.4"
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -20,7 +20,7 @@ const load = (): Promise<{
|
||||
subset: (
|
||||
fontBuffer: ArrayBuffer,
|
||||
codePoints: ReadonlySet<number>,
|
||||
) => Uint8Array;
|
||||
) => Uint8Array<ArrayBuffer>;
|
||||
}> => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
|
||||
@@ -18,7 +18,7 @@ type Vector = any;
|
||||
let loadedWasm: ReturnType<typeof load> | null = null;
|
||||
|
||||
// re-map from internal vector into byte array
|
||||
function convertFromVecToUint8Array(vector: Vector): Uint8Array {
|
||||
function convertFromVecToUint8Array(vector: Vector): Uint8Array<ArrayBuffer> {
|
||||
const arr = [];
|
||||
for (let i = 0, l = vector.size(); i < l; i++) {
|
||||
arr.push(vector.get(i));
|
||||
@@ -29,8 +29,8 @@ function convertFromVecToUint8Array(vector: Vector): Uint8Array {
|
||||
|
||||
// TODO: consider adding support for fetching the wasm from an URL (external CDN, data URL, etc.)
|
||||
const load = (): Promise<{
|
||||
compress: (buffer: ArrayBuffer) => Uint8Array;
|
||||
decompress: (buffer: ArrayBuffer) => Uint8Array;
|
||||
compress: (buffer: ArrayBuffer) => Uint8Array<ArrayBuffer>;
|
||||
decompress: (buffer: ArrayBuffer) => Uint8Array<ArrayBuffer>;
|
||||
}> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
|
||||
@@ -1223,7 +1223,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 2019559783,
|
||||
"versionNonce": 401146281,
|
||||
"width": 10,
|
||||
"x": -20,
|
||||
"y": -10,
|
||||
@@ -1431,14 +1431,14 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": null,
|
||||
"seed": 238820263,
|
||||
"seed": 1014066025,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 1505387817,
|
||||
"versionNonce": 1604849351,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
"y": 30,
|
||||
@@ -1463,14 +1463,14 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": null,
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 915032327,
|
||||
"versionNonce": 493213705,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
@@ -1765,14 +1765,14 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": null,
|
||||
"seed": 238820263,
|
||||
"seed": 1014066025,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 1505387817,
|
||||
"versionNonce": 1604849351,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
"y": 30,
|
||||
@@ -1797,14 +1797,14 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": null,
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 915032327,
|
||||
"versionNonce": 493213705,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
@@ -2108,7 +2108,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 2019559783,
|
||||
"versionNonce": 401146281,
|
||||
"width": 10,
|
||||
"x": -20,
|
||||
"y": -10,
|
||||
@@ -2321,7 +2321,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 1014066025,
|
||||
"versionNonce": 1116226695,
|
||||
"width": 10,
|
||||
"x": -20,
|
||||
"y": -10,
|
||||
@@ -2567,7 +2567,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 2019559783,
|
||||
"versionNonce": 401146281,
|
||||
"width": 10,
|
||||
"x": -20,
|
||||
"y": -10,
|
||||
@@ -2592,14 +2592,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": null,
|
||||
"seed": 238820263,
|
||||
"seed": 1014066025,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 5,
|
||||
"versionNonce": 1604849351,
|
||||
"versionNonce": 400692809,
|
||||
"width": 10,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
@@ -2868,14 +2868,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": null,
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 81784553,
|
||||
"versionNonce": 915032327,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
@@ -2902,14 +2902,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": null,
|
||||
"seed": 238820263,
|
||||
"seed": 1014066025,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 747212839,
|
||||
"versionNonce": 81784553,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
"y": 30,
|
||||
@@ -3238,14 +3238,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
||||
"opacity": 60,
|
||||
"roughness": 2,
|
||||
"roundness": null,
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#e03131",
|
||||
"strokeStyle": "dotted",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 1359939303,
|
||||
"versionNonce": 1402203177,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
@@ -3270,14 +3270,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
||||
"opacity": 60,
|
||||
"roughness": 2,
|
||||
"roundness": null,
|
||||
"seed": 640725609,
|
||||
"seed": 1898319239,
|
||||
"strokeColor": "#e03131",
|
||||
"strokeStyle": "dotted",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 9,
|
||||
"versionNonce": 908564423,
|
||||
"versionNonce": 941653321,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
"y": 30,
|
||||
@@ -3732,14 +3732,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": null,
|
||||
"seed": 400692809,
|
||||
"seed": 238820263,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 81784553,
|
||||
"versionNonce": 915032327,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
"y": 30,
|
||||
@@ -3764,14 +3764,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": null,
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 1150084233,
|
||||
"versionNonce": 2019559783,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
@@ -4058,14 +4058,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": null,
|
||||
"seed": 238820263,
|
||||
"seed": 1014066025,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 915032327,
|
||||
"versionNonce": 493213705,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
"y": 30,
|
||||
@@ -4090,14 +4090,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": null,
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 1150084233,
|
||||
"versionNonce": 2019559783,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
@@ -4387,14 +4387,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": null,
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 5,
|
||||
"versionNonce": 1006504105,
|
||||
"versionNonce": 760410951,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
@@ -4419,14 +4419,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": null,
|
||||
"seed": 400692809,
|
||||
"seed": 238820263,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 5,
|
||||
"versionNonce": 289600103,
|
||||
"versionNonce": 1006504105,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
"y": 30,
|
||||
@@ -5675,14 +5675,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": null,
|
||||
"seed": 453191,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 1014066025,
|
||||
"versionNonce": 1150084233,
|
||||
"width": 10,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
@@ -5707,14 +5707,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": null,
|
||||
"seed": 1505387817,
|
||||
"seed": 400692809,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 915032327,
|
||||
"versionNonce": 23633383,
|
||||
"width": 10,
|
||||
"x": 12,
|
||||
"y": 0,
|
||||
@@ -6899,14 +6899,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": null,
|
||||
"seed": 449462985,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 1723083209,
|
||||
"versionNonce": 747212839,
|
||||
"width": 10,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
@@ -6933,14 +6933,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": null,
|
||||
"seed": 400692809,
|
||||
"seed": 238820263,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 760410951,
|
||||
"versionNonce": 1723083209,
|
||||
"width": 10,
|
||||
"x": 12,
|
||||
"y": 0,
|
||||
@@ -9837,7 +9837,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] el
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 2019559783,
|
||||
"versionNonce": 401146281,
|
||||
"width": 10,
|
||||
"x": -20,
|
||||
"y": -10,
|
||||
|
||||
@@ -13935,7 +13935,7 @@ exports[`regression tests > switches from group of selected elements to another
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": null,
|
||||
"seed": 289600103,
|
||||
"seed": 1006504105,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
|
||||
@@ -41,8 +41,8 @@ exports[`select single element on the scene > arrow 1`] = `
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 2019559783,
|
||||
"version": 5,
|
||||
"versionNonce": 1116226695,
|
||||
"width": 30,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
@@ -88,8 +88,8 @@ exports[`select single element on the scene > arrow escape 1`] = `
|
||||
"strokeWidth": 2,
|
||||
"type": "line",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 2019559783,
|
||||
"version": 5,
|
||||
"versionNonce": 1116226695,
|
||||
"width": 30,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
@@ -120,8 +120,8 @@ exports[`select single element on the scene > diamond 1`] = `
|
||||
"strokeWidth": 2,
|
||||
"type": "diamond",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 401146281,
|
||||
"version": 4,
|
||||
"versionNonce": 2019559783,
|
||||
"width": 30,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
@@ -152,8 +152,8 @@ exports[`select single element on the scene > ellipse 1`] = `
|
||||
"strokeWidth": 2,
|
||||
"type": "ellipse",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 401146281,
|
||||
"version": 4,
|
||||
"versionNonce": 2019559783,
|
||||
"width": 30,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
@@ -184,8 +184,8 @@ exports[`select single element on the scene > rectangle 1`] = `
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 401146281,
|
||||
"version": 4,
|
||||
"versionNonce": 2019559783,
|
||||
"width": 30,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
|
||||
@@ -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
|
||||
@@ -464,7 +466,7 @@ export class API {
|
||||
static readFile = async <T extends "utf8" | null>(
|
||||
filepath: string,
|
||||
encoding?: T,
|
||||
): Promise<T extends "utf8" ? string : Buffer> => {
|
||||
): Promise<T extends "utf8" ? string : ArrayBuffer> => {
|
||||
filepath = path.isAbsolute(filepath)
|
||||
? filepath
|
||||
: path.resolve(path.join(__dirname, "../", filepath));
|
||||
|
||||
@@ -315,6 +315,7 @@ describe("history", () => {
|
||||
]);
|
||||
|
||||
mouse.downAt(0, 0);
|
||||
mouse.moveTo(25, 25);
|
||||
mouse.moveTo(50, 50);
|
||||
mouse.upAt(50, 50);
|
||||
expect(API.getUndoStack().length).toBe(3);
|
||||
|
||||
@@ -467,6 +467,7 @@ describe("regression tests", () => {
|
||||
|
||||
mouse.reset();
|
||||
mouse.down();
|
||||
mouse.move(-1000, -1000);
|
||||
mouse.restorePosition(...end);
|
||||
mouse.up();
|
||||
|
||||
@@ -517,6 +518,7 @@ describe("regression tests", () => {
|
||||
|
||||
mouse.reset();
|
||||
mouse.down();
|
||||
mouse.move(-1000, -1000);
|
||||
mouse.restorePosition(...end);
|
||||
mouse.up();
|
||||
|
||||
@@ -534,6 +536,7 @@ describe("regression tests", () => {
|
||||
|
||||
mouse.moveTo(-10, -10); // the NW resizing handle is at [0, 0], so moving further
|
||||
mouse.down();
|
||||
mouse.move(-1000, -1000);
|
||||
mouse.restorePosition(...end);
|
||||
mouse.up();
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ describe("box-selection", () => {
|
||||
API.setElements([rect1, rect2]);
|
||||
|
||||
mouse.downAt(175, -20);
|
||||
mouse.move(-1000, -1000);
|
||||
mouse.moveTo(85, 70);
|
||||
mouse.up();
|
||||
|
||||
@@ -72,6 +73,7 @@ describe("box-selection", () => {
|
||||
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.downAt(75, -20);
|
||||
mouse.move(-1000, -1000);
|
||||
mouse.moveTo(-15, 70);
|
||||
mouse.up();
|
||||
});
|
||||
@@ -93,6 +95,7 @@ describe("box-selection", () => {
|
||||
API.setElements([rect1]);
|
||||
|
||||
mouse.downAt(75, -20);
|
||||
mouse.move(-1000, -1000);
|
||||
mouse.moveTo(-15, 70);
|
||||
|
||||
assertSelectedElements([rect1.id]);
|
||||
@@ -138,6 +141,7 @@ describe("inner box-selection", () => {
|
||||
API.setElements([rect1, rect2, rect3]);
|
||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||
mouse.downAt(40, 40);
|
||||
mouse.move(-1000, -1000);
|
||||
mouse.moveTo(290, 290);
|
||||
mouse.up();
|
||||
|
||||
@@ -175,6 +179,7 @@ describe("inner box-selection", () => {
|
||||
|
||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||
mouse.downAt(40, 40);
|
||||
mouse.move(-1000, -1000);
|
||||
mouse.moveTo(rect2.x + rect2.width + 10, rect2.y + rect2.height + 10);
|
||||
mouse.up();
|
||||
|
||||
@@ -212,6 +217,7 @@ describe("inner box-selection", () => {
|
||||
API.setElements([rect1, rect2, rect3]);
|
||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||
mouse.downAt(rect2.x - 20, rect2.y - 20);
|
||||
mouse.move(-1000, -1000);
|
||||
mouse.moveTo(rect2.x + rect2.width + 10, rect2.y + rect2.height + 10);
|
||||
assertSelectedElements([rect2.id, rect3.id]);
|
||||
expect(h.state.selectedGroupIds).toEqual({ A: true });
|
||||
@@ -253,9 +259,10 @@ describe("selection element", () => {
|
||||
|
||||
const canvas = container.querySelector("canvas.interactive")!;
|
||||
fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
|
||||
fireEvent.pointerMove(canvas, { clientX: -1000, clientY: -1000 });
|
||||
fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 });
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(4);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(3);
|
||||
const selectionElement = h.state.selectionElement!;
|
||||
expect(selectionElement).not.toBeNull();
|
||||
@@ -275,10 +282,11 @@ describe("selection element", () => {
|
||||
|
||||
const canvas = container.querySelector("canvas.interactive")!;
|
||||
fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
|
||||
fireEvent.pointerMove(canvas, { clientX: -1000, clientY: -1000 });
|
||||
fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 });
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(3);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
});
|
||||
@@ -303,6 +311,7 @@ describe("select single element on the scene", () => {
|
||||
const tool = getByToolName("rectangle");
|
||||
fireEvent.click(tool);
|
||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||
fireEvent.pointerMove(canvas, { clientX: -1000, clientY: -1000 });
|
||||
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
||||
fireEvent.pointerUp(canvas);
|
||||
fireEvent.keyDown(document, {
|
||||
@@ -335,6 +344,7 @@ describe("select single element on the scene", () => {
|
||||
const tool = getByToolName("diamond");
|
||||
fireEvent.click(tool);
|
||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||
fireEvent.pointerMove(canvas, { clientX: -1000, clientY: -1000 });
|
||||
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
||||
fireEvent.pointerUp(canvas);
|
||||
fireEvent.keyDown(document, {
|
||||
@@ -367,6 +377,7 @@ describe("select single element on the scene", () => {
|
||||
const tool = getByToolName("ellipse");
|
||||
fireEvent.click(tool);
|
||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||
fireEvent.pointerMove(canvas, { clientX: -1000, clientY: -1000 });
|
||||
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
||||
fireEvent.pointerUp(canvas);
|
||||
fireEvent.keyDown(document, {
|
||||
@@ -399,6 +410,7 @@ describe("select single element on the scene", () => {
|
||||
const tool = getByToolName("arrow");
|
||||
fireEvent.click(tool);
|
||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||
fireEvent.pointerMove(canvas, { clientX: -1000, clientY: -1000 });
|
||||
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
||||
fireEvent.pointerUp(canvas);
|
||||
fireEvent.keyDown(document, {
|
||||
@@ -425,8 +437,8 @@ describe("select single element on the scene", () => {
|
||||
fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 });
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(10);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(8);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(1);
|
||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||
@@ -443,6 +455,7 @@ describe("select single element on the scene", () => {
|
||||
const tool = getByToolName("line");
|
||||
fireEvent.click(tool);
|
||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||
fireEvent.pointerMove(canvas, { clientX: -1000, clientY: -1000 });
|
||||
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
||||
fireEvent.pointerUp(canvas);
|
||||
fireEvent.keyDown(document, {
|
||||
@@ -469,8 +482,8 @@ describe("select single element on the scene", () => {
|
||||
fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 });
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(10);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(8);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(1);
|
||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||
@@ -525,6 +538,7 @@ describe("selectedElementIds stability", () => {
|
||||
expect(h.state.selectedElementIds).toBe(selectedElementIds_1);
|
||||
|
||||
mouse.downAt(-50, -50);
|
||||
mouse.move(-1000, -1000);
|
||||
mouse.moveTo(50, 50);
|
||||
|
||||
const selectedElementIds_2 = h.state.selectedElementIds;
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
"devDependencies": {
|
||||
"cross-env": "7.0.3",
|
||||
"fonteditor-core": "2.4.0",
|
||||
"typescript": "4.9.4",
|
||||
"typescript": "5.9.3",
|
||||
"wawoff2": "2.0.1",
|
||||
"which": "4.0.0"
|
||||
},
|
||||
|
||||
@@ -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,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,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,
|
||||
|
||||
@@ -9290,10 +9290,10 @@ typed-array-length@^1.0.7:
|
||||
possible-typed-array-names "^1.0.0"
|
||||
reflect.getprototypeof "^1.0.6"
|
||||
|
||||
typescript@4.9.4:
|
||||
version "4.9.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78"
|
||||
integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==
|
||||
typescript@5.9.3:
|
||||
version "5.9.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
|
||||
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
|
||||
|
||||
typescript@^5:
|
||||
version "5.8.2"
|
||||
|
||||
Reference in New Issue
Block a user