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
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
- name: Setup Node.js 18.x
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 20.x
|
||||||
- name: Set up publish access
|
- name: Set up publish access
|
||||||
run: |
|
run: |
|
||||||
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
|
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Setup Node.js 18.x
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 20.x
|
||||||
|
|
||||||
- name: Install and lint
|
- name: Install and lint
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
|
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
|
||||||
|
|
||||||
- name: Setup Node.js 18.x
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 20.x
|
||||||
|
|
||||||
- name: Create report file
|
- name: Create report file
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Setup Node.js 18.x
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 20.x
|
||||||
- name: Install and build
|
- name: Install and build
|
||||||
run: |
|
run: |
|
||||||
yarn --frozen-lockfile
|
yarn --frozen-lockfile
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: Setup Node.js 18.x
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 20.x
|
||||||
- name: Install in packages/excalidraw
|
- name: Install in packages/excalidraw
|
||||||
run: yarn
|
run: yarn
|
||||||
working-directory: packages/excalidraw
|
working-directory: packages/excalidraw
|
||||||
|
|||||||
@@ -441,7 +441,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private decryptPayload = async (
|
private decryptPayload = async (
|
||||||
iv: Uint8Array,
|
iv: Uint8Array<ArrayBuffer>,
|
||||||
encryptedData: ArrayBuffer,
|
encryptedData: ArrayBuffer,
|
||||||
decryptionKey: string,
|
decryptionKey: string,
|
||||||
): Promise<ValueOf<SocketUpdateDataSource>> => {
|
): Promise<ValueOf<SocketUpdateDataSource>> => {
|
||||||
@@ -562,7 +562,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||||||
// All socket listeners are moving to Portal
|
// All socket listeners are moving to Portal
|
||||||
this.portal.socket.on(
|
this.portal.socket.on(
|
||||||
"client-broadcast",
|
"client-broadcast",
|
||||||
async (encryptedData: ArrayBuffer, iv: Uint8Array) => {
|
async (encryptedData: ArrayBuffer, iv: Uint8Array<ArrayBuffer>) => {
|
||||||
if (!this.portal.roomKey) {
|
if (!this.portal.roomKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -746,7 +746,10 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||||||
): ReconciledExcalidrawElement[] => {
|
): ReconciledExcalidrawElement[] => {
|
||||||
const localElements = this.getSceneElementsIncludingDeleted();
|
const localElements = this.getSceneElementsIncludingDeleted();
|
||||||
const appState = this.excalidrawAPI.getAppState();
|
const appState = this.excalidrawAPI.getAppState();
|
||||||
const restoredRemoteElements = restoreElements(remoteElements, null);
|
const restoredRemoteElements = restoreElements(
|
||||||
|
remoteElements,
|
||||||
|
this.excalidrawAPI.getSceneElementsMapIncludingDeleted(),
|
||||||
|
);
|
||||||
const reconciledElements = reconcileElements(
|
const reconciledElements = reconcileElements(
|
||||||
localElements,
|
localElements,
|
||||||
restoredRemoteElements as RemoteExcalidrawElement[],
|
restoredRemoteElements as RemoteExcalidrawElement[],
|
||||||
|
|||||||
@@ -105,8 +105,8 @@ const decryptElements = async (
|
|||||||
data: FirebaseStoredScene,
|
data: FirebaseStoredScene,
|
||||||
roomKey: string,
|
roomKey: string,
|
||||||
): Promise<readonly ExcalidrawElement[]> => {
|
): Promise<readonly ExcalidrawElement[]> => {
|
||||||
const ciphertext = data.ciphertext.toUint8Array();
|
const ciphertext = data.ciphertext.toUint8Array() as Uint8Array<ArrayBuffer>;
|
||||||
const iv = data.iv.toUint8Array();
|
const iv = data.iv.toUint8Array() as Uint8Array<ArrayBuffer>;
|
||||||
|
|
||||||
const decrypted = await decryptData(iv, ciphertext, roomKey);
|
const decrypted = await decryptData(iv, ciphertext, roomKey);
|
||||||
const decodedData = new TextDecoder("utf-8").decode(
|
const decodedData = new TextDecoder("utf-8").decode(
|
||||||
|
|||||||
+1
-1
@@ -34,7 +34,7 @@
|
|||||||
"prettier": "2.6.2",
|
"prettier": "2.6.2",
|
||||||
"rewire": "6.0.0",
|
"rewire": "6.0.0",
|
||||||
"rimraf": "^5.0.0",
|
"rimraf": "^5.0.0",
|
||||||
"typescript": "4.9.4",
|
"typescript": "5.9.3",
|
||||||
"vite": "5.0.12",
|
"vite": "5.0.12",
|
||||||
"vite-plugin-checker": "0.7.2",
|
"vite-plugin-checker": "0.7.2",
|
||||||
"vite-plugin-ejs": "1.7.0",
|
"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",
|
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 CJK_HAND_DRAWN_FALLBACK_FONT = "Xiaolai";
|
||||||
export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
|
export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
|
||||||
|
|
||||||
@@ -410,7 +417,6 @@ export const DEFAULT_ELEMENT_PROPS: {
|
|||||||
roughness: ExcalidrawElement["roughness"];
|
roughness: ExcalidrawElement["roughness"];
|
||||||
opacity: ExcalidrawElement["opacity"];
|
opacity: ExcalidrawElement["opacity"];
|
||||||
locked: ExcalidrawElement["locked"];
|
locked: ExcalidrawElement["locked"];
|
||||||
containerBehavior: ExcalidrawElement["containerBehavior"];
|
|
||||||
} = {
|
} = {
|
||||||
strokeColor: COLOR_PALETTE.black,
|
strokeColor: COLOR_PALETTE.black,
|
||||||
backgroundColor: COLOR_PALETTE.transparent,
|
backgroundColor: COLOR_PALETTE.transparent,
|
||||||
@@ -420,10 +426,6 @@ export const DEFAULT_ELEMENT_PROPS: {
|
|||||||
roughness: ROUGHNESS.artist,
|
roughness: ROUGHNESS.artist,
|
||||||
opacity: 100,
|
opacity: 100,
|
||||||
locked: false,
|
locked: false,
|
||||||
containerBehavior: {
|
|
||||||
textFlow: "growing",
|
|
||||||
margin: BOUND_TEXT_PADDING,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LIBRARY_SIDEBAR_TAB = "library";
|
export const LIBRARY_SIDEBAR_TAB = "library";
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from "./binary-heap";
|
export * from "./binary-heap";
|
||||||
|
export * from "./bounds";
|
||||||
export * from "./colors";
|
export * from "./colors";
|
||||||
export * from "./constants";
|
export * from "./constants";
|
||||||
export * from "./font-metadata";
|
export * from "./font-metadata";
|
||||||
|
|||||||
@@ -6,12 +6,10 @@ import {
|
|||||||
type LocalPoint,
|
type LocalPoint,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import { isBounds } from "@excalidraw/element";
|
|
||||||
|
|
||||||
import type { Curve } from "@excalidraw/math";
|
import type { Curve } from "@excalidraw/math";
|
||||||
import type { LineSegment } from "@excalidraw/utils";
|
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
|
// The global data holder to collect the debug operations
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
+5
-4
@@ -1,11 +1,12 @@
|
|||||||
import { pointFrom } from "@excalidraw/math";
|
import { pointFrom } from "@excalidraw/math";
|
||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
|
|
||||||
import type { ExcalidrawArrowElement } from "@excalidraw/element/types";
|
import {
|
||||||
|
convertToExcalidrawElements,
|
||||||
|
type ExcalidrawElementSkeleton,
|
||||||
|
} from "../transform";
|
||||||
|
|
||||||
import { convertToExcalidrawElements } from "./transform";
|
import type { ExcalidrawArrowElement } from "../types";
|
||||||
|
|
||||||
import type { ExcalidrawElementSkeleton } from "./transform";
|
|
||||||
|
|
||||||
const opts = { regenerateIds: false };
|
const opts = { regenerateIds: false };
|
||||||
|
|
||||||
@@ -22,10 +22,9 @@ import {
|
|||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import type { LineSegment, LocalPoint, Radians } from "@excalidraw/math";
|
import type { LineSegment, LocalPoint, Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import type { MapEntry, Mutable } from "@excalidraw/common/utility-types";
|
import type { MapEntry, Mutable } from "@excalidraw/common/utility-types";
|
||||||
|
import type { Bounds } from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
doBoundsIntersect,
|
doBoundsIntersect,
|
||||||
@@ -54,17 +53,21 @@ import {
|
|||||||
isBindableElement,
|
isBindableElement,
|
||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
|
isRectangularElement,
|
||||||
isRectanguloidElement,
|
isRectanguloidElement,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
|
|
||||||
import { aabbForElement, elementCenterPoint } from "./bounds";
|
import { aabbForElement, elementCenterPoint } from "./bounds";
|
||||||
import { updateElbowArrowPoints } from "./elbowArrow";
|
import { updateElbowArrowPoints } from "./elbowArrow";
|
||||||
import { projectFixedPointOntoDiagonal } from "./utils";
|
import {
|
||||||
|
deconstructDiamondElement,
|
||||||
|
deconstructRectanguloidElement,
|
||||||
|
projectFixedPointOntoDiagonal,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
import type { Scene } from "./Scene";
|
import type { Scene } from "./Scene";
|
||||||
|
|
||||||
import type { Bounds } from "./bounds";
|
|
||||||
import type { ElementUpdate } from "./mutateElement";
|
import type { ElementUpdate } from "./mutateElement";
|
||||||
import type {
|
import type {
|
||||||
BindMode,
|
BindMode,
|
||||||
@@ -73,6 +76,7 @@ import type {
|
|||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
|
ExcalidrawRectanguloidElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
FixedPoint,
|
FixedPoint,
|
||||||
FixedPointBinding,
|
FixedPointBinding,
|
||||||
@@ -146,17 +150,22 @@ export const isBindingEnabled = (appState: AppState): boolean => {
|
|||||||
export const bindOrUnbindBindingElement = (
|
export const bindOrUnbindBindingElement = (
|
||||||
arrow: NonDeleted<ExcalidrawArrowElement>,
|
arrow: NonDeleted<ExcalidrawArrowElement>,
|
||||||
draggingPoints: PointsPositionUpdates,
|
draggingPoints: PointsPositionUpdates,
|
||||||
|
scenePointerX: number,
|
||||||
|
scenePointerY: number,
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
opts?: {
|
opts?: {
|
||||||
newArrow?: boolean;
|
newArrow?: boolean;
|
||||||
altKey?: boolean;
|
altKey?: boolean;
|
||||||
|
angleLocked?: boolean;
|
||||||
initialBinding?: boolean;
|
initialBinding?: boolean;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const { start, end } = getBindingStrategyForDraggingBindingElementEndpoints(
|
const { start, end } = getBindingStrategyForDraggingBindingElementEndpoints(
|
||||||
arrow,
|
arrow,
|
||||||
draggingPoints,
|
draggingPoints,
|
||||||
|
scenePointerX,
|
||||||
|
scenePointerY,
|
||||||
scene.getNonDeletedElementsMap(),
|
scene.getNonDeletedElementsMap(),
|
||||||
scene.getNonDeletedElements(),
|
scene.getNonDeletedElements(),
|
||||||
appState,
|
appState,
|
||||||
@@ -556,12 +565,14 @@ const bindingStrategyForSimpleArrowEndpointDragging_complex = (
|
|||||||
export const getBindingStrategyForDraggingBindingElementEndpoints = (
|
export const getBindingStrategyForDraggingBindingElementEndpoints = (
|
||||||
arrow: NonDeleted<ExcalidrawArrowElement>,
|
arrow: NonDeleted<ExcalidrawArrowElement>,
|
||||||
draggingPoints: PointsPositionUpdates,
|
draggingPoints: PointsPositionUpdates,
|
||||||
|
screenPointerX: number,
|
||||||
|
screenPointerY: number,
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
opts?: {
|
opts?: {
|
||||||
newArrow?: boolean;
|
newArrow?: boolean;
|
||||||
shiftKey?: boolean;
|
angleLocked?: boolean;
|
||||||
altKey?: boolean;
|
altKey?: boolean;
|
||||||
finalize?: boolean;
|
finalize?: boolean;
|
||||||
initialBinding?: boolean;
|
initialBinding?: boolean;
|
||||||
@@ -582,6 +593,8 @@ export const getBindingStrategyForDraggingBindingElementEndpoints = (
|
|||||||
return getBindingStrategyForDraggingBindingElementEndpoints_simple(
|
return getBindingStrategyForDraggingBindingElementEndpoints_simple(
|
||||||
arrow,
|
arrow,
|
||||||
draggingPoints,
|
draggingPoints,
|
||||||
|
screenPointerX,
|
||||||
|
screenPointerY,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
@@ -592,12 +605,14 @@ export const getBindingStrategyForDraggingBindingElementEndpoints = (
|
|||||||
const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
||||||
arrow: NonDeleted<ExcalidrawArrowElement>,
|
arrow: NonDeleted<ExcalidrawArrowElement>,
|
||||||
draggingPoints: PointsPositionUpdates,
|
draggingPoints: PointsPositionUpdates,
|
||||||
|
scenePointerX: number,
|
||||||
|
scenePointerY: number,
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
opts?: {
|
opts?: {
|
||||||
newArrow?: boolean;
|
newArrow?: boolean;
|
||||||
shiftKey?: boolean;
|
angleLocked?: boolean;
|
||||||
altKey?: boolean;
|
altKey?: boolean;
|
||||||
finalize?: boolean;
|
finalize?: boolean;
|
||||||
initialBinding?: boolean;
|
initialBinding?: boolean;
|
||||||
@@ -669,7 +684,15 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
|||||||
elementsMap,
|
elementsMap,
|
||||||
(e) => maxBindingDistance_simple(appState.zoom),
|
(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
|
const otherBindableElement = otherBinding
|
||||||
? (elementsMap.get(
|
? (elementsMap.get(
|
||||||
otherBinding.elementId,
|
otherBinding.elementId,
|
||||||
@@ -770,6 +793,12 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
|||||||
}
|
}
|
||||||
: { mode: null };
|
: { mode: null };
|
||||||
|
|
||||||
|
const otherEndpoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||||
|
arrow,
|
||||||
|
startDragged ? -1 : 0,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
const other: BindingStrategy =
|
const other: BindingStrategy =
|
||||||
otherBindableElement &&
|
otherBindableElement &&
|
||||||
!otherFocusPointIsInElement &&
|
!otherFocusPointIsInElement &&
|
||||||
@@ -779,6 +808,19 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
|||||||
element: otherBindableElement,
|
element: otherBindableElement,
|
||||||
focusPoint: appState.selectedLinearElement.initialState.altFocusPoint,
|
focusPoint: appState.selectedLinearElement.initialState.altFocusPoint,
|
||||||
}
|
}
|
||||||
|
: opts?.angleLocked && otherBindableElement
|
||||||
|
? {
|
||||||
|
mode: "orbit",
|
||||||
|
element: otherBindableElement,
|
||||||
|
focusPoint:
|
||||||
|
projectFixedPointOntoDiagonal(
|
||||||
|
arrow,
|
||||||
|
otherEndpoint,
|
||||||
|
otherBindableElement,
|
||||||
|
startDragged ? "end" : "start",
|
||||||
|
elementsMap,
|
||||||
|
) || otherEndpoint,
|
||||||
|
}
|
||||||
: { mode: undefined };
|
: { mode: undefined };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -924,6 +966,8 @@ export const bindOrUnbindBindingElements = (
|
|||||||
bindOrUnbindBindingElement(
|
bindOrUnbindBindingElement(
|
||||||
arrow,
|
arrow,
|
||||||
new Map(), // No dragging points in this case
|
new Map(), // No dragging points in this case
|
||||||
|
Infinity,
|
||||||
|
Infinity,
|
||||||
scene,
|
scene,
|
||||||
appState,
|
appState,
|
||||||
);
|
);
|
||||||
@@ -1126,7 +1170,14 @@ export const updateBindings = (
|
|||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
if (isArrowElement(latestElement)) {
|
if (isArrowElement(latestElement)) {
|
||||||
bindOrUnbindBindingElement(latestElement, new Map(), scene, appState);
|
bindOrUnbindBindingElement(
|
||||||
|
latestElement,
|
||||||
|
new Map(),
|
||||||
|
Infinity,
|
||||||
|
Infinity,
|
||||||
|
scene,
|
||||||
|
appState,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
updateBoundElements(latestElement, scene, {
|
updateBoundElements(latestElement, scene, {
|
||||||
...options,
|
...options,
|
||||||
@@ -2289,3 +2340,434 @@ export const normalizeFixedPoint = <T extends FixedPoint | null>(
|
|||||||
}
|
}
|
||||||
return fixedPoint as any as T extends null ? null : FixedPoint;
|
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 {
|
import {
|
||||||
arrayToMap,
|
arrayToMap,
|
||||||
|
type Bounds,
|
||||||
invariant,
|
invariant,
|
||||||
rescalePoints,
|
rescalePoints,
|
||||||
sizeOf,
|
sizeOf,
|
||||||
@@ -78,16 +79,6 @@ export type RectangleBox = {
|
|||||||
|
|
||||||
type MaybeQuadraticSolution = [number | null, number | null] | false;
|
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 [
|
export type SceneBounds = readonly [
|
||||||
sceneX: number,
|
sceneX: number,
|
||||||
sceneY: number,
|
sceneY: number,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { invariant, isTransparent } from "@excalidraw/common";
|
import { invariant, isTransparent, type Bounds } from "@excalidraw/common";
|
||||||
import {
|
import {
|
||||||
curveIntersectLineSegment,
|
curveIntersectLineSegment,
|
||||||
isPointWithinBounds,
|
isPointWithinBounds,
|
||||||
@@ -29,7 +29,6 @@ import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
|
|||||||
|
|
||||||
import { isPathALoop } from "./utils";
|
import { isPathALoop } from "./utils";
|
||||||
import {
|
import {
|
||||||
type Bounds,
|
|
||||||
doBoundsIntersect,
|
doBoundsIntersect,
|
||||||
elementCenterPoint,
|
elementCenterPoint,
|
||||||
getCenterForBounds,
|
getCenterForBounds,
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ export const hasStrokeColor = (type: ElementOrToolType) =>
|
|||||||
type === "freedraw" ||
|
type === "freedraw" ||
|
||||||
type === "arrow" ||
|
type === "arrow" ||
|
||||||
type === "line" ||
|
type === "line" ||
|
||||||
type === "text";
|
type === "text" ||
|
||||||
|
type === "embeddable";
|
||||||
|
|
||||||
export const hasStrokeWidth = (type: ElementOrToolType) =>
|
export const hasStrokeWidth = (type: ElementOrToolType) =>
|
||||||
type === "rectangle" ||
|
type === "rectangle" ||
|
||||||
@@ -45,9 +46,6 @@ export const canChangeRoundness = (type: ElementOrToolType) =>
|
|||||||
type === "diamond" ||
|
type === "diamond" ||
|
||||||
type === "image";
|
type === "image";
|
||||||
|
|
||||||
export const hasContainerBehavior = (type: ElementOrToolType) =>
|
|
||||||
type === "rectangle" || type === "diamond" || type === "ellipse";
|
|
||||||
|
|
||||||
export const toolIsArrow = (type: ElementOrToolType) => type === "arrow";
|
export const toolIsArrow = (type: ElementOrToolType) => type === "arrow";
|
||||||
|
|
||||||
export const canHaveArrowheads = (type: ElementOrToolType) => type === "arrow";
|
export const canHaveArrowheads = (type: ElementOrToolType) => type === "arrow";
|
||||||
|
|||||||
@@ -2056,14 +2056,11 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static stripIrrelevantProps(partial: ElementPartial): ElementPartial {
|
private static stripIrrelevantProps(
|
||||||
const {
|
partial: Partial<OrderedExcalidrawElement>,
|
||||||
id: _id,
|
): ElementPartial {
|
||||||
updated: _updated,
|
const { id, updated, ...strippedPartial } = partial;
|
||||||
seed: _seed,
|
|
||||||
...strippedPartial
|
|
||||||
} = partial as any;
|
|
||||||
|
|
||||||
return strippedPartial as ElementPartial;
|
return strippedPartial;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
type Bounds,
|
||||||
TEXT_AUTOWRAP_THRESHOLD,
|
TEXT_AUTOWRAP_THRESHOLD,
|
||||||
getGridPoint,
|
getGridPoint,
|
||||||
getFontString,
|
getFontString,
|
||||||
@@ -29,7 +30,6 @@ import {
|
|||||||
|
|
||||||
import type { Scene } from "./Scene";
|
import type { Scene } from "./Scene";
|
||||||
|
|
||||||
import type { Bounds } from "./bounds";
|
|
||||||
import type { ExcalidrawElement } from "./types";
|
import type { ExcalidrawElement } from "./types";
|
||||||
|
|
||||||
export const dragSelectedElements = (
|
export const dragSelectedElements = (
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
type Bounds,
|
||||||
BinaryHeap,
|
BinaryHeap,
|
||||||
invariant,
|
invariant,
|
||||||
isAnyTrue,
|
isAnyTrue,
|
||||||
@@ -54,7 +55,6 @@ import {
|
|||||||
import { aabbForElement, pointInsideBounds } from "./bounds";
|
import { aabbForElement, pointInsideBounds } from "./bounds";
|
||||||
import { getHoveredElementForBinding } from "./collision";
|
import { getHoveredElementForBinding } from "./collision";
|
||||||
|
|
||||||
import type { Bounds } from "./bounds";
|
|
||||||
import type { Heading } from "./heading";
|
import type { Heading } from "./heading";
|
||||||
import type {
|
import type {
|
||||||
Arrowhead,
|
Arrowhead,
|
||||||
|
|||||||
@@ -268,7 +268,6 @@ const addNewNode = (
|
|||||||
opacity: element.opacity,
|
opacity: element.opacity,
|
||||||
fillStyle: element.fillStyle,
|
fillStyle: element.fillStyle,
|
||||||
strokeStyle: element.strokeStyle,
|
strokeStyle: element.strokeStyle,
|
||||||
containerBehavior: element.containerBehavior,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
invariant(
|
invariant(
|
||||||
@@ -347,7 +346,6 @@ export const addNewNodes = (
|
|||||||
opacity: startNode.opacity,
|
opacity: startNode.opacity,
|
||||||
fillStyle: startNode.fillStyle,
|
fillStyle: startNode.fillStyle,
|
||||||
strokeStyle: startNode.strokeStyle,
|
strokeStyle: startNode.strokeStyle,
|
||||||
containerBehavior: startNode.containerBehavior,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
invariant(
|
invariant(
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { invariant, isDevEnv, isTestEnv } from "@excalidraw/common";
|
import {
|
||||||
|
invariant,
|
||||||
|
isDevEnv,
|
||||||
|
isTestEnv,
|
||||||
|
type Bounds,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
pointFrom,
|
pointFrom,
|
||||||
@@ -19,7 +24,7 @@ import type {
|
|||||||
Vector,
|
Vector,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import { getCenterForBounds, type Bounds } from "./bounds";
|
import { getCenterForBounds } from "./bounds";
|
||||||
|
|
||||||
import type { ExcalidrawBindableElement } from "./types";
|
import type { ExcalidrawBindableElement } from "./types";
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ export * from "./store";
|
|||||||
export * from "./textElement";
|
export * from "./textElement";
|
||||||
export * from "./textMeasurements";
|
export * from "./textMeasurements";
|
||||||
export * from "./textWrapping";
|
export * from "./textWrapping";
|
||||||
|
export * from "./transform";
|
||||||
export * from "./transformHandles";
|
export * from "./transformHandles";
|
||||||
export * from "./typeChecks";
|
export * from "./typeChecks";
|
||||||
export * from "./utils";
|
export * from "./utils";
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import {
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
deconstructLinearOrFreeDrawElement,
|
deconstructLinearOrFreeDrawElement,
|
||||||
getHoveredElementForBinding,
|
|
||||||
isPathALoop,
|
isPathALoop,
|
||||||
moveArrowAboveBindable,
|
moveArrowAboveBindable,
|
||||||
projectFixedPointOntoDiagonal,
|
projectFixedPointOntoDiagonal,
|
||||||
@@ -43,6 +42,7 @@ import type {
|
|||||||
NullableGridSize,
|
NullableGridSize,
|
||||||
Zoom,
|
Zoom,
|
||||||
} from "@excalidraw/excalidraw/types";
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
import type { Bounds } from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
calculateFixedPointForNonElbowArrowBinding,
|
calculateFixedPointForNonElbowArrowBinding,
|
||||||
@@ -69,7 +69,6 @@ import { isLineElement } from "./typeChecks";
|
|||||||
|
|
||||||
import type { Scene } from "./Scene";
|
import type { Scene } from "./Scene";
|
||||||
|
|
||||||
import type { Bounds } from "./bounds";
|
|
||||||
import type {
|
import type {
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
@@ -306,21 +305,11 @@ export class LinearElementEditor {
|
|||||||
const customLineAngle =
|
const customLineAngle =
|
||||||
linearElementEditor.customLineAngle ??
|
linearElementEditor.customLineAngle ??
|
||||||
determineCustomLinearAngle(pivotPoint, element.points[idx]);
|
determineCustomLinearAngle(pivotPoint, element.points[idx]);
|
||||||
const hoveredElement = getHoveredElementForBinding(
|
|
||||||
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
|
|
||||||
elements,
|
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Determine if point movement should happen and how much
|
// Determine if point movement should happen and how much
|
||||||
let deltaX = 0;
|
let deltaX = 0;
|
||||||
let deltaY = 0;
|
let deltaY = 0;
|
||||||
if (
|
if (shouldRotateWithDiscreteAngle(event)) {
|
||||||
shouldRotateWithDiscreteAngle(event) &&
|
|
||||||
!hoveredElement &&
|
|
||||||
!element.startBinding &&
|
|
||||||
!element.endBinding
|
|
||||||
) {
|
|
||||||
const [width, height] = LinearElementEditor._getShiftLockedDelta(
|
const [width, height] = LinearElementEditor._getShiftLockedDelta(
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@@ -354,11 +343,13 @@ export class LinearElementEditor {
|
|||||||
[idx],
|
[idx],
|
||||||
deltaX,
|
deltaX,
|
||||||
deltaY,
|
deltaY,
|
||||||
|
scenePointerX,
|
||||||
|
scenePointerY,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
element,
|
element,
|
||||||
elements,
|
elements,
|
||||||
app,
|
app,
|
||||||
event.shiftKey,
|
shouldRotateWithDiscreteAngle(event),
|
||||||
event.altKey,
|
event.altKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -492,22 +483,11 @@ export class LinearElementEditor {
|
|||||||
const endIsSelected = selectedPointsIndices.includes(
|
const endIsSelected = selectedPointsIndices.includes(
|
||||||
element.points.length - 1,
|
element.points.length - 1,
|
||||||
);
|
);
|
||||||
const hoveredElement = getHoveredElementForBinding(
|
|
||||||
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
|
|
||||||
elements,
|
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Determine if point movement should happen and how much
|
// Determine if point movement should happen and how much
|
||||||
let deltaX = 0;
|
let deltaX = 0;
|
||||||
let deltaY = 0;
|
let deltaY = 0;
|
||||||
if (
|
if (shouldRotateWithDiscreteAngle(event) && singlePointDragged) {
|
||||||
shouldRotateWithDiscreteAngle(event) &&
|
|
||||||
singlePointDragged &&
|
|
||||||
!hoveredElement &&
|
|
||||||
!element.startBinding &&
|
|
||||||
!element.endBinding
|
|
||||||
) {
|
|
||||||
const [width, height] = LinearElementEditor._getShiftLockedDelta(
|
const [width, height] = LinearElementEditor._getShiftLockedDelta(
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@@ -520,7 +500,6 @@ export class LinearElementEditor {
|
|||||||
width + pivotPoint[0],
|
width + pivotPoint[0],
|
||||||
height + pivotPoint[1],
|
height + pivotPoint[1],
|
||||||
);
|
);
|
||||||
|
|
||||||
deltaX = target[0] - draggingPoint[0];
|
deltaX = target[0] - draggingPoint[0];
|
||||||
deltaY = target[1] - draggingPoint[1];
|
deltaY = target[1] - draggingPoint[1];
|
||||||
} else {
|
} else {
|
||||||
@@ -541,11 +520,13 @@ export class LinearElementEditor {
|
|||||||
selectedPointsIndices,
|
selectedPointsIndices,
|
||||||
deltaX,
|
deltaX,
|
||||||
deltaY,
|
deltaY,
|
||||||
|
scenePointerX,
|
||||||
|
scenePointerY,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
element,
|
element,
|
||||||
elements,
|
elements,
|
||||||
app,
|
app,
|
||||||
event.shiftKey,
|
shouldRotateWithDiscreteAngle(event) && singlePointDragged,
|
||||||
event.altKey,
|
event.altKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -2088,11 +2069,13 @@ const pointDraggingUpdates = (
|
|||||||
selectedPointsIndices: readonly number[],
|
selectedPointsIndices: readonly number[],
|
||||||
deltaX: number,
|
deltaX: number,
|
||||||
deltaY: number,
|
deltaY: number,
|
||||||
|
scenePointerX: number,
|
||||||
|
scenePointerY: number,
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
shiftKey: boolean,
|
angleLocked: boolean,
|
||||||
altKey: boolean,
|
altKey: boolean,
|
||||||
): {
|
): {
|
||||||
positions: PointsPositionUpdates;
|
positions: PointsPositionUpdates;
|
||||||
@@ -2128,12 +2111,14 @@ const pointDraggingUpdates = (
|
|||||||
const { start, end } = getBindingStrategyForDraggingBindingElementEndpoints(
|
const { start, end } = getBindingStrategyForDraggingBindingElementEndpoints(
|
||||||
element,
|
element,
|
||||||
naiveDraggingPoints,
|
naiveDraggingPoints,
|
||||||
|
scenePointerX,
|
||||||
|
scenePointerY,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
elements,
|
elements,
|
||||||
app.state,
|
app.state,
|
||||||
{
|
{
|
||||||
newArrow: !!app.state.newElement,
|
newArrow: !!app.state.newElement,
|
||||||
shiftKey,
|
angleLocked,
|
||||||
altKey,
|
altKey,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -2250,10 +2235,15 @@ const pointDraggingUpdates = (
|
|||||||
// We need to use a custom intersector to ensure that if there is a big "jump"
|
// 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
|
// in the arrow's position, we can position it with outline avoidance
|
||||||
// pixel-perfectly and avoid "dancing" arrows.
|
// pixel-perfectly and avoid "dancing" arrows.
|
||||||
const customIntersector =
|
// NOTE: Direction matters here, so we create two intersectors
|
||||||
|
const startCustomIntersector =
|
||||||
start.focusPoint && end.focusPoint
|
start.focusPoint && end.focusPoint
|
||||||
? lineSegment(start.focusPoint, end.focusPoint)
|
? lineSegment(start.focusPoint, end.focusPoint)
|
||||||
: undefined;
|
: 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
|
// Needed to handle a special case where an existing arrow is dragged over
|
||||||
// the same element it is bound to on the other side
|
// the same element it is bound to on the other side
|
||||||
@@ -2290,7 +2280,7 @@ const pointDraggingUpdates = (
|
|||||||
nextArrow.endBinding,
|
nextArrow.endBinding,
|
||||||
endBindable,
|
endBindable,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
customIntersector,
|
endCustomIntersector,
|
||||||
) || nextArrow.points[nextArrow.points.length - 1]
|
) || nextArrow.points[nextArrow.points.length - 1]
|
||||||
: nextArrow.points[nextArrow.points.length - 1];
|
: nextArrow.points[nextArrow.points.length - 1];
|
||||||
|
|
||||||
@@ -2321,7 +2311,7 @@ const pointDraggingUpdates = (
|
|||||||
nextArrow.startBinding,
|
nextArrow.startBinding,
|
||||||
startBindable,
|
startBindable,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
customIntersector,
|
startCustomIntersector,
|
||||||
) || nextArrow.points[0]
|
) || nextArrow.points[0]
|
||||||
: nextArrow.points[0];
|
: nextArrow.points[0];
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
getFontString,
|
getFontString,
|
||||||
getUpdatedTimestamp,
|
getUpdatedTimestamp,
|
||||||
getLineHeight,
|
getLineHeight,
|
||||||
BOUND_TEXT_PADDING,
|
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import type { Radians } from "@excalidraw/math";
|
import type { Radians } from "@excalidraw/math";
|
||||||
@@ -22,11 +21,11 @@ import {
|
|||||||
getResizedElementAbsoluteCoords,
|
getResizedElementAbsoluteCoords,
|
||||||
} from "./bounds";
|
} from "./bounds";
|
||||||
import { newElementWith } from "./mutateElement";
|
import { newElementWith } from "./mutateElement";
|
||||||
import { getBoundTextMaxWidth, getBoundTextMaxHeight } from "./textElement";
|
import { getBoundTextMaxWidth, getInitialTextMetrics } from "./textElement";
|
||||||
import { normalizeText, measureText } from "./textMeasurements";
|
import { normalizeText, measureText } from "./textMeasurements";
|
||||||
import { wrapText } from "./textWrapping";
|
import { wrapText } from "./textWrapping";
|
||||||
|
|
||||||
import { isFlowchartType, isLineElement } from "./typeChecks";
|
import { isLineElement } from "./typeChecks";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
@@ -49,8 +48,6 @@ import type {
|
|||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
ExcalidrawLineElement,
|
ExcalidrawLineElement,
|
||||||
ExcalidrawFlowchartNodeElement,
|
|
||||||
ContainerBehavior,
|
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export type ElementConstructorOpts = MarkOptional<
|
export type ElementConstructorOpts = MarkOptional<
|
||||||
@@ -161,26 +158,9 @@ const _newElementBase = <T extends ExcalidrawElement>(
|
|||||||
export const newElement = (
|
export const newElement = (
|
||||||
opts: {
|
opts: {
|
||||||
type: ExcalidrawGenericElement["type"];
|
type: ExcalidrawGenericElement["type"];
|
||||||
containerBehavior?: ContainerBehavior;
|
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawGenericElement> => {
|
): NonDeleted<ExcalidrawGenericElement> =>
|
||||||
if (isFlowchartType(opts.type)) {
|
_newElementBase<ExcalidrawGenericElement>(opts.type, opts);
|
||||||
return {
|
|
||||||
..._newElementBase<ExcalidrawFlowchartNodeElement>(
|
|
||||||
opts.type as ExcalidrawFlowchartNodeElement["type"],
|
|
||||||
opts,
|
|
||||||
),
|
|
||||||
containerBehavior: {
|
|
||||||
textFlow: opts.containerBehavior?.textFlow ?? "growing",
|
|
||||||
margin: opts.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
|
|
||||||
},
|
|
||||||
} as NonDeleted<ExcalidrawFlowchartNodeElement>;
|
|
||||||
}
|
|
||||||
return _newElementBase<ExcalidrawGenericElement>(
|
|
||||||
opts.type,
|
|
||||||
opts,
|
|
||||||
) as NonDeleted<ExcalidrawGenericElement>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const newEmbeddableElement = (
|
export const newEmbeddableElement = (
|
||||||
opts: {
|
opts: {
|
||||||
@@ -256,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 = (
|
export const newTextElement = (
|
||||||
opts: {
|
opts: NewTextElementOptions,
|
||||||
text: string;
|
|
||||||
originalText?: string;
|
|
||||||
fontSize?: number;
|
|
||||||
fontFamily?: FontFamilyValues;
|
|
||||||
textAlign?: TextAlign;
|
|
||||||
verticalAlign?: VerticalAlign;
|
|
||||||
containerId?: ExcalidrawTextContainer["id"] | null;
|
|
||||||
lineHeight?: ExcalidrawTextElement["lineHeight"];
|
|
||||||
autoResize?: ExcalidrawTextElement["autoResize"];
|
|
||||||
} & ElementConstructorOpts,
|
|
||||||
): NonDeleted<ExcalidrawTextElement> => {
|
): NonDeleted<ExcalidrawTextElement> => {
|
||||||
const fontFamily = opts.fontFamily || DEFAULT_FONT_FAMILY;
|
const fontFamily = opts.fontFamily || DEFAULT_FONT_FAMILY;
|
||||||
const fontSize = opts.fontSize || DEFAULT_FONT_SIZE;
|
const fontSize = opts.fontSize || DEFAULT_FONT_SIZE;
|
||||||
const lineHeight = opts.lineHeight || getLineHeight(fontFamily);
|
const lineHeight = opts.lineHeight || getLineHeight(fontFamily);
|
||||||
const text = normalizeText(opts.text);
|
const normalizedText = normalizeText(opts.text);
|
||||||
const metrics = measureText(
|
const originalText = opts.originalText ?? normalizedText;
|
||||||
text,
|
const metrics = getInitialTextMetrics(
|
||||||
getFontString({ fontFamily, fontSize }),
|
{ ...opts, text: normalizedText },
|
||||||
lineHeight,
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
);
|
);
|
||||||
const textAlign = opts.textAlign || DEFAULT_TEXT_ALIGN;
|
const textAlign = opts.textAlign || DEFAULT_TEXT_ALIGN;
|
||||||
const verticalAlign = opts.verticalAlign || DEFAULT_VERTICAL_ALIGN;
|
const verticalAlign = opts.verticalAlign || DEFAULT_VERTICAL_ALIGN;
|
||||||
@@ -287,7 +270,7 @@ export const newTextElement = (
|
|||||||
|
|
||||||
const textElementProps: ExcalidrawTextElement = {
|
const textElementProps: ExcalidrawTextElement = {
|
||||||
..._newElementBase<ExcalidrawTextElement>("text", opts),
|
..._newElementBase<ExcalidrawTextElement>("text", opts),
|
||||||
text,
|
text: normalizedText,
|
||||||
fontSize,
|
fontSize,
|
||||||
fontFamily,
|
fontFamily,
|
||||||
textAlign,
|
textAlign,
|
||||||
@@ -297,7 +280,7 @@ export const newTextElement = (
|
|||||||
width: metrics.width,
|
width: metrics.width,
|
||||||
height: metrics.height,
|
height: metrics.height,
|
||||||
containerId: opts.containerId || null,
|
containerId: opts.containerId || null,
|
||||||
originalText: opts.originalText ?? text,
|
originalText,
|
||||||
autoResize: opts.autoResize ?? true,
|
autoResize: opts.autoResize ?? true,
|
||||||
lineHeight,
|
lineHeight,
|
||||||
};
|
};
|
||||||
@@ -437,96 +420,6 @@ const adjustXYWithRotation = (
|
|||||||
return [x, y];
|
return [x, y];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sticky note font sizing constants
|
|
||||||
export const STICKY_NOTE_FONT_STEP = 2;
|
|
||||||
export const STICKY_NOTE_MIN_FONT_SIZE = 8;
|
|
||||||
export const STICKY_NOTE_MAX_FONT_SIZE = 72;
|
|
||||||
|
|
||||||
export interface StickyNoteFontComputationResult {
|
|
||||||
fontSize: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
wrappedText: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computes the appropriate font size (snapped to step) so that the text fits
|
|
||||||
* inside the sticky note container without resizing the container.
|
|
||||||
* It first tries to shrink if overflowing, otherwise opportunistically enlarges
|
|
||||||
* (still snapped) while it fits. Width is constrained by wrap at container width.
|
|
||||||
*/
|
|
||||||
export const computeStickyNoteFontSize = (
|
|
||||||
text: string,
|
|
||||||
element: ExcalidrawTextElement,
|
|
||||||
container: ExcalidrawTextContainer,
|
|
||||||
maxGrowFontSize?: number,
|
|
||||||
): StickyNoteFontComputationResult => {
|
|
||||||
const step = STICKY_NOTE_FONT_STEP;
|
|
||||||
const maxH = getBoundTextMaxHeight(container, element as any);
|
|
||||||
const maxW = getBoundTextMaxWidth(container, element);
|
|
||||||
|
|
||||||
const snap = (size: number) =>
|
|
||||||
Math.max(
|
|
||||||
STICKY_NOTE_MIN_FONT_SIZE,
|
|
||||||
Math.max(step, Math.floor(size / step) * step),
|
|
||||||
);
|
|
||||||
|
|
||||||
let size = snap(element.fontSize);
|
|
||||||
const growthCap = snap(
|
|
||||||
maxGrowFontSize != null ? maxGrowFontSize : STICKY_NOTE_MAX_FONT_SIZE,
|
|
||||||
);
|
|
||||||
|
|
||||||
const lineHeight = element.lineHeight;
|
|
||||||
const fontFamily = element.fontFamily;
|
|
||||||
|
|
||||||
const measure = (fontSize: number) => {
|
|
||||||
const font = getFontString({ fontFamily, fontSize });
|
|
||||||
const wrappedText = wrapText(text, font, maxW);
|
|
||||||
const metrics = measureText(wrappedText, font, lineHeight);
|
|
||||||
return {
|
|
||||||
wrappedText,
|
|
||||||
width: Math.min(metrics.width, maxW),
|
|
||||||
height: metrics.height,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
let { wrappedText, width, height } = measure(size);
|
|
||||||
|
|
||||||
if (height > maxH) {
|
|
||||||
// shrink until fits or min
|
|
||||||
while (size > STICKY_NOTE_MIN_FONT_SIZE) {
|
|
||||||
const next = snap(size - step);
|
|
||||||
if (next === size) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
size = next;
|
|
||||||
({ wrappedText, width, height } = measure(size));
|
|
||||||
if (height <= maxH) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// grow back only up to growthCap (initial session font size)
|
|
||||||
while (size < growthCap) {
|
|
||||||
const next = snap(Math.min(size + step, growthCap));
|
|
||||||
if (next === size) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const m = measure(next);
|
|
||||||
if (m.height <= maxH) {
|
|
||||||
size = next;
|
|
||||||
wrappedText = m.wrappedText;
|
|
||||||
width = m.width;
|
|
||||||
height = m.height;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { fontSize: size, width, height, wrappedText };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const refreshTextDimensions = (
|
export const refreshTextDimensions = (
|
||||||
textElement: ExcalidrawTextElement,
|
textElement: ExcalidrawTextElement,
|
||||||
container: ExcalidrawTextContainer | null,
|
container: ExcalidrawTextContainer | null,
|
||||||
|
|||||||
@@ -329,24 +329,16 @@ const generateElementCanvas = (
|
|||||||
boundTextCanvasContext.translate(-shiftX, -shiftY);
|
boundTextCanvasContext.translate(-shiftX, -shiftY);
|
||||||
// Clear the bound text area
|
// Clear the bound text area
|
||||||
boundTextCanvasContext.clearRect(
|
boundTextCanvasContext.clearRect(
|
||||||
-(
|
-(boundTextElement.width / 2 + BOUND_TEXT_PADDING) *
|
||||||
boundTextElement.width / 2 +
|
|
||||||
(element.containerBehavior?.margin ?? BOUND_TEXT_PADDING)
|
|
||||||
) *
|
|
||||||
window.devicePixelRatio *
|
window.devicePixelRatio *
|
||||||
scale,
|
scale,
|
||||||
-(
|
-(boundTextElement.height / 2 + BOUND_TEXT_PADDING) *
|
||||||
boundTextElement.height / 2 +
|
|
||||||
(element.containerBehavior?.margin ?? BOUND_TEXT_PADDING)
|
|
||||||
) *
|
|
||||||
window.devicePixelRatio *
|
window.devicePixelRatio *
|
||||||
scale,
|
scale,
|
||||||
(boundTextElement.width +
|
(boundTextElement.width + BOUND_TEXT_PADDING * 2) *
|
||||||
(element.containerBehavior?.margin ?? BOUND_TEXT_PADDING) * 2) *
|
|
||||||
window.devicePixelRatio *
|
window.devicePixelRatio *
|
||||||
scale,
|
scale,
|
||||||
(boundTextElement.height +
|
(boundTextElement.height + BOUND_TEXT_PADDING * 2) *
|
||||||
(element.containerBehavior?.margin ?? BOUND_TEXT_PADDING) * 2) *
|
|
||||||
window.devicePixelRatio *
|
window.devicePixelRatio *
|
||||||
scale,
|
scale,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
SHIFT_LOCKING_ANGLE,
|
SHIFT_LOCKING_ANGLE,
|
||||||
rescalePoints,
|
rescalePoints,
|
||||||
getFontString,
|
getFontString,
|
||||||
BOUND_TEXT_PADDING,
|
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import type { GlobalPoint } from "@excalidraw/math";
|
import type { GlobalPoint } from "@excalidraw/math";
|
||||||
@@ -785,12 +784,10 @@ export const resizeSingleElement = (
|
|||||||
const minWidth = getApproxMinLineWidth(
|
const minWidth = getApproxMinLineWidth(
|
||||||
getFontString(boundTextElement),
|
getFontString(boundTextElement),
|
||||||
boundTextElement.lineHeight,
|
boundTextElement.lineHeight,
|
||||||
latestElement.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
|
|
||||||
);
|
);
|
||||||
const minHeight = getApproxMinLineHeight(
|
const minHeight = getApproxMinLineHeight(
|
||||||
boundTextElement.fontSize,
|
boundTextElement.fontSize,
|
||||||
boundTextElement.lineHeight,
|
boundTextElement.lineHeight,
|
||||||
latestElement.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
|
|
||||||
);
|
);
|
||||||
nextWidth = Math.max(nextWidth, minWidth);
|
nextWidth = Math.max(nextWidth, minWidth);
|
||||||
nextHeight = Math.max(nextHeight, minHeight);
|
nextHeight = Math.max(nextHeight, minHeight);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import type { GlobalPoint, LineSegment, LocalPoint } from "@excalidraw/math";
|
import type { GlobalPoint, LineSegment, LocalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
import type { AppState, Zoom } from "@excalidraw/excalidraw/types";
|
import type { AppState, Zoom } from "@excalidraw/excalidraw/types";
|
||||||
|
import type { Bounds } from "@excalidraw/common";
|
||||||
|
|
||||||
import { getElementAbsoluteCoords } from "./bounds";
|
import { getElementAbsoluteCoords } from "./bounds";
|
||||||
import {
|
import {
|
||||||
@@ -23,7 +24,6 @@ import {
|
|||||||
} from "./transformHandles";
|
} from "./transformHandles";
|
||||||
import { isImageElement, isLinearElement } from "./typeChecks";
|
import { isImageElement, isLinearElement } from "./typeChecks";
|
||||||
|
|
||||||
import type { Bounds } from "./bounds";
|
|
||||||
import type {
|
import type {
|
||||||
TransformHandleType,
|
TransformHandleType,
|
||||||
TransformHandle,
|
TransformHandle,
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
getFontString,
|
getFontString,
|
||||||
isProdEnv,
|
isProdEnv,
|
||||||
invariant,
|
invariant,
|
||||||
|
DEFAULT_FONT_FAMILY,
|
||||||
|
getLineHeight,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import { pointFrom, pointRotateRads, type Radians } from "@excalidraw/math";
|
import { pointFrom, pointRotateRads, type Radians } from "@excalidraw/math";
|
||||||
@@ -30,6 +32,8 @@ import {
|
|||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
|
|
||||||
|
import type { NewTextElementOptions } from "./newElement";
|
||||||
|
|
||||||
import type { Scene } from "./Scene";
|
import type { Scene } from "./Scene";
|
||||||
|
|
||||||
import type { MaybeTransformHandleType } from "./transformHandles";
|
import type { MaybeTransformHandleType } from "./transformHandles";
|
||||||
@@ -40,6 +44,7 @@ import type {
|
|||||||
ExcalidrawTextContainer,
|
ExcalidrawTextContainer,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
|
FontFamilyValues,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@@ -108,7 +113,6 @@ export const redrawTextBoundingBox = (
|
|||||||
const nextHeight = computeContainerDimensionForBoundText(
|
const nextHeight = computeContainerDimensionForBoundText(
|
||||||
metrics.height,
|
metrics.height,
|
||||||
container.type,
|
container.type,
|
||||||
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
|
|
||||||
);
|
);
|
||||||
scene.mutateElement(container, { height: nextHeight });
|
scene.mutateElement(container, { height: nextHeight });
|
||||||
updateOriginalContainerCache(container.id, nextHeight);
|
updateOriginalContainerCache(container.id, nextHeight);
|
||||||
@@ -118,7 +122,6 @@ export const redrawTextBoundingBox = (
|
|||||||
const nextWidth = computeContainerDimensionForBoundText(
|
const nextWidth = computeContainerDimensionForBoundText(
|
||||||
metrics.width,
|
metrics.width,
|
||||||
container.type,
|
container.type,
|
||||||
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
|
|
||||||
);
|
);
|
||||||
scene.mutateElement(container, { width: nextWidth });
|
scene.mutateElement(container, { width: nextWidth });
|
||||||
}
|
}
|
||||||
@@ -189,7 +192,6 @@ export const handleBindTextResize = (
|
|||||||
containerHeight = computeContainerDimensionForBoundText(
|
containerHeight = computeContainerDimensionForBoundText(
|
||||||
nextHeight,
|
nextHeight,
|
||||||
container.type,
|
container.type,
|
||||||
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const diff = containerHeight - container.height;
|
const diff = containerHeight - container.height;
|
||||||
@@ -356,8 +358,8 @@ export const getContainerCenter = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getContainerCoords = (container: NonDeletedExcalidrawElement) => {
|
export const getContainerCoords = (container: NonDeletedExcalidrawElement) => {
|
||||||
let offsetX = container.containerBehavior?.margin ?? BOUND_TEXT_PADDING;
|
let offsetX = BOUND_TEXT_PADDING;
|
||||||
let offsetY = container.containerBehavior?.margin ?? BOUND_TEXT_PADDING;
|
let offsetY = BOUND_TEXT_PADDING;
|
||||||
|
|
||||||
if (container.type === "ellipse") {
|
if (container.type === "ellipse") {
|
||||||
// The derivation of coordinates is explained in https://github.com/excalidraw/excalidraw/pull/6172
|
// The derivation of coordinates is explained in https://github.com/excalidraw/excalidraw/pull/6172
|
||||||
@@ -449,10 +451,9 @@ export const isValidTextContainer = (element: {
|
|||||||
export const computeContainerDimensionForBoundText = (
|
export const computeContainerDimensionForBoundText = (
|
||||||
dimension: number,
|
dimension: number,
|
||||||
containerType: ExtractSetType<typeof VALID_CONTAINER_TYPES>,
|
containerType: ExtractSetType<typeof VALID_CONTAINER_TYPES>,
|
||||||
boundTextPadding: number,
|
|
||||||
) => {
|
) => {
|
||||||
dimension = Math.ceil(dimension);
|
dimension = Math.ceil(dimension);
|
||||||
const padding = boundTextPadding * 2;
|
const padding = BOUND_TEXT_PADDING * 2;
|
||||||
|
|
||||||
if (containerType === "ellipse") {
|
if (containerType === "ellipse") {
|
||||||
return Math.round(((dimension + padding) / Math.sqrt(2)) * 2);
|
return Math.round(((dimension + padding) / Math.sqrt(2)) * 2);
|
||||||
@@ -471,8 +472,6 @@ export const getBoundTextMaxWidth = (
|
|||||||
boundTextElement: ExcalidrawTextElement | null,
|
boundTextElement: ExcalidrawTextElement | null,
|
||||||
) => {
|
) => {
|
||||||
const { width } = container;
|
const { width } = container;
|
||||||
const boundTextPadding =
|
|
||||||
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING;
|
|
||||||
if (isArrowElement(container)) {
|
if (isArrowElement(container)) {
|
||||||
const minWidth =
|
const minWidth =
|
||||||
(boundTextElement?.fontSize ?? DEFAULT_FONT_SIZE) *
|
(boundTextElement?.fontSize ?? DEFAULT_FONT_SIZE) *
|
||||||
@@ -483,14 +482,14 @@ export const getBoundTextMaxWidth = (
|
|||||||
// The width of the largest rectangle inscribed inside an ellipse is
|
// The width of the largest rectangle inscribed inside an ellipse is
|
||||||
// Math.round((ellipse.width / 2) * Math.sqrt(2)) which is derived from
|
// Math.round((ellipse.width / 2) * Math.sqrt(2)) which is derived from
|
||||||
// equation of an ellipse -https://github.com/excalidraw/excalidraw/pull/6172
|
// equation of an ellipse -https://github.com/excalidraw/excalidraw/pull/6172
|
||||||
return Math.round((width / 2) * Math.sqrt(2)) - boundTextPadding * 2;
|
return Math.round((width / 2) * Math.sqrt(2)) - BOUND_TEXT_PADDING * 2;
|
||||||
}
|
}
|
||||||
if (container.type === "diamond") {
|
if (container.type === "diamond") {
|
||||||
// The width of the largest rectangle inscribed inside a rhombus is
|
// The width of the largest rectangle inscribed inside a rhombus is
|
||||||
// Math.round(width / 2) - https://github.com/excalidraw/excalidraw/pull/6265
|
// Math.round(width / 2) - https://github.com/excalidraw/excalidraw/pull/6265
|
||||||
return Math.round(width / 2) - boundTextPadding * 2;
|
return Math.round(width / 2) - BOUND_TEXT_PADDING * 2;
|
||||||
}
|
}
|
||||||
return width - boundTextPadding * 2;
|
return width - BOUND_TEXT_PADDING * 2;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBoundTextMaxHeight = (
|
export const getBoundTextMaxHeight = (
|
||||||
@@ -498,10 +497,8 @@ export const getBoundTextMaxHeight = (
|
|||||||
boundTextElement: ExcalidrawTextElementWithContainer,
|
boundTextElement: ExcalidrawTextElementWithContainer,
|
||||||
) => {
|
) => {
|
||||||
const { height } = container;
|
const { height } = container;
|
||||||
const boundTextPadding =
|
|
||||||
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING;
|
|
||||||
if (isArrowElement(container)) {
|
if (isArrowElement(container)) {
|
||||||
const containerHeight = height - boundTextPadding * 8 * 2;
|
const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2;
|
||||||
if (containerHeight <= 0) {
|
if (containerHeight <= 0) {
|
||||||
return boundTextElement.height;
|
return boundTextElement.height;
|
||||||
}
|
}
|
||||||
@@ -511,14 +508,14 @@ export const getBoundTextMaxHeight = (
|
|||||||
// The height of the largest rectangle inscribed inside an ellipse is
|
// The height of the largest rectangle inscribed inside an ellipse is
|
||||||
// Math.round((ellipse.height / 2) * Math.sqrt(2)) which is derived from
|
// Math.round((ellipse.height / 2) * Math.sqrt(2)) which is derived from
|
||||||
// equation of an ellipse - https://github.com/excalidraw/excalidraw/pull/6172
|
// equation of an ellipse - https://github.com/excalidraw/excalidraw/pull/6172
|
||||||
return Math.round((height / 2) * Math.sqrt(2)) - boundTextPadding * 2;
|
return Math.round((height / 2) * Math.sqrt(2)) - BOUND_TEXT_PADDING * 2;
|
||||||
}
|
}
|
||||||
if (container.type === "diamond") {
|
if (container.type === "diamond") {
|
||||||
// The height of the largest rectangle inscribed inside a rhombus is
|
// The height of the largest rectangle inscribed inside a rhombus is
|
||||||
// Math.round(height / 2) - https://github.com/excalidraw/excalidraw/pull/6265
|
// Math.round(height / 2) - https://github.com/excalidraw/excalidraw/pull/6265
|
||||||
return Math.round(height / 2) - boundTextPadding * 2;
|
return Math.round(height / 2) - BOUND_TEXT_PADDING * 2;
|
||||||
}
|
}
|
||||||
return height - boundTextPadding * 2;
|
return height - BOUND_TEXT_PADDING * 2;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** retrieves text from text elements and concatenates to a single string */
|
/** retrieves text from text elements and concatenates to a single string */
|
||||||
@@ -536,3 +533,24 @@ export const getTextFromElements = (
|
|||||||
.join(separator);
|
.join(separator);
|
||||||
return text;
|
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),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -32,24 +32,22 @@ const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
|
|||||||
export const getApproxMinLineWidth = (
|
export const getApproxMinLineWidth = (
|
||||||
font: FontString,
|
font: FontString,
|
||||||
lineHeight: ExcalidrawTextElement["lineHeight"],
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
boundTextPadding: number = BOUND_TEXT_PADDING,
|
|
||||||
) => {
|
) => {
|
||||||
const maxCharWidth = getMaxCharWidth(font);
|
const maxCharWidth = getMaxCharWidth(font);
|
||||||
if (maxCharWidth === 0) {
|
if (maxCharWidth === 0) {
|
||||||
return (
|
return (
|
||||||
measureText(DUMMY_TEXT.split("").join("\n"), font, lineHeight).width +
|
measureText(DUMMY_TEXT.split("").join("\n"), font, lineHeight).width +
|
||||||
boundTextPadding * 2
|
BOUND_TEXT_PADDING * 2
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return maxCharWidth + boundTextPadding * 2;
|
return maxCharWidth + BOUND_TEXT_PADDING * 2;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMinTextElementWidth = (
|
export const getMinTextElementWidth = (
|
||||||
font: FontString,
|
font: FontString,
|
||||||
lineHeight: ExcalidrawTextElement["lineHeight"],
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
boundTextPadding: number = BOUND_TEXT_PADDING,
|
|
||||||
) => {
|
) => {
|
||||||
return measureText("", font, lineHeight).width + boundTextPadding * 2;
|
return measureText("", font, lineHeight).width + BOUND_TEXT_PADDING * 2;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isMeasureTextSupported = () => {
|
export const isMeasureTextSupported = () => {
|
||||||
@@ -101,9 +99,8 @@ export const getLineHeightInPx = (
|
|||||||
export const getApproxMinLineHeight = (
|
export const getApproxMinLineHeight = (
|
||||||
fontSize: ExcalidrawTextElement["fontSize"],
|
fontSize: ExcalidrawTextElement["fontSize"],
|
||||||
lineHeight: ExcalidrawTextElement["lineHeight"],
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
boundTextPadding: number = BOUND_TEXT_PADDING,
|
|
||||||
) => {
|
) => {
|
||||||
return getLineHeightInPx(fontSize, lineHeight) + boundTextPadding * 2;
|
return getLineHeightInPx(fontSize, lineHeight) + BOUND_TEXT_PADDING * 2;
|
||||||
};
|
};
|
||||||
|
|
||||||
let textMetricsProvider: TextMetricsProvider | undefined;
|
let textMetricsProvider: TextMetricsProvider | undefined;
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ import {
|
|||||||
arrayToMap,
|
arrayToMap,
|
||||||
assertNever,
|
assertNever,
|
||||||
cloneJSON,
|
cloneJSON,
|
||||||
getFontString,
|
|
||||||
isDevEnv,
|
isDevEnv,
|
||||||
toBrandedType,
|
toBrandedType,
|
||||||
getLineHeight,
|
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import { bindBindingElement } from "@excalidraw/element";
|
import type { MarkOptional } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
|
import { bindBindingElement } from "./binding";
|
||||||
import {
|
import {
|
||||||
newArrowElement,
|
newArrowElement,
|
||||||
newElement,
|
newElement,
|
||||||
@@ -25,21 +25,19 @@ import {
|
|||||||
newLinearElement,
|
newLinearElement,
|
||||||
newMagicFrameElement,
|
newMagicFrameElement,
|
||||||
newTextElement,
|
newTextElement,
|
||||||
} from "@excalidraw/element";
|
type ElementConstructorOpts,
|
||||||
import { measureText, normalizeText } from "@excalidraw/element";
|
} from "./newElement";
|
||||||
import { isArrowElement } from "@excalidraw/element";
|
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 { Scene } from "./Scene";
|
||||||
|
|
||||||
import type { ElementConstructorOpts } from "@excalidraw/element";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
@@ -59,9 +57,7 @@ import type {
|
|||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
TextAlign,
|
TextAlign,
|
||||||
VerticalAlign,
|
VerticalAlign,
|
||||||
} from "@excalidraw/element/types";
|
} from "./types";
|
||||||
|
|
||||||
import type { MarkOptional } from "@excalidraw/common/utility-types";
|
|
||||||
|
|
||||||
export type ValidLinearElement = {
|
export type ValidLinearElement = {
|
||||||
type: "arrow" | "line";
|
type: "arrow" | "line";
|
||||||
@@ -580,14 +576,7 @@ export const convertToExcalidrawElements = (
|
|||||||
case "text": {
|
case "text": {
|
||||||
const fontFamily = element?.fontFamily || DEFAULT_FONT_FAMILY;
|
const fontFamily = element?.fontFamily || DEFAULT_FONT_FAMILY;
|
||||||
const fontSize = element?.fontSize || DEFAULT_FONT_SIZE;
|
const fontSize = element?.fontSize || DEFAULT_FONT_SIZE;
|
||||||
const lineHeight = element?.lineHeight || getLineHeight(fontFamily);
|
const metrics = getInitialTextMetrics(element, fontFamily, fontSize);
|
||||||
const text = element.text ?? "";
|
|
||||||
const normalizedText = normalizeText(text);
|
|
||||||
const metrics = measureText(
|
|
||||||
normalizedText,
|
|
||||||
getFontString({ fontFamily, fontSize }),
|
|
||||||
lineHeight,
|
|
||||||
);
|
|
||||||
|
|
||||||
excalidrawElement = newTextElement({
|
excalidrawElement = newTextElement({
|
||||||
width: metrics.width,
|
width: metrics.width,
|
||||||
@@ -11,6 +11,7 @@ import type {
|
|||||||
InteractiveCanvasAppState,
|
InteractiveCanvasAppState,
|
||||||
Zoom,
|
Zoom,
|
||||||
} from "@excalidraw/excalidraw/types";
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
import type { Bounds } from "@excalidraw/common";
|
||||||
|
|
||||||
import { getElementAbsoluteCoords } from "./bounds";
|
import { getElementAbsoluteCoords } from "./bounds";
|
||||||
import {
|
import {
|
||||||
@@ -20,7 +21,6 @@ import {
|
|||||||
isLinearElement,
|
isLinearElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
|
|
||||||
import type { Bounds } from "./bounds";
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import type { ElementOrToolType } from "@excalidraw/excalidraw/types";
|
|||||||
|
|
||||||
import type { MarkNonNullable } from "@excalidraw/common/utility-types";
|
import type { MarkNonNullable } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import type { Bounds } from "./bounds";
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
@@ -272,12 +271,15 @@ export const isExcalidrawElement = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isFlowchartType = (type: string): boolean =>
|
|
||||||
["rectangle", "ellipse", "diamond"].includes(type);
|
|
||||||
|
|
||||||
export const isFlowchartNodeElement = (
|
export const isFlowchartNodeElement = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
): element is ExcalidrawFlowchartNodeElement => isFlowchartType(element.type);
|
): element is ExcalidrawFlowchartNodeElement => {
|
||||||
|
return (
|
||||||
|
element.type === "rectangle" ||
|
||||||
|
element.type === "ellipse" ||
|
||||||
|
element.type === "diamond"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const hasBoundTextElement = (
|
export const hasBoundTextElement = (
|
||||||
element: ExcalidrawElement | null,
|
element: ExcalidrawElement | null,
|
||||||
@@ -353,15 +355,6 @@ export const getDefaultRoundnessTypeForElement = (
|
|||||||
return null;
|
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 = (
|
export const getLinearElementSubType = (
|
||||||
element: ExcalidrawLinearElement,
|
element: ExcalidrawLinearElement,
|
||||||
): ExcalidrawLinearElementSubType => {
|
): ExcalidrawLinearElementSubType => {
|
||||||
|
|||||||
@@ -27,10 +27,6 @@ export type StrokeRoundness = "round" | "sharp";
|
|||||||
export type RoundnessType = ValueOf<typeof ROUNDNESS>;
|
export type RoundnessType = ValueOf<typeof ROUNDNESS>;
|
||||||
export type StrokeStyle = "solid" | "dashed" | "dotted";
|
export type StrokeStyle = "solid" | "dashed" | "dotted";
|
||||||
export type TextAlign = typeof TEXT_ALIGN[keyof typeof TEXT_ALIGN];
|
export type TextAlign = typeof TEXT_ALIGN[keyof typeof TEXT_ALIGN];
|
||||||
export type ContainerBehavior = {
|
|
||||||
textFlow: "growing" | "fixed";
|
|
||||||
margin?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type VerticalAlignKeys = keyof typeof VERTICAL_ALIGN;
|
type VerticalAlignKeys = keyof typeof VERTICAL_ALIGN;
|
||||||
export type VerticalAlign = typeof VERTICAL_ALIGN[VerticalAlignKeys];
|
export type VerticalAlign = typeof VERTICAL_ALIGN[VerticalAlignKeys];
|
||||||
@@ -83,26 +79,21 @@ type _ExcalidrawElementBase = Readonly<{
|
|||||||
link: string | null;
|
link: string | null;
|
||||||
locked: boolean;
|
locked: boolean;
|
||||||
customData?: Record<string, any>;
|
customData?: Record<string, any>;
|
||||||
containerBehavior?: ContainerBehavior;
|
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type ExcalidrawSelectionElement = _ExcalidrawElementBase & {
|
export type ExcalidrawSelectionElement = _ExcalidrawElementBase & {
|
||||||
type: "selection";
|
type: "selection";
|
||||||
};
|
};
|
||||||
|
|
||||||
type _ExcalidrawStickyNoteContainer = _ExcalidrawElementBase & {
|
export type ExcalidrawRectangleElement = _ExcalidrawElementBase & {
|
||||||
containerBehavior: ContainerBehavior;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ExcalidrawRectangleElement = _ExcalidrawStickyNoteContainer & {
|
|
||||||
type: "rectangle";
|
type: "rectangle";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ExcalidrawDiamondElement = _ExcalidrawStickyNoteContainer & {
|
export type ExcalidrawDiamondElement = _ExcalidrawElementBase & {
|
||||||
type: "diamond";
|
type: "diamond";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ExcalidrawEllipseElement = _ExcalidrawStickyNoteContainer & {
|
export type ExcalidrawEllipseElement = _ExcalidrawElementBase & {
|
||||||
type: "ellipse";
|
type: "ellipse";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -493,6 +493,7 @@ describe("binding for simple arrows", () => {
|
|||||||
expect(arrow.endBinding?.elementId).toBe(rectRight.id);
|
expect(arrow.endBinding?.elementId).toBe(rectRight.id);
|
||||||
|
|
||||||
mouse.downAt(-100, -100);
|
mouse.downAt(-100, -100);
|
||||||
|
mouse.moveTo(0, 0);
|
||||||
mouse.moveTo(650, 750);
|
mouse.moveTo(650, 750);
|
||||||
mouse.up(0, 0);
|
mouse.up(0, 0);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { pointFrom } from "@excalidraw/math";
|
|||||||
|
|
||||||
import { Excalidraw } from "@excalidraw/excalidraw";
|
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||||
import {
|
import {
|
||||||
|
type Bounds,
|
||||||
KEYS,
|
KEYS,
|
||||||
getSizeFromPoints,
|
getSizeFromPoints,
|
||||||
reseed,
|
reseed,
|
||||||
@@ -22,7 +23,6 @@ import { resizeSingleElement } from "../src/resizeElements";
|
|||||||
import { LinearElementEditor } from "../src/linearElementEditor";
|
import { LinearElementEditor } from "../src/linearElementEditor";
|
||||||
import { getElementPointsCoords } from "../src/bounds";
|
import { getElementPointsCoords } from "../src/bounds";
|
||||||
|
|
||||||
import type { Bounds } from "../src/bounds";
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
ExcalidrawFreeDrawElement,
|
ExcalidrawFreeDrawElement,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BOUND_TEXT_PADDING, getLineHeight } from "@excalidraw/common";
|
import { getLineHeight } from "@excalidraw/common";
|
||||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
|
|
||||||
import { FONT_FAMILY, TEXT_ALIGN, VERTICAL_ALIGN } from "@excalidraw/common";
|
import { FONT_FAMILY, TEXT_ALIGN, VERTICAL_ALIGN } from "@excalidraw/common";
|
||||||
@@ -63,13 +63,9 @@ describe("Test measureText", () => {
|
|||||||
type: "rectangle",
|
type: "rectangle",
|
||||||
...params,
|
...params,
|
||||||
});
|
});
|
||||||
expect(
|
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
|
||||||
computeContainerDimensionForBoundText(
|
160,
|
||||||
150,
|
);
|
||||||
element.type,
|
|
||||||
BOUND_TEXT_PADDING,
|
|
||||||
),
|
|
||||||
).toEqual(160);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should compute container height correctly for ellipse", () => {
|
it("should compute container height correctly for ellipse", () => {
|
||||||
@@ -77,13 +73,9 @@ describe("Test measureText", () => {
|
|||||||
type: "ellipse",
|
type: "ellipse",
|
||||||
...params,
|
...params,
|
||||||
});
|
});
|
||||||
expect(
|
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
|
||||||
computeContainerDimensionForBoundText(
|
226,
|
||||||
150,
|
);
|
||||||
element.type,
|
|
||||||
BOUND_TEXT_PADDING,
|
|
||||||
),
|
|
||||||
).toEqual(226);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should compute container height correctly for diamond", () => {
|
it("should compute container height correctly for diamond", () => {
|
||||||
@@ -91,13 +83,9 @@ describe("Test measureText", () => {
|
|||||||
type: "diamond",
|
type: "diamond",
|
||||||
...params,
|
...params,
|
||||||
});
|
});
|
||||||
expect(
|
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
|
||||||
computeContainerDimensionForBoundText(
|
320,
|
||||||
150,
|
);
|
||||||
element.type,
|
|
||||||
BOUND_TEXT_PADDING,
|
|
||||||
),
|
|
||||||
).toEqual(320);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -236,9 +236,6 @@ export const actionWrapTextInContainer = register({
|
|||||||
let updatedElements: readonly ExcalidrawElement[] = elements.slice();
|
let updatedElements: readonly ExcalidrawElement[] = elements.slice();
|
||||||
const containerIds: Mutable<AppState["selectedElementIds"]> = {};
|
const containerIds: Mutable<AppState["selectedElementIds"]> = {};
|
||||||
|
|
||||||
const boundTextPadding =
|
|
||||||
appState.currentItemContainerBehavior?.margin ?? BOUND_TEXT_PADDING;
|
|
||||||
|
|
||||||
for (const textElement of selectedElements) {
|
for (const textElement of selectedElements) {
|
||||||
if (isTextElement(textElement) && !isBoundToContainer(textElement)) {
|
if (isTextElement(textElement) && !isBoundToContainer(textElement)) {
|
||||||
const container = newElement({
|
const container = newElement({
|
||||||
@@ -264,21 +261,18 @@ export const actionWrapTextInContainer = register({
|
|||||||
: null,
|
: null,
|
||||||
opacity: 100,
|
opacity: 100,
|
||||||
locked: false,
|
locked: false,
|
||||||
x: textElement.x - boundTextPadding,
|
x: textElement.x - BOUND_TEXT_PADDING,
|
||||||
y: textElement.y - boundTextPadding,
|
y: textElement.y - BOUND_TEXT_PADDING,
|
||||||
width: computeContainerDimensionForBoundText(
|
width: computeContainerDimensionForBoundText(
|
||||||
textElement.width,
|
textElement.width,
|
||||||
"rectangle",
|
"rectangle",
|
||||||
boundTextPadding,
|
|
||||||
),
|
),
|
||||||
height: computeContainerDimensionForBoundText(
|
height: computeContainerDimensionForBoundText(
|
||||||
textElement.height,
|
textElement.height,
|
||||||
"rectangle",
|
"rectangle",
|
||||||
boundTextPadding,
|
|
||||||
),
|
),
|
||||||
groupIds: textElement.groupIds,
|
groupIds: textElement.groupIds,
|
||||||
frameId: textElement.frameId,
|
frameId: textElement.frameId,
|
||||||
containerBehavior: appState.currentItemContainerBehavior,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// update bindings
|
// update bindings
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
KEYS,
|
KEYS,
|
||||||
arrayToMap,
|
arrayToMap,
|
||||||
invariant,
|
invariant,
|
||||||
|
shouldRotateWithDiscreteAngle,
|
||||||
updateActiveTool,
|
updateActiveTool,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
import { isPathALoop } from "@excalidraw/element";
|
import { isPathALoop } from "@excalidraw/element";
|
||||||
@@ -102,10 +103,19 @@ export const actionFinalize = register<FormData>({
|
|||||||
return map;
|
return map;
|
||||||
}, new Map()) ?? new Map();
|
}, new Map()) ?? new Map();
|
||||||
|
|
||||||
bindOrUnbindBindingElement(element, draggedPoints, scene, appState, {
|
bindOrUnbindBindingElement(
|
||||||
newArrow,
|
element,
|
||||||
altKey: event.altKey,
|
draggedPoints,
|
||||||
});
|
sceneCoords.x,
|
||||||
|
sceneCoords.y,
|
||||||
|
scene,
|
||||||
|
appState,
|
||||||
|
{
|
||||||
|
newArrow,
|
||||||
|
altKey: event.altKey,
|
||||||
|
angleLocked: shouldRotateWithDiscreteAngle(event),
|
||||||
|
},
|
||||||
|
);
|
||||||
} else if (isLineElement(element)) {
|
} else if (isLineElement(element)) {
|
||||||
if (
|
if (
|
||||||
appState.selectedLinearElement?.isEditing &&
|
appState.selectedLinearElement?.isEditing &&
|
||||||
|
|||||||
@@ -21,16 +21,11 @@ import {
|
|||||||
getLineHeight,
|
getLineHeight,
|
||||||
isTransparent,
|
isTransparent,
|
||||||
reduceToCommonValue,
|
reduceToCommonValue,
|
||||||
BOUND_TEXT_PADDING,
|
|
||||||
invariant,
|
invariant,
|
||||||
|
FONT_SIZES,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import { canBecomePolygon, getNonDeletedElements } from "@excalidraw/element";
|
||||||
canBecomePolygon,
|
|
||||||
getNonDeletedElements,
|
|
||||||
hasContainerBehavior,
|
|
||||||
isFlowchartNodeElement,
|
|
||||||
} from "@excalidraw/element";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
bindBindingElement,
|
bindBindingElement,
|
||||||
@@ -71,7 +66,6 @@ import type { LocalPoint, Radians } from "@excalidraw/math";
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
Arrowhead,
|
Arrowhead,
|
||||||
ContainerBehavior,
|
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
@@ -133,11 +127,6 @@ import {
|
|||||||
ArrowheadCrowfootIcon,
|
ArrowheadCrowfootIcon,
|
||||||
ArrowheadCrowfootOneIcon,
|
ArrowheadCrowfootOneIcon,
|
||||||
ArrowheadCrowfootOneOrManyIcon,
|
ArrowheadCrowfootOneOrManyIcon,
|
||||||
stickyNoteIcon,
|
|
||||||
growingContainerIcon,
|
|
||||||
marginLargeIcon,
|
|
||||||
marginMediumIcon,
|
|
||||||
marginSmallIcon,
|
|
||||||
} from "../components/icons";
|
} from "../components/icons";
|
||||||
|
|
||||||
import { Fonts } from "../fonts";
|
import { Fonts } from "../fonts";
|
||||||
@@ -770,25 +759,25 @@ export const actionChangeFontSize = register<ExcalidrawTextElement["fontSize"]>(
|
|||||||
group="font-size"
|
group="font-size"
|
||||||
options={[
|
options={[
|
||||||
{
|
{
|
||||||
value: 16,
|
value: FONT_SIZES.sm,
|
||||||
text: t("labels.small"),
|
text: t("labels.small"),
|
||||||
icon: FontSizeSmallIcon,
|
icon: FontSizeSmallIcon,
|
||||||
testId: "fontSize-small",
|
testId: "fontSize-small",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 20,
|
value: FONT_SIZES.md,
|
||||||
text: t("labels.medium"),
|
text: t("labels.medium"),
|
||||||
icon: FontSizeMediumIcon,
|
icon: FontSizeMediumIcon,
|
||||||
testId: "fontSize-medium",
|
testId: "fontSize-medium",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 28,
|
value: FONT_SIZES.lg,
|
||||||
text: t("labels.large"),
|
text: t("labels.large"),
|
||||||
icon: FontSizeLargeIcon,
|
icon: FontSizeLargeIcon,
|
||||||
testId: "fontSize-large",
|
testId: "fontSize-large",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 36,
|
value: FONT_SIZES.xl,
|
||||||
text: t("labels.veryLarge"),
|
text: t("labels.veryLarge"),
|
||||||
icon: FontSizeExtraLargeIcon,
|
icon: FontSizeExtraLargeIcon,
|
||||||
testId: "fontSize-veryLarge",
|
testId: "fontSize-veryLarge",
|
||||||
@@ -1560,275 +1549,6 @@ export const actionChangeRoundness = register<"sharp" | "round">({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const getMargin = (value: "small" | "medium" | "large") => {
|
|
||||||
switch (value) {
|
|
||||||
case "small":
|
|
||||||
return BOUND_TEXT_PADDING;
|
|
||||||
case "medium":
|
|
||||||
return 15;
|
|
||||||
case "large":
|
|
||||||
return 25;
|
|
||||||
default:
|
|
||||||
return BOUND_TEXT_PADDING;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMarginValue = (margin: number | null) => {
|
|
||||||
if (margin === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
switch (margin) {
|
|
||||||
case BOUND_TEXT_PADDING:
|
|
||||||
return "small";
|
|
||||||
case 15:
|
|
||||||
return "medium";
|
|
||||||
case 25:
|
|
||||||
return "large";
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const actionChangeContainerBehavior = register<
|
|
||||||
| { textFlow: ContainerBehavior["textFlow"] }
|
|
||||||
| { margin: NonNullable<ReturnType<typeof getMarginValue>> }
|
|
||||||
>({
|
|
||||||
name: "changeContainerBehavior",
|
|
||||||
label: "labels.container",
|
|
||||||
trackEvent: false,
|
|
||||||
perform: (elements, appState, value, app) => {
|
|
||||||
invariant(value, "actionChangeContainerBehavior: value must be defined");
|
|
||||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
|
||||||
let selected = getSelectedElements(elements, appState, {
|
|
||||||
includeBoundTextElement: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (selected.length === 0 && appState.editingTextElement) {
|
|
||||||
selected = [appState.editingTextElement];
|
|
||||||
}
|
|
||||||
|
|
||||||
const containerIdsToUpdate = new Set<string>();
|
|
||||||
|
|
||||||
// collect directly selected eligible containers
|
|
||||||
for (const el of selected) {
|
|
||||||
if (isFlowchartNodeElement(el)) {
|
|
||||||
containerIdsToUpdate.add(el.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if none, and exactly one selected text element -> use its container if eligible
|
|
||||||
if (
|
|
||||||
containerIdsToUpdate.size === 0 &&
|
|
||||||
selected.length === 1 &&
|
|
||||||
isTextElement(selected[0]) &&
|
|
||||||
selected[0].containerId
|
|
||||||
) {
|
|
||||||
const container = elementsMap.get(selected[0].containerId);
|
|
||||||
if (
|
|
||||||
container &&
|
|
||||||
isFlowchartNodeElement(container) &&
|
|
||||||
getBoundTextElement(container, elementsMap)
|
|
||||||
) {
|
|
||||||
containerIdsToUpdate.add(container.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("margin" in value) {
|
|
||||||
const marginSize = getMargin(value.margin);
|
|
||||||
if (containerIdsToUpdate.size === 0) {
|
|
||||||
return {
|
|
||||||
appState: {
|
|
||||||
...appState,
|
|
||||||
currentItemContainerBehavior: {
|
|
||||||
textFlow:
|
|
||||||
appState.currentItemContainerBehavior?.textFlow ?? "growing",
|
|
||||||
margin: marginSize,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const nextElements = elements.map((el) =>
|
|
||||||
containerIdsToUpdate.has(el.id)
|
|
||||||
? newElementWith(el, {
|
|
||||||
containerBehavior: {
|
|
||||||
textFlow: el.containerBehavior?.textFlow ?? "growing",
|
|
||||||
margin: marginSize,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
: el,
|
|
||||||
);
|
|
||||||
// Invalidate containers to trigger re-render
|
|
||||||
containerIdsToUpdate.forEach((id) => {
|
|
||||||
const container = nextElements.find((el) => el.id === id);
|
|
||||||
if (container) {
|
|
||||||
const boundText = getBoundTextElement(
|
|
||||||
container,
|
|
||||||
arrayToMap(nextElements),
|
|
||||||
);
|
|
||||||
if (boundText) {
|
|
||||||
redrawTextBoundingBox(boundText, container, app.scene);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
elements: nextElements,
|
|
||||||
appState: {
|
|
||||||
...appState,
|
|
||||||
currentItemContainerBehavior: {
|
|
||||||
textFlow:
|
|
||||||
appState.currentItemContainerBehavior?.textFlow ?? "growing",
|
|
||||||
margin: marginSize,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const textFlow = value.textFlow;
|
|
||||||
const nextElements = elements.map((el) =>
|
|
||||||
containerIdsToUpdate.has(el.id)
|
|
||||||
? newElementWith(el, {
|
|
||||||
containerBehavior: {
|
|
||||||
textFlow,
|
|
||||||
margin: el.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
: el,
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
elements: nextElements,
|
|
||||||
appState: {
|
|
||||||
...appState,
|
|
||||||
currentItemContainerBehavior: {
|
|
||||||
textFlow,
|
|
||||||
margin:
|
|
||||||
appState.currentItemContainerBehavior?.margin ?? BOUND_TEXT_PADDING,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
PanelComponent: ({ elements, appState, updateData, app }) => {
|
|
||||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
|
||||||
let selected = getSelectedElements(elements, appState, {
|
|
||||||
includeBoundTextElement: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (selected.length === 0 && appState.editingTextElement) {
|
|
||||||
selected = [appState.editingTextElement];
|
|
||||||
}
|
|
||||||
|
|
||||||
let targetContainers: ExcalidrawElement[] = [];
|
|
||||||
|
|
||||||
// case 1: one text element selected -> target its container if eligible
|
|
||||||
if (
|
|
||||||
selected.length === 1 &&
|
|
||||||
isTextElement(selected[0]) &&
|
|
||||||
selected[0].containerId
|
|
||||||
) {
|
|
||||||
const container = elementsMap.get(selected[0].containerId);
|
|
||||||
if (
|
|
||||||
container &&
|
|
||||||
isFlowchartNodeElement(container) &&
|
|
||||||
getBoundTextElement(container, elementsMap)
|
|
||||||
) {
|
|
||||||
targetContainers = [container];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// case 2: any eligible containers directly selected
|
|
||||||
targetContainers = selected.filter((el) => isFlowchartNodeElement(el));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
targetContainers.length === 0 &&
|
|
||||||
!hasContainerBehavior(appState.activeTool.type)
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const textFlow =
|
|
||||||
targetContainers.length === 0
|
|
||||||
? appState.currentItemContainerBehavior?.textFlow ?? "growing"
|
|
||||||
: reduceToCommonValue(
|
|
||||||
targetContainers,
|
|
||||||
(el) => el.containerBehavior?.textFlow ?? "growing",
|
|
||||||
) ??
|
|
||||||
// mixed selection -> show null so nothing appears selected
|
|
||||||
null;
|
|
||||||
|
|
||||||
const marginValue =
|
|
||||||
targetContainers.length === 0
|
|
||||||
? appState.currentItemContainerBehavior?.margin ?? BOUND_TEXT_PADDING
|
|
||||||
: reduceToCommonValue(
|
|
||||||
targetContainers,
|
|
||||||
(el) => el.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
|
|
||||||
) ??
|
|
||||||
// mixed selection -> show null so nothing appears selected
|
|
||||||
null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<fieldset>
|
|
||||||
<legend>{t("labels.container")}</legend>
|
|
||||||
<div className="buttonList">
|
|
||||||
<RadioSelection
|
|
||||||
group="container"
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
value: "growing",
|
|
||||||
text: t("labels.container_growing"),
|
|
||||||
icon: growingContainerIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "fixed",
|
|
||||||
text: t("labels.container_fixed"),
|
|
||||||
icon: stickyNoteIcon,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
value={
|
|
||||||
textFlow ??
|
|
||||||
(targetContainers.length
|
|
||||||
? null
|
|
||||||
: appState.currentItemContainerBehavior?.textFlow ?? "growing")
|
|
||||||
}
|
|
||||||
onChange={(val) => updateData({ textFlow: val })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="buttonList">
|
|
||||||
<RadioSelection
|
|
||||||
group="container"
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
value: "small",
|
|
||||||
text: t("labels.container_margin_small"),
|
|
||||||
icon: marginSmallIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "medium",
|
|
||||||
text: t("labels.container_margin_medium"),
|
|
||||||
icon: marginMediumIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "large",
|
|
||||||
text: t("labels.container_margin_large"),
|
|
||||||
icon: marginLargeIcon,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
value={getMarginValue(
|
|
||||||
marginValue ??
|
|
||||||
(targetContainers.length
|
|
||||||
? null
|
|
||||||
: appState.currentItemContainerBehavior?.margin ??
|
|
||||||
BOUND_TEXT_PADDING),
|
|
||||||
)}
|
|
||||||
onChange={(val) => updateData({ margin: val })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const getArrowheadOptions = (flip: boolean) => {
|
const getArrowheadOptions = (flip: boolean) => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ export {
|
|||||||
actionChangeTextAlign,
|
actionChangeTextAlign,
|
||||||
actionChangeVerticalAlign,
|
actionChangeVerticalAlign,
|
||||||
actionChangeArrowProperties,
|
actionChangeArrowProperties,
|
||||||
actionChangeContainerBehavior,
|
|
||||||
} from "./actionProperties";
|
} from "./actionProperties";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ export type ActionName =
|
|||||||
| "changeStrokeShape"
|
| "changeStrokeShape"
|
||||||
| "changeSloppiness"
|
| "changeSloppiness"
|
||||||
| "changeStrokeStyle"
|
| "changeStrokeStyle"
|
||||||
| "changeContainerBehavior"
|
|
||||||
| "changeArrowhead"
|
| "changeArrowhead"
|
||||||
| "changeArrowType"
|
| "changeArrowType"
|
||||||
| "changeArrowProperties"
|
| "changeArrowProperties"
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ export const getDefaultAppState = (): Omit<
|
|||||||
currentItemStrokeStyle: DEFAULT_ELEMENT_PROPS.strokeStyle,
|
currentItemStrokeStyle: DEFAULT_ELEMENT_PROPS.strokeStyle,
|
||||||
currentItemStrokeWidth: DEFAULT_ELEMENT_PROPS.strokeWidth,
|
currentItemStrokeWidth: DEFAULT_ELEMENT_PROPS.strokeWidth,
|
||||||
currentItemTextAlign: DEFAULT_TEXT_ALIGN,
|
currentItemTextAlign: DEFAULT_TEXT_ALIGN,
|
||||||
currentItemContainerBehavior: DEFAULT_ELEMENT_PROPS.containerBehavior,
|
|
||||||
currentHoveredFontFamily: null,
|
currentHoveredFontFamily: null,
|
||||||
cursorButton: "up",
|
cursorButton: "up",
|
||||||
activeEmbeddable: null,
|
activeEmbeddable: null,
|
||||||
@@ -174,7 +173,6 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
currentItemStrokeStyle: { browser: true, export: false, server: false },
|
currentItemStrokeStyle: { browser: true, export: false, server: false },
|
||||||
currentItemStrokeWidth: { browser: true, export: false, server: false },
|
currentItemStrokeWidth: { browser: true, export: false, server: false },
|
||||||
currentItemTextAlign: { browser: true, export: false, server: false },
|
currentItemTextAlign: { browser: true, export: false, server: false },
|
||||||
currentItemContainerBehavior: { browser: true, export: false, server: false },
|
|
||||||
currentHoveredFontFamily: { browser: false, export: false, server: false },
|
currentHoveredFontFamily: { browser: false, export: false, server: false },
|
||||||
cursorButton: { browser: true, export: false, server: false },
|
cursorButton: { browser: true, export: false, server: false },
|
||||||
activeEmbeddable: { browser: false, export: false, server: false },
|
activeEmbeddable: { browser: false, export: false, server: false },
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
randomId,
|
randomId,
|
||||||
isDevEnv,
|
isDevEnv,
|
||||||
BOUND_TEXT_PADDING,
|
FONT_SIZES,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -214,7 +214,7 @@ const chartXLabels = (
|
|||||||
y: y + BAR_GAP / 2,
|
y: y + BAR_GAP / 2,
|
||||||
width: BAR_WIDTH,
|
width: BAR_WIDTH,
|
||||||
angle: 5.87 as Radians,
|
angle: 5.87 as Radians,
|
||||||
fontSize: 16,
|
fontSize: FONT_SIZES.sm,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
verticalAlign: "top",
|
verticalAlign: "top",
|
||||||
});
|
});
|
||||||
@@ -335,10 +335,6 @@ const chartBaseElements = (
|
|||||||
strokeColor: COLOR_PALETTE.black,
|
strokeColor: COLOR_PALETTE.black,
|
||||||
fillStyle: "solid",
|
fillStyle: "solid",
|
||||||
opacity: 6,
|
opacity: 6,
|
||||||
containerBehavior: {
|
|
||||||
textFlow: "growing",
|
|
||||||
margin: BOUND_TEXT_PADDING,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -371,10 +367,6 @@ const chartTypeBar = (
|
|||||||
y: y - barHeight - BAR_GAP,
|
y: y - barHeight - BAR_GAP,
|
||||||
width: BAR_WIDTH,
|
width: BAR_WIDTH,
|
||||||
height: barHeight,
|
height: barHeight,
|
||||||
containerBehavior: {
|
|
||||||
textFlow: "growing",
|
|
||||||
margin: BOUND_TEXT_PADDING,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -441,10 +433,6 @@ const chartTypeLine = (
|
|||||||
y: y + cy - BAR_GAP * 2,
|
y: y + cy - BAR_GAP * 2,
|
||||||
width: BAR_GAP,
|
width: BAR_GAP,
|
||||||
height: BAR_GAP,
|
height: BAR_GAP,
|
||||||
containerBehavior: {
|
|
||||||
textFlow: "growing",
|
|
||||||
margin: BOUND_TEXT_PADDING,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -113,7 +113,6 @@ export const createPasteEvent = ({
|
|||||||
if (typeof value !== "string") {
|
if (typeof value !== "string") {
|
||||||
files = files || [];
|
files = files || [];
|
||||||
files.push(value);
|
files.push(value);
|
||||||
event.clipboardData?.items.add(value);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ import {
|
|||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getContainerElement,
|
|
||||||
hasContainerBehavior,
|
|
||||||
isFlowchartNodeElement,
|
|
||||||
shouldAllowVerticalAlign,
|
shouldAllowVerticalAlign,
|
||||||
suppportsHorizontalAlign,
|
suppportsHorizontalAlign,
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
@@ -158,15 +155,6 @@ export const SelectedShapeActions = ({
|
|||||||
) {
|
) {
|
||||||
isSingleElementBoundContainer = true;
|
isSingleElementBoundContainer = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const textContainer =
|
|
||||||
targetElements.length === 1 && isTextElement(targetElements[0])
|
|
||||||
? getContainerElement(targetElements[0], elementsMap)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const isStickyNoteContainer =
|
|
||||||
textContainer && isFlowchartNodeElement(textContainer);
|
|
||||||
|
|
||||||
const isEditingTextOrNewElement = Boolean(
|
const isEditingTextOrNewElement = Boolean(
|
||||||
appState.editingTextElement || appState.newElement,
|
appState.editingTextElement || appState.newElement,
|
||||||
);
|
);
|
||||||
@@ -230,11 +218,6 @@ export const SelectedShapeActions = ({
|
|||||||
<>{renderAction("changeRoundness")}</>
|
<>{renderAction("changeRoundness")}</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(hasContainerBehavior(appState.activeTool.type) ||
|
|
||||||
targetElements.some((element) => isFlowchartNodeElement(element))) && (
|
|
||||||
<>{renderAction("changeContainerBehavior")}</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(toolIsArrow(appState.activeTool.type) ||
|
{(toolIsArrow(appState.activeTool.type) ||
|
||||||
targetElements.some((element) => toolIsArrow(element.type))) && (
|
targetElements.some((element) => toolIsArrow(element.type))) && (
|
||||||
<>{renderAction("changeArrowType")}</>
|
<>{renderAction("changeArrowType")}</>
|
||||||
@@ -258,8 +241,6 @@ export const SelectedShapeActions = ({
|
|||||||
<>{renderAction("changeArrowhead")}</>
|
<>{renderAction("changeArrowhead")}</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isStickyNoteContainer && <>{renderAction("changeContainerBehavior")}</>}
|
|
||||||
|
|
||||||
{renderAction("changeOpacity")}
|
{renderAction("changeOpacity")}
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
@@ -427,10 +408,6 @@ const CombinedShapeProperties = ({
|
|||||||
canChangeRoundness(element.type),
|
canChangeRoundness(element.type),
|
||||||
)) &&
|
)) &&
|
||||||
renderAction("changeRoundness")}
|
renderAction("changeRoundness")}
|
||||||
{(hasContainerBehavior(appState.activeTool.type) ||
|
|
||||||
targetElements.some((element) =>
|
|
||||||
isFlowchartNodeElement(element),
|
|
||||||
)) && <>{renderAction("changeContainerBehavior")}</>}
|
|
||||||
{renderAction("changeOpacity")}
|
{renderAction("changeOpacity")}
|
||||||
</div>
|
</div>
|
||||||
</PropertiesPopover>
|
</PropertiesPopover>
|
||||||
@@ -555,14 +532,6 @@ const CombinedTextProperties = ({
|
|||||||
const { saveCaretPosition, restoreCaretPosition } = useTextEditorFocus();
|
const { saveCaretPosition, restoreCaretPosition } = useTextEditorFocus();
|
||||||
const isOpen = appState.openPopup === "compactTextProperties";
|
const isOpen = appState.openPopup === "compactTextProperties";
|
||||||
|
|
||||||
const textContainer =
|
|
||||||
targetElements.length === 1 && isTextElement(targetElements[0])
|
|
||||||
? getContainerElement(targetElements[0], elementsMap)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const isStickyNoteContainer =
|
|
||||||
textContainer && isFlowchartNodeElement(textContainer);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="compact-action-item">
|
<div className="compact-action-item">
|
||||||
<Popover.Root
|
<Popover.Root
|
||||||
@@ -628,9 +597,6 @@ const CombinedTextProperties = ({
|
|||||||
renderAction("changeTextAlign")}
|
renderAction("changeTextAlign")}
|
||||||
{shouldAllowVerticalAlign(targetElements, elementsMap) &&
|
{shouldAllowVerticalAlign(targetElements, elementsMap) &&
|
||||||
renderAction("changeVerticalAlign")}
|
renderAction("changeVerticalAlign")}
|
||||||
{isStickyNoteContainer && (
|
|
||||||
<>{renderAction("changeContainerBehavior")}</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</PropertiesPopover>
|
</PropertiesPopover>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -96,7 +96,6 @@ import {
|
|||||||
MINIMUM_ARROW_SIZE,
|
MINIMUM_ARROW_SIZE,
|
||||||
DOUBLE_TAP_POSITION_THRESHOLD,
|
DOUBLE_TAP_POSITION_THRESHOLD,
|
||||||
BIND_MODE_TIMEOUT,
|
BIND_MODE_TIMEOUT,
|
||||||
BOUND_TEXT_PADDING,
|
|
||||||
invariant,
|
invariant,
|
||||||
getFeatureFlag,
|
getFeatureFlag,
|
||||||
createUserAgentDescriptor,
|
createUserAgentDescriptor,
|
||||||
@@ -247,9 +246,10 @@ import {
|
|||||||
mutateElement,
|
mutateElement,
|
||||||
getElementBounds,
|
getElementBounds,
|
||||||
doBoundsIntersect,
|
doBoundsIntersect,
|
||||||
isFlowchartType,
|
|
||||||
isPointInElement,
|
isPointInElement,
|
||||||
maxBindingDistance_simple,
|
maxBindingDistance_simple,
|
||||||
|
convertToExcalidrawElements,
|
||||||
|
type ExcalidrawElementSkeleton,
|
||||||
} from "@excalidraw/element";
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
|
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
|
||||||
@@ -397,7 +397,6 @@ import {
|
|||||||
SnapCache,
|
SnapCache,
|
||||||
isGridModeEnabled,
|
isGridModeEnabled,
|
||||||
} from "../snapping";
|
} from "../snapping";
|
||||||
import { convertToExcalidrawElements } from "../data/transform";
|
|
||||||
import { Renderer } from "../scene/Renderer";
|
import { Renderer } from "../scene/Renderer";
|
||||||
import {
|
import {
|
||||||
setEraserCursor,
|
setEraserCursor,
|
||||||
@@ -459,7 +458,7 @@ import type { ClipboardData, PastedMixedContent } from "../clipboard";
|
|||||||
import type { ExportedElements } from "../data";
|
import type { ExportedElements } from "../data";
|
||||||
import type { ContextMenuItems } from "./ContextMenu";
|
import type { ContextMenuItems } from "./ContextMenu";
|
||||||
import type { FileSystemHandle } from "../data/filesystem";
|
import type { FileSystemHandle } from "../data/filesystem";
|
||||||
import type { ExcalidrawElementSkeleton } from "../data/transform";
|
|
||||||
import type {
|
import type {
|
||||||
AppClassProperties,
|
AppClassProperties,
|
||||||
AppProps,
|
AppProps,
|
||||||
@@ -5778,13 +5777,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const minWidth = getApproxMinLineWidth(
|
const minWidth = getApproxMinLineWidth(
|
||||||
getFontString(fontString),
|
getFontString(fontString),
|
||||||
lineHeight,
|
lineHeight,
|
||||||
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
|
|
||||||
);
|
|
||||||
const minHeight = getApproxMinLineHeight(
|
|
||||||
fontSize,
|
|
||||||
lineHeight,
|
|
||||||
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
|
|
||||||
);
|
);
|
||||||
|
const minHeight = getApproxMinLineHeight(fontSize, lineHeight);
|
||||||
const newHeight = Math.max(container.height, minHeight);
|
const newHeight = Math.max(container.height, minHeight);
|
||||||
const newWidth = Math.max(container.width, minWidth);
|
const newWidth = Math.max(container.width, minWidth);
|
||||||
this.scene.mutateElement(container, {
|
this.scene.mutateElement(container, {
|
||||||
@@ -8624,9 +8618,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
]),
|
]),
|
||||||
|
point[0],
|
||||||
|
point[1],
|
||||||
this.scene,
|
this.scene,
|
||||||
this.state,
|
this.state,
|
||||||
{ newArrow: true, altKey: event.altKey, initialBinding: true },
|
{
|
||||||
|
newArrow: true,
|
||||||
|
altKey: event.altKey,
|
||||||
|
initialBinding: true,
|
||||||
|
angleLocked: shouldRotateWithDiscreteAngle(event.nativeEvent),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8728,9 +8729,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
roundness: this.getCurrentItemRoundness(elementType),
|
roundness: this.getCurrentItemRoundness(elementType),
|
||||||
locked: false,
|
locked: false,
|
||||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||||
...(isFlowchartType(elementType) && {
|
|
||||||
containerBehavior: this.state.currentItemContainerBehavior,
|
|
||||||
}),
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
let element;
|
let element;
|
||||||
@@ -11473,7 +11471,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
): void => {
|
): void => {
|
||||||
const selectionElement = this.state.selectionElement;
|
const selectionElement = this.state.selectionElement;
|
||||||
const pointerCoords = pointerDownState.lastCoords;
|
const pointerCoords = pointerDownState.lastCoords;
|
||||||
if (selectionElement && this.state.activeTool.type !== "eraser") {
|
if (
|
||||||
|
selectionElement &&
|
||||||
|
pointerDownState.boxSelection.hasOccurred &&
|
||||||
|
this.state.activeTool.type !== "eraser"
|
||||||
|
) {
|
||||||
dragNewElement({
|
dragNewElement({
|
||||||
newElement: selectionElement,
|
newElement: selectionElement,
|
||||||
elementType: this.state.activeTool.type,
|
elementType: this.state.activeTool.type,
|
||||||
@@ -11537,25 +11539,27 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
snapLines,
|
snapLines,
|
||||||
});
|
});
|
||||||
|
|
||||||
dragNewElement({
|
if (!isBindingElement(newElement)) {
|
||||||
newElement,
|
dragNewElement({
|
||||||
elementType: this.state.activeTool.type,
|
newElement,
|
||||||
originX: pointerDownState.originInGrid.x,
|
elementType: this.state.activeTool.type,
|
||||||
originY: pointerDownState.originInGrid.y,
|
originX: pointerDownState.originInGrid.x,
|
||||||
x: gridX,
|
originY: pointerDownState.originInGrid.y,
|
||||||
y: gridY,
|
x: gridX,
|
||||||
width: distance(pointerDownState.originInGrid.x, gridX),
|
y: gridY,
|
||||||
height: distance(pointerDownState.originInGrid.y, gridY),
|
width: distance(pointerDownState.originInGrid.x, gridX),
|
||||||
shouldMaintainAspectRatio: isImageElement(newElement)
|
height: distance(pointerDownState.originInGrid.y, gridY),
|
||||||
? !shouldMaintainAspectRatio(event)
|
shouldMaintainAspectRatio: isImageElement(newElement)
|
||||||
: shouldMaintainAspectRatio(event),
|
? !shouldMaintainAspectRatio(event)
|
||||||
shouldResizeFromCenter: shouldResizeFromCenter(event),
|
: shouldMaintainAspectRatio(event),
|
||||||
zoom: this.state.zoom.value,
|
shouldResizeFromCenter: shouldResizeFromCenter(event),
|
||||||
scene: this.scene,
|
zoom: this.state.zoom.value,
|
||||||
widthAspectRatio: aspectRatio,
|
scene: this.scene,
|
||||||
originOffset: this.state.originSnapOffset,
|
widthAspectRatio: aspectRatio,
|
||||||
informMutation,
|
originOffset: this.state.originSnapOffset,
|
||||||
});
|
informMutation,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
newElement,
|
newElement,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { hitElementBoundingBox } from "@excalidraw/element";
|
|||||||
|
|
||||||
import type { GlobalPoint, Radians } from "@excalidraw/math";
|
import type { GlobalPoint, Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import type { Bounds } from "@excalidraw/element";
|
import type { Bounds } from "@excalidraw/common";
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
|
|||||||
@@ -2373,101 +2373,3 @@ export const presentationIcon = createIcon(
|
|||||||
</g>,
|
</g>,
|
||||||
tablerIconProps,
|
tablerIconProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const stickyNoteIcon = createIcon(
|
|
||||||
<g>
|
|
||||||
<path d="M16 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V8Z" />
|
|
||||||
<path d="M15 3v4a2 2 0 0 0 2 2h4" />
|
|
||||||
</g>,
|
|
||||||
tablerIconProps,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const marginSmallIcon = createIcon(
|
|
||||||
<g fill="none">
|
|
||||||
<rect
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="1.5"
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
x="2"
|
|
||||||
y="2"
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="1"
|
|
||||||
stroke-dasharray="1 1"
|
|
||||||
/>
|
|
||||||
</g>,
|
|
||||||
tablerIconProps,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const marginMediumIcon = createIcon(
|
|
||||||
<g fill="none">
|
|
||||||
<rect
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="1.5"
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
x="4"
|
|
||||||
y="4"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="1"
|
|
||||||
stroke-dasharray="1 1"
|
|
||||||
/>
|
|
||||||
</g>,
|
|
||||||
tablerIconProps,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const marginLargeIcon = createIcon(
|
|
||||||
<g fill="none">
|
|
||||||
<rect
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="1.5"
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
x="6"
|
|
||||||
y="6"
|
|
||||||
width="12"
|
|
||||||
height="12"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="1"
|
|
||||||
stroke-dasharray="1 1"
|
|
||||||
/>
|
|
||||||
</g>,
|
|
||||||
tablerIconProps,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const growingContainerIcon = createIcon(
|
|
||||||
<g
|
|
||||||
stroke="currentColor"
|
|
||||||
fill="none"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
>
|
|
||||||
<path d="M14 21h1" />
|
|
||||||
<path d="M21 14v1" />
|
|
||||||
<path d="M21 19a2 2 0 0 1-2 2" />
|
|
||||||
<path d="M21 9v1" />
|
|
||||||
<path d="M3 14v1" />
|
|
||||||
<path d="M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2" />
|
|
||||||
<path d="M3 9v1" />
|
|
||||||
<path d="M5 21a2 2 0 0 1-2-2" />
|
|
||||||
<path d="M9 21h1" />
|
|
||||||
</g>,
|
|
||||||
tablerIconProps,
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ function dataView(
|
|||||||
*
|
*
|
||||||
* @param buffers each buffer (chunk) must be at most 2^32 bits large (~4GB)
|
* @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(
|
const bufferView = new Uint8Array(
|
||||||
VERSION_DATAVIEW_BYTES +
|
VERSION_DATAVIEW_BYTES +
|
||||||
NEXT_CHUNK_SIZE_DATAVIEW_BYTES * buffers.length +
|
NEXT_CHUNK_SIZE_DATAVIEW_BYTES * buffers.length +
|
||||||
@@ -295,12 +295,12 @@ const splitBuffers = (concatenatedBuffer: Uint8Array) => {
|
|||||||
|
|
||||||
/** @private */
|
/** @private */
|
||||||
const _encryptAndCompress = async (
|
const _encryptAndCompress = async (
|
||||||
data: Uint8Array | string,
|
data: Uint8Array<ArrayBuffer> | string,
|
||||||
encryptionKey: string,
|
encryptionKey: string,
|
||||||
) => {
|
) => {
|
||||||
const { encryptedBuffer, iv } = await encryptData(
|
const { encryptedBuffer, iv } = await encryptData(
|
||||||
encryptionKey,
|
encryptionKey,
|
||||||
deflate(data),
|
deflate(data) as Uint8Array<ArrayBuffer>,
|
||||||
);
|
);
|
||||||
|
|
||||||
return { iv, buffer: new Uint8Array(encryptedBuffer) };
|
return { iv, buffer: new Uint8Array(encryptedBuffer) };
|
||||||
@@ -330,7 +330,7 @@ export const compressData = async <T extends Record<string, any> = never>(
|
|||||||
: {
|
: {
|
||||||
metadata: T;
|
metadata: T;
|
||||||
}),
|
}),
|
||||||
): Promise<Uint8Array> => {
|
): Promise<Uint8Array<ArrayBuffer>> => {
|
||||||
const fileInfo: FileEncodingInfo = {
|
const fileInfo: FileEncodingInfo = {
|
||||||
version: 2,
|
version: 2,
|
||||||
compression: "pako@1",
|
compression: "pako@1",
|
||||||
@@ -355,8 +355,8 @@ export const compressData = async <T extends Record<string, any> = never>(
|
|||||||
|
|
||||||
/** @private */
|
/** @private */
|
||||||
const _decryptAndDecompress = async (
|
const _decryptAndDecompress = async (
|
||||||
iv: Uint8Array,
|
iv: Uint8Array<ArrayBuffer>,
|
||||||
decryptedBuffer: Uint8Array,
|
decryptedBuffer: Uint8Array<ArrayBuffer>,
|
||||||
decryptionKey: string,
|
decryptionKey: string,
|
||||||
isCompressed: boolean,
|
isCompressed: boolean,
|
||||||
) => {
|
) => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { blobToArrayBuffer } from "./blob";
|
|||||||
|
|
||||||
export const IV_LENGTH_BYTES = 12;
|
export const IV_LENGTH_BYTES = 12;
|
||||||
|
|
||||||
export const createIV = () => {
|
export const createIV = (): Uint8Array<ArrayBuffer> => {
|
||||||
const arr = new Uint8Array(IV_LENGTH_BYTES);
|
const arr = new Uint8Array(IV_LENGTH_BYTES);
|
||||||
return window.crypto.getRandomValues(arr);
|
return window.crypto.getRandomValues(arr);
|
||||||
};
|
};
|
||||||
@@ -49,12 +49,12 @@ export const getCryptoKey = (key: string, usage: KeyUsage) =>
|
|||||||
|
|
||||||
export const encryptData = async (
|
export const encryptData = async (
|
||||||
key: string | CryptoKey,
|
key: string | CryptoKey,
|
||||||
data: Uint8Array | ArrayBuffer | Blob | File | string,
|
data: Uint8Array<ArrayBuffer> | ArrayBuffer | Blob | File | string,
|
||||||
): Promise<{ encryptedBuffer: ArrayBuffer; iv: Uint8Array }> => {
|
): Promise<{ encryptedBuffer: ArrayBuffer; iv: Uint8Array<ArrayBuffer> }> => {
|
||||||
const importedKey =
|
const importedKey =
|
||||||
typeof key === "string" ? await getCryptoKey(key, "encrypt") : key;
|
typeof key === "string" ? await getCryptoKey(key, "encrypt") : key;
|
||||||
const iv = createIV();
|
const iv = createIV();
|
||||||
const buffer: ArrayBuffer | Uint8Array =
|
const buffer: ArrayBuffer | Uint8Array<ArrayBuffer> =
|
||||||
typeof data === "string"
|
typeof data === "string"
|
||||||
? new TextEncoder().encode(data)
|
? new TextEncoder().encode(data)
|
||||||
: data instanceof Uint8Array
|
: data instanceof Uint8Array
|
||||||
@@ -71,15 +71,15 @@ export const encryptData = async (
|
|||||||
iv,
|
iv,
|
||||||
},
|
},
|
||||||
importedKey,
|
importedKey,
|
||||||
buffer as ArrayBuffer | Uint8Array,
|
buffer,
|
||||||
);
|
);
|
||||||
|
|
||||||
return { encryptedBuffer, iv };
|
return { encryptedBuffer, iv };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const decryptData = async (
|
export const decryptData = async (
|
||||||
iv: Uint8Array,
|
iv: Uint8Array<ArrayBuffer>,
|
||||||
encrypted: Uint8Array | ArrayBuffer,
|
encrypted: Uint8Array<ArrayBuffer> | ArrayBuffer,
|
||||||
privateKey: string,
|
privateKey: string,
|
||||||
): Promise<ArrayBuffer> => {
|
): Promise<ArrayBuffer> => {
|
||||||
const key = await getCryptoKey(privateKey, "decrypt");
|
const key = await getCryptoKey(privateKey, "decrypt");
|
||||||
|
|||||||
@@ -17,12 +17,10 @@ import {
|
|||||||
getSizeFromPoints,
|
getSizeFromPoints,
|
||||||
normalizeLink,
|
normalizeLink,
|
||||||
getLineHeight,
|
getLineHeight,
|
||||||
BOUND_TEXT_PADDING,
|
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
import {
|
import {
|
||||||
calculateFixedPointForNonElbowArrowBinding,
|
calculateFixedPointForNonElbowArrowBinding,
|
||||||
getNonDeletedElements,
|
getNonDeletedElements,
|
||||||
isFlowchartType,
|
|
||||||
isPointInElement,
|
isPointInElement,
|
||||||
isValidPolygon,
|
isValidPolygon,
|
||||||
projectFixedPointOntoDiagonal,
|
projectFixedPointOntoDiagonal,
|
||||||
@@ -58,11 +56,11 @@ import type { LocalPoint, Radians } from "@excalidraw/math";
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
|
ElementsMapOrArray,
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawElementType,
|
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawSelectionElement,
|
ExcalidrawSelectionElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
@@ -132,7 +130,8 @@ const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => {
|
|||||||
const repairBinding = <T extends ExcalidrawArrowElement>(
|
const repairBinding = <T extends ExcalidrawArrowElement>(
|
||||||
element: T,
|
element: T,
|
||||||
binding: FixedPointBinding | null,
|
binding: FixedPointBinding | null,
|
||||||
elementsMap: Readonly<ElementsMap>,
|
targetElementsMap: Readonly<ElementsMap>,
|
||||||
|
localElementsMap: Readonly<ElementsMap> | null | undefined,
|
||||||
startOrEnd: "start" | "end",
|
startOrEnd: "start" | "end",
|
||||||
): FixedPointBinding | null => {
|
): FixedPointBinding | null => {
|
||||||
if (!binding) {
|
if (!binding) {
|
||||||
@@ -151,18 +150,27 @@ const repairBinding = <T extends ExcalidrawArrowElement>(
|
|||||||
return fixedPointBinding;
|
return fixedPointBinding;
|
||||||
}
|
}
|
||||||
|
|
||||||
const boundElement =
|
// Fallback if the bound element is missing but the binding is at least
|
||||||
(elementsMap.get(binding.elementId) as ExcalidrawBindableElement) ||
|
// looking like a valid one shape-wise
|
||||||
undefined;
|
if (binding.mode && binding.fixedPoint && binding.elementId) {
|
||||||
if (boundElement) {
|
return {
|
||||||
if (binding.mode) {
|
elementId: binding.elementId,
|
||||||
return {
|
mode: binding.mode,
|
||||||
elementId: binding.elementId,
|
fixedPoint: normalizeFixedPoint(binding.fixedPoint || [0.5, 0.5]),
|
||||||
mode: binding.mode || "orbit",
|
} as FixedPointBinding | null;
|
||||||
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(
|
const p = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||||
element,
|
element,
|
||||||
startOrEnd === "start" ? 0 : element.points.length - 1,
|
startOrEnd === "start" ? 0 : element.points.length - 1,
|
||||||
@@ -196,30 +204,19 @@ const repairBinding = <T extends ExcalidrawArrowElement>(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.error(`could not repair binding for element`);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type _ElementForRestoreBase = Required<
|
const restoreElementWithProperties = <
|
||||||
Omit<ExcalidrawElement, "customData" | "containerBehavior">
|
T extends Required<Omit<ExcalidrawElement, "customData">> & {
|
||||||
> &
|
|
||||||
Pick<ExcalidrawElement, "containerBehavior"> & {
|
|
||||||
customData?: ExcalidrawElement["customData"];
|
customData?: ExcalidrawElement["customData"];
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
boundElementIds?: readonly ExcalidrawElement["id"][];
|
boundElementIds?: readonly ExcalidrawElement["id"][];
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
strokeSharpness?: StrokeRoundness;
|
strokeSharpness?: StrokeRoundness;
|
||||||
};
|
},
|
||||||
|
|
||||||
const restoreElementWithProperties = <
|
|
||||||
T extends _ElementForRestoreBase &
|
|
||||||
Omit<
|
|
||||||
any,
|
|
||||||
| keyof ExcalidrawElement
|
|
||||||
| "boundElementIds"
|
|
||||||
| "strokeSharpness"
|
|
||||||
| "customData"
|
|
||||||
| "containerBehavior"
|
|
||||||
>,
|
|
||||||
K extends Pick<T, keyof Omit<Required<T>, keyof ExcalidrawElement>>,
|
K extends Pick<T, keyof Omit<Required<T>, keyof ExcalidrawElement>>,
|
||||||
>(
|
>(
|
||||||
element: T,
|
element: T,
|
||||||
@@ -231,13 +228,11 @@ const restoreElementWithProperties = <
|
|||||||
> &
|
> &
|
||||||
Partial<Pick<ExcalidrawElement, "type" | "x" | "y" | "customData">>,
|
Partial<Pick<ExcalidrawElement, "type" | "x" | "y" | "customData">>,
|
||||||
): T => {
|
): T => {
|
||||||
const nextType = (extra.type || element.type) as ExcalidrawElementType;
|
|
||||||
|
|
||||||
const base: Pick<T, keyof ExcalidrawElement> = {
|
const base: Pick<T, keyof ExcalidrawElement> = {
|
||||||
type: nextType,
|
type: extra.type || element.type,
|
||||||
version: element.version || 1,
|
|
||||||
// all elements must have version > 0 so getSceneVersion() will pick up
|
// all elements must have version > 0 so getSceneVersion() will pick up
|
||||||
// newly added elements
|
// newly added elements
|
||||||
|
version: element.version || 1,
|
||||||
versionNonce: element.versionNonce ?? 0,
|
versionNonce: element.versionNonce ?? 0,
|
||||||
index: element.index ?? null,
|
index: element.index ?? null,
|
||||||
isDeleted: element.isDeleted ?? false,
|
isDeleted: element.isDeleted ?? false,
|
||||||
@@ -265,7 +260,7 @@ const restoreElementWithProperties = <
|
|||||||
? {
|
? {
|
||||||
// for old elements that would now use adaptive radius algo,
|
// for old elements that would now use adaptive radius algo,
|
||||||
// use legacy algo instead
|
// use legacy algo instead
|
||||||
type: isUsingAdaptiveRadius(nextType)
|
type: isUsingAdaptiveRadius(element.type)
|
||||||
? ROUNDNESS.LEGACY
|
? ROUNDNESS.LEGACY
|
||||||
: ROUNDNESS.PROPORTIONAL_RADIUS,
|
: ROUNDNESS.PROPORTIONAL_RADIUS,
|
||||||
}
|
}
|
||||||
@@ -276,18 +271,10 @@ const restoreElementWithProperties = <
|
|||||||
updated: element.updated ?? getUpdatedTimestamp(),
|
updated: element.updated ?? getUpdatedTimestamp(),
|
||||||
link: element.link ? normalizeLink(element.link) : null,
|
link: element.link ? normalizeLink(element.link) : null,
|
||||||
locked: element.locked ?? false,
|
locked: element.locked ?? false,
|
||||||
...(isFlowchartType(nextType)
|
|
||||||
? {
|
|
||||||
containerBehavior: {
|
|
||||||
textFlow: element.containerBehavior?.textFlow ?? "growing",
|
|
||||||
margin: element.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if ("customData" in element || "customData" in extra) {
|
if ("customData" in element || "customData" in extra) {
|
||||||
(base as any).customData =
|
base.customData =
|
||||||
"customData" in extra ? extra.customData : element.customData;
|
"customData" in extra ? extra.customData : element.customData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,15 +289,16 @@ const restoreElementWithProperties = <
|
|||||||
} as unknown as T;
|
} as unknown as T;
|
||||||
|
|
||||||
// strip legacy props (migrated in previous steps)
|
// strip legacy props (migrated in previous steps)
|
||||||
delete (ret as any).strokeSharpness;
|
delete ret.strokeSharpness;
|
||||||
delete (ret as any).boundElementIds;
|
delete ret.boundElementIds;
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const restoreElement = (
|
export const restoreElement = (
|
||||||
element: Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
|
element: Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
|
||||||
elementsMap: Readonly<ElementsMap>,
|
targetElementsMap: Readonly<ElementsMap>,
|
||||||
|
localElementsMap: Readonly<ElementsMap> | null | undefined,
|
||||||
opts?: {
|
opts?: {
|
||||||
deleteInvisibleElements?: boolean;
|
deleteInvisibleElements?: boolean;
|
||||||
},
|
},
|
||||||
@@ -431,13 +419,15 @@ export const restoreElement = (
|
|||||||
startBinding: repairBinding(
|
startBinding: repairBinding(
|
||||||
element as ExcalidrawArrowElement,
|
element as ExcalidrawArrowElement,
|
||||||
element.startBinding,
|
element.startBinding,
|
||||||
elementsMap,
|
targetElementsMap,
|
||||||
|
localElementsMap,
|
||||||
"start",
|
"start",
|
||||||
),
|
),
|
||||||
endBinding: repairBinding(
|
endBinding: repairBinding(
|
||||||
element as ExcalidrawArrowElement,
|
element as ExcalidrawArrowElement,
|
||||||
element.endBinding,
|
element.endBinding,
|
||||||
elementsMap,
|
targetElementsMap,
|
||||||
|
localElementsMap,
|
||||||
"end",
|
"end",
|
||||||
),
|
),
|
||||||
startArrowhead,
|
startArrowhead,
|
||||||
@@ -601,7 +591,7 @@ const repairFrameMembership = (
|
|||||||
export const restoreElements = (
|
export const restoreElements = (
|
||||||
targetElements: ImportedDataState["elements"],
|
targetElements: ImportedDataState["elements"],
|
||||||
/** NOTE doesn't serve for reconciliation */
|
/** NOTE doesn't serve for reconciliation */
|
||||||
localElements: readonly ExcalidrawElement[] | null | undefined,
|
localElements: Readonly<ElementsMapOrArray> | null | undefined,
|
||||||
opts?:
|
opts?:
|
||||||
| {
|
| {
|
||||||
refreshDimensions?: boolean;
|
refreshDimensions?: boolean;
|
||||||
@@ -612,7 +602,7 @@ export const restoreElements = (
|
|||||||
): OrderedExcalidrawElement[] => {
|
): OrderedExcalidrawElement[] => {
|
||||||
// used to detect duplicate top-level element ids
|
// used to detect duplicate top-level element ids
|
||||||
const existingIds = new Set<string>();
|
const existingIds = new Set<string>();
|
||||||
const elementsMap = arrayToMap(targetElements || []);
|
const targetElementsMap = arrayToMap(targetElements || []);
|
||||||
const localElementsMap = localElements ? arrayToMap(localElements) : null;
|
const localElementsMap = localElements ? arrayToMap(localElements) : null;
|
||||||
const restoredElements = syncInvalidIndices(
|
const restoredElements = syncInvalidIndices(
|
||||||
(targetElements || []).reduce((elements, element) => {
|
(targetElements || []).reduce((elements, element) => {
|
||||||
@@ -624,7 +614,8 @@ export const restoreElements = (
|
|||||||
|
|
||||||
let migratedElement: ExcalidrawElement | null = restoreElement(
|
let migratedElement: ExcalidrawElement | null = restoreElement(
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
targetElementsMap,
|
||||||
|
localElementsMap,
|
||||||
{
|
{
|
||||||
deleteInvisibleElements: opts?.deleteInvisibleElements,
|
deleteInvisibleElements: opts?.deleteInvisibleElements,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { shouldTestInside } from "@excalidraw/element";
|
|||||||
import { hasBoundTextElement, isBoundToContainer } from "@excalidraw/element";
|
import { hasBoundTextElement, isBoundToContainer } from "@excalidraw/element";
|
||||||
import { getBoundTextElementId } 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 { GlobalPoint, LineSegment } from "@excalidraw/math/types";
|
||||||
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
CJK_HAND_DRAWN_FALLBACK_FONT,
|
CJK_HAND_DRAWN_FALLBACK_FONT,
|
||||||
WINDOWS_EMOJI_FALLBACK_FONT,
|
WINDOWS_EMOJI_FALLBACK_FONT,
|
||||||
getFontFamilyFallbacks,
|
getFontFamilyFallbacks,
|
||||||
|
FONT_SIZES,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
import { getContainerElement } from "@excalidraw/element";
|
import { getContainerElement } from "@excalidraw/element";
|
||||||
import { charWidth } from "@excalidraw/element";
|
import { charWidth } from "@excalidraw/element";
|
||||||
@@ -240,7 +241,7 @@ export class Fonts {
|
|||||||
for (const [index, fontFamily] of fontFamilies.entries()) {
|
for (const [index, fontFamily] of fontFamilies.entries()) {
|
||||||
const font = getFontString({
|
const font = getFontString({
|
||||||
fontFamily,
|
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!
|
// 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 };
|
function decode(data: Uint8Array): { keyword: string; text: string };
|
||||||
}
|
}
|
||||||
declare module "png-chunks-encode" {
|
declare module "png-chunks-encode" {
|
||||||
function encode(chunks: TEXtChunk[]): Uint8Array;
|
function encode(chunks: TEXtChunk[]): Uint8Array<ArrayBuffer>;
|
||||||
export = encode;
|
export = encode;
|
||||||
}
|
}
|
||||||
declare module "png-chunks-extract" {
|
declare module "png-chunks-extract" {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function useOutsideClick<T extends HTMLElement>(
|
|||||||
* Returning `undefined` will fallback to the default behavior.
|
* Returning `undefined` will fallback to the default behavior.
|
||||||
*/
|
*/
|
||||||
isInside?: (
|
isInside?: (
|
||||||
event: Event & { target: HTMLElement },
|
event: Event & { target: T },
|
||||||
/** the element of the passed ref */
|
/** the element of the passed ref */
|
||||||
container: T,
|
container: T,
|
||||||
) => boolean | undefined,
|
) => boolean | undefined,
|
||||||
|
|||||||
@@ -293,8 +293,11 @@ export { TTDDialog } from "./components/TTDDialog/TTDDialog";
|
|||||||
export { TTDDialogTrigger } from "./components/TTDDialog/TTDDialogTrigger";
|
export { TTDDialogTrigger } from "./components/TTDDialog/TTDDialogTrigger";
|
||||||
|
|
||||||
export { zoomToFitBounds } from "./actions/actionCanvas";
|
export { zoomToFitBounds } from "./actions/actionCanvas";
|
||||||
export { convertToExcalidrawElements } from "./data/transform";
|
export {
|
||||||
export { getCommonBounds, getVisibleSceneBounds } from "@excalidraw/element";
|
getCommonBounds,
|
||||||
|
getVisibleSceneBounds,
|
||||||
|
convertToExcalidrawElements,
|
||||||
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
elementsOverlappingBBox,
|
elementsOverlappingBBox,
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import {
|
|||||||
polygonIncludesPointNonZero,
|
polygonIncludesPointNonZero,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
|
import { type Bounds } from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type Bounds,
|
|
||||||
computeBoundTextPosition,
|
computeBoundTextPosition,
|
||||||
doBoundsIntersect,
|
doBoundsIntersect,
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
|
|||||||
@@ -31,12 +31,6 @@
|
|||||||
"strokeStyle_dashed": "Dashed",
|
"strokeStyle_dashed": "Dashed",
|
||||||
"strokeStyle_dotted": "Dotted",
|
"strokeStyle_dotted": "Dotted",
|
||||||
"sloppiness": "Sloppiness",
|
"sloppiness": "Sloppiness",
|
||||||
"container": "Container",
|
|
||||||
"container_fixed": "Sticky note",
|
|
||||||
"container_growing": "Fit to text",
|
|
||||||
"container_margin_small": "Small margin",
|
|
||||||
"container_margin_medium": "Medium margin",
|
|
||||||
"container_margin_large": "Large margin",
|
|
||||||
"opacity": "Opacity",
|
"opacity": "Opacity",
|
||||||
"textAlign": "Text align",
|
"textAlign": "Text align",
|
||||||
"edges": "Edges",
|
"edges": "Edges",
|
||||||
|
|||||||
@@ -133,6 +133,6 @@
|
|||||||
"fonteditor-core": "2.4.1",
|
"fonteditor-core": "2.4.1",
|
||||||
"harfbuzzjs": "0.3.6",
|
"harfbuzzjs": "0.3.6",
|
||||||
"jest-diff": "29.7.0",
|
"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 { newTextElement } from "@excalidraw/element";
|
||||||
|
|
||||||
import type { Bounds } from "@excalidraw/element";
|
import type { Bounds } from "@excalidraw/common";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
|
|
||||||
import type { InclusiveRange } from "@excalidraw/math";
|
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 { MaybeTransformHandleType } from "@excalidraw/element";
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const load = (): Promise<{
|
|||||||
subset: (
|
subset: (
|
||||||
fontBuffer: ArrayBuffer,
|
fontBuffer: ArrayBuffer,
|
||||||
codePoints: ReadonlySet<number>,
|
codePoints: ReadonlySet<number>,
|
||||||
) => Uint8Array;
|
) => Uint8Array<ArrayBuffer>;
|
||||||
}> => {
|
}> => {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ type Vector = any;
|
|||||||
let loadedWasm: ReturnType<typeof load> | null = null;
|
let loadedWasm: ReturnType<typeof load> | null = null;
|
||||||
|
|
||||||
// re-map from internal vector into byte array
|
// re-map from internal vector into byte array
|
||||||
function convertFromVecToUint8Array(vector: Vector): Uint8Array {
|
function convertFromVecToUint8Array(vector: Vector): Uint8Array<ArrayBuffer> {
|
||||||
const arr = [];
|
const arr = [];
|
||||||
for (let i = 0, l = vector.size(); i < l; i++) {
|
for (let i = 0, l = vector.size(); i < l; i++) {
|
||||||
arr.push(vector.get(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.)
|
// TODO: consider adding support for fetching the wasm from an URL (external CDN, data URL, etc.)
|
||||||
const load = (): Promise<{
|
const load = (): Promise<{
|
||||||
compress: (buffer: ArrayBuffer) => Uint8Array;
|
compress: (buffer: ArrayBuffer) => Uint8Array<ArrayBuffer>;
|
||||||
decompress: (buffer: ArrayBuffer) => Uint8Array;
|
decompress: (buffer: ArrayBuffer) => Uint8Array<ArrayBuffer>;
|
||||||
}> => {
|
}> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1223,7 +1223,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 2019559783,
|
"versionNonce": 401146281,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -20,
|
"x": -20,
|
||||||
"y": -10,
|
"y": -10,
|
||||||
@@ -1431,14 +1431,14 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 238820263,
|
"seed": 1014066025,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 1505387817,
|
"versionNonce": 1604849351,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
@@ -1463,14 +1463,14 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 449462985,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 915032327,
|
"versionNonce": 493213705,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -1765,14 +1765,14 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 238820263,
|
"seed": 1014066025,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 1505387817,
|
"versionNonce": 1604849351,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
@@ -1797,14 +1797,14 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 449462985,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 915032327,
|
"versionNonce": 493213705,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -2108,7 +2108,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
|||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 2019559783,
|
"versionNonce": 401146281,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -20,
|
"x": -20,
|
||||||
"y": -10,
|
"y": -10,
|
||||||
@@ -2321,7 +2321,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 1014066025,
|
"versionNonce": 1116226695,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -20,
|
"x": -20,
|
||||||
"y": -10,
|
"y": -10,
|
||||||
@@ -2567,7 +2567,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 2019559783,
|
"versionNonce": 401146281,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -20,
|
"x": -20,
|
||||||
"y": -10,
|
"y": -10,
|
||||||
@@ -2592,14 +2592,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 238820263,
|
"seed": 1014066025,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 5,
|
"version": 5,
|
||||||
"versionNonce": 1604849351,
|
"versionNonce": 400692809,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -2868,14 +2868,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 449462985,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 81784553,
|
"versionNonce": 915032327,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -2902,14 +2902,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 238820263,
|
"seed": 1014066025,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 747212839,
|
"versionNonce": 81784553,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
@@ -3238,14 +3238,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"opacity": 60,
|
"opacity": 60,
|
||||||
"roughness": 2,
|
"roughness": 2,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 449462985,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#e03131",
|
"strokeColor": "#e03131",
|
||||||
"strokeStyle": "dotted",
|
"strokeStyle": "dotted",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 1359939303,
|
"versionNonce": 1402203177,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -3270,14 +3270,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"opacity": 60,
|
"opacity": 60,
|
||||||
"roughness": 2,
|
"roughness": 2,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 640725609,
|
"seed": 1898319239,
|
||||||
"strokeColor": "#e03131",
|
"strokeColor": "#e03131",
|
||||||
"strokeStyle": "dotted",
|
"strokeStyle": "dotted",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 9,
|
"version": 9,
|
||||||
"versionNonce": 908564423,
|
"versionNonce": 941653321,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
@@ -3732,14 +3732,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 400692809,
|
"seed": 238820263,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 81784553,
|
"versionNonce": 915032327,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
@@ -3764,14 +3764,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 449462985,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 1150084233,
|
"versionNonce": 2019559783,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -4058,14 +4058,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 238820263,
|
"seed": 1014066025,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 915032327,
|
"versionNonce": 493213705,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
@@ -4090,14 +4090,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 449462985,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 1150084233,
|
"versionNonce": 2019559783,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -4387,14 +4387,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 449462985,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 5,
|
"version": 5,
|
||||||
"versionNonce": 1006504105,
|
"versionNonce": 760410951,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -4419,14 +4419,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 400692809,
|
"seed": 238820263,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 5,
|
"version": 5,
|
||||||
"versionNonce": 289600103,
|
"versionNonce": 1006504105,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
@@ -5675,14 +5675,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 453191,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 1014066025,
|
"versionNonce": 1150084233,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -5707,14 +5707,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 1505387817,
|
"seed": 400692809,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 915032327,
|
"versionNonce": 23633383,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 12,
|
"x": 12,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -6899,14 +6899,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 449462985,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 1723083209,
|
"versionNonce": 747212839,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -6933,14 +6933,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 400692809,
|
"seed": 238820263,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 760410951,
|
"versionNonce": 1723083209,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 12,
|
"x": 12,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@@ -9837,7 +9837,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] el
|
|||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 2019559783,
|
"versionNonce": 401146281,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -20,
|
"x": -20,
|
||||||
"y": -10,
|
"y": -10,
|
||||||
|
|||||||
@@ -13935,7 +13935,7 @@ exports[`regression tests > switches from group of selected elements to another
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 289600103,
|
"seed": 1006504105,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ exports[`select single element on the scene > arrow 1`] = `
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 5,
|
||||||
"versionNonce": 2019559783,
|
"versionNonce": 1116226695,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 10,
|
"x": 10,
|
||||||
"y": 10,
|
"y": 10,
|
||||||
@@ -88,8 +88,8 @@ exports[`select single element on the scene > arrow escape 1`] = `
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "line",
|
"type": "line",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 5,
|
||||||
"versionNonce": 2019559783,
|
"versionNonce": 1116226695,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 10,
|
"x": 10,
|
||||||
"y": 10,
|
"y": 10,
|
||||||
@@ -120,8 +120,8 @@ exports[`select single element on the scene > diamond 1`] = `
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "diamond",
|
"type": "diamond",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 4,
|
||||||
"versionNonce": 401146281,
|
"versionNonce": 2019559783,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 10,
|
"x": 10,
|
||||||
"y": 10,
|
"y": 10,
|
||||||
@@ -152,8 +152,8 @@ exports[`select single element on the scene > ellipse 1`] = `
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "ellipse",
|
"type": "ellipse",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 4,
|
||||||
"versionNonce": 401146281,
|
"versionNonce": 2019559783,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 10,
|
"x": 10,
|
||||||
"y": 10,
|
"y": 10,
|
||||||
@@ -184,8 +184,8 @@ exports[`select single element on the scene > rectangle 1`] = `
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 4,
|
||||||
"versionNonce": 401146281,
|
"versionNonce": 2019559783,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 10,
|
"x": 10,
|
||||||
"y": 10,
|
"y": 10,
|
||||||
|
|||||||
+5
-9
@@ -1,4 +1,4 @@
|
|||||||
import { BOUND_TEXT_PADDING, DEFAULT_FONT_FAMILY } from "@excalidraw/common";
|
import { DEFAULT_FONT_FAMILY } from "@excalidraw/common";
|
||||||
|
|
||||||
import type { Radians } from "@excalidraw/math";
|
import type { Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
@@ -34,29 +34,25 @@ const elementBase: Omit<ExcalidrawElement, "type"> = {
|
|||||||
|
|
||||||
export const rectangleFixture: ExcalidrawElement = {
|
export const rectangleFixture: ExcalidrawElement = {
|
||||||
...elementBase,
|
...elementBase,
|
||||||
containerBehavior: { textFlow: "growing", margin: BOUND_TEXT_PADDING },
|
|
||||||
type: "rectangle",
|
type: "rectangle",
|
||||||
} as unknown as ExcalidrawElement;
|
};
|
||||||
export const embeddableFixture: ExcalidrawElement = {
|
export const embeddableFixture: ExcalidrawElement = {
|
||||||
...elementBase,
|
...elementBase,
|
||||||
type: "embeddable",
|
type: "embeddable",
|
||||||
};
|
};
|
||||||
export const ellipseFixture: ExcalidrawElement = {
|
export const ellipseFixture: ExcalidrawElement = {
|
||||||
...elementBase,
|
...elementBase,
|
||||||
containerBehavior: { textFlow: "growing", margin: BOUND_TEXT_PADDING },
|
|
||||||
type: "ellipse",
|
type: "ellipse",
|
||||||
} as unknown as ExcalidrawElement;
|
};
|
||||||
export const diamondFixture: ExcalidrawElement = {
|
export const diamondFixture: ExcalidrawElement = {
|
||||||
...elementBase,
|
...elementBase,
|
||||||
containerBehavior: { textFlow: "growing", margin: BOUND_TEXT_PADDING },
|
|
||||||
type: "diamond",
|
type: "diamond",
|
||||||
} as unknown as ExcalidrawElement;
|
};
|
||||||
export const rectangleWithLinkFixture: ExcalidrawElement = {
|
export const rectangleWithLinkFixture: ExcalidrawElement = {
|
||||||
...elementBase,
|
...elementBase,
|
||||||
containerBehavior: { textFlow: "growing", margin: BOUND_TEXT_PADDING },
|
|
||||||
type: "rectangle",
|
type: "rectangle",
|
||||||
link: "excalidraw.com",
|
link: "excalidraw.com",
|
||||||
} as unknown as ExcalidrawElement;
|
};
|
||||||
|
|
||||||
export const textFixture: ExcalidrawElement = {
|
export const textFixture: ExcalidrawElement = {
|
||||||
...elementBase,
|
...elementBase,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import util from "util";
|
|||||||
|
|
||||||
import { pointFrom, type LocalPoint, type Radians } from "@excalidraw/math";
|
import { pointFrom, type LocalPoint, type Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import { BOUND_TEXT_PADDING, DEFAULT_VERTICAL_ALIGN, ROUNDNESS, assertNever } from "@excalidraw/common";
|
import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS, assertNever } from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
newArrowElement,
|
newArrowElement,
|
||||||
@@ -23,6 +23,8 @@ import { isLinearElementType } from "@excalidraw/element";
|
|||||||
import { getSelectedElements } from "@excalidraw/element";
|
import { getSelectedElements } from "@excalidraw/element";
|
||||||
import { selectGroupsForSelectedElements } from "@excalidraw/element";
|
import { selectGroupsForSelectedElements } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import { FONT_SIZES } from "@excalidraw/common";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawGenericElement,
|
ExcalidrawGenericElement,
|
||||||
@@ -217,10 +219,6 @@ export class API {
|
|||||||
: never;
|
: never;
|
||||||
elbowed?: boolean;
|
elbowed?: boolean;
|
||||||
fixedSegments?: FixedSegment[] | null;
|
fixedSegments?: FixedSegment[] | null;
|
||||||
containerBehavior?: T extends "rectangle" | "diamond" | "ellipse"
|
|
||||||
? ExcalidrawGenericElement["containerBehavior"]
|
|
||||||
: never;
|
|
||||||
} = {
|
|
||||||
}): T extends "arrow" | "line"
|
}): T extends "arrow" | "line"
|
||||||
? ExcalidrawLinearElement
|
? ExcalidrawLinearElement
|
||||||
: T extends "freedraw"
|
: T extends "freedraw"
|
||||||
@@ -286,10 +284,6 @@ export class API {
|
|||||||
element = newElement({
|
element = newElement({
|
||||||
type: type as "rectangle" | "diamond" | "ellipse",
|
type: type as "rectangle" | "diamond" | "ellipse",
|
||||||
...base,
|
...base,
|
||||||
containerBehavior: {
|
|
||||||
textFlow: rest.containerBehavior?.textFlow ?? "growing",
|
|
||||||
margin: rest.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "embeddable":
|
case "embeddable":
|
||||||
@@ -414,7 +408,7 @@ export class API {
|
|||||||
text: opts?.label?.text || "sample-text",
|
text: opts?.label?.text || "sample-text",
|
||||||
width: 50,
|
width: 50,
|
||||||
height: 20,
|
height: 20,
|
||||||
fontSize: 16,
|
fontSize: FONT_SIZES.sm,
|
||||||
containerId: rectangle.id,
|
containerId: rectangle.id,
|
||||||
frameId:
|
frameId:
|
||||||
opts?.label?.frameId === undefined
|
opts?.label?.frameId === undefined
|
||||||
@@ -472,7 +466,7 @@ export class API {
|
|||||||
static readFile = async <T extends "utf8" | null>(
|
static readFile = async <T extends "utf8" | null>(
|
||||||
filepath: string,
|
filepath: string,
|
||||||
encoding?: T,
|
encoding?: T,
|
||||||
): Promise<T extends "utf8" ? string : Buffer> => {
|
): Promise<T extends "utf8" ? string : ArrayBuffer> => {
|
||||||
filepath = path.isAbsolute(filepath)
|
filepath = path.isAbsolute(filepath)
|
||||||
? filepath
|
? filepath
|
||||||
: path.resolve(path.join(__dirname, "../", filepath));
|
: path.resolve(path.join(__dirname, "../", filepath));
|
||||||
|
|||||||
@@ -315,6 +315,7 @@ describe("history", () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
mouse.downAt(0, 0);
|
mouse.downAt(0, 0);
|
||||||
|
mouse.moveTo(25, 25);
|
||||||
mouse.moveTo(50, 50);
|
mouse.moveTo(50, 50);
|
||||||
mouse.upAt(50, 50);
|
mouse.upAt(50, 50);
|
||||||
expect(API.getUndoStack().length).toBe(3);
|
expect(API.getUndoStack().length).toBe(3);
|
||||||
|
|||||||
@@ -467,6 +467,7 @@ describe("regression tests", () => {
|
|||||||
|
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
mouse.down();
|
mouse.down();
|
||||||
|
mouse.move(-1000, -1000);
|
||||||
mouse.restorePosition(...end);
|
mouse.restorePosition(...end);
|
||||||
mouse.up();
|
mouse.up();
|
||||||
|
|
||||||
@@ -517,6 +518,7 @@ describe("regression tests", () => {
|
|||||||
|
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
mouse.down();
|
mouse.down();
|
||||||
|
mouse.move(-1000, -1000);
|
||||||
mouse.restorePosition(...end);
|
mouse.restorePosition(...end);
|
||||||
mouse.up();
|
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.moveTo(-10, -10); // the NW resizing handle is at [0, 0], so moving further
|
||||||
mouse.down();
|
mouse.down();
|
||||||
|
mouse.move(-1000, -1000);
|
||||||
mouse.restorePosition(...end);
|
mouse.restorePosition(...end);
|
||||||
mouse.up();
|
mouse.up();
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ describe("box-selection", () => {
|
|||||||
API.setElements([rect1, rect2]);
|
API.setElements([rect1, rect2]);
|
||||||
|
|
||||||
mouse.downAt(175, -20);
|
mouse.downAt(175, -20);
|
||||||
|
mouse.move(-1000, -1000);
|
||||||
mouse.moveTo(85, 70);
|
mouse.moveTo(85, 70);
|
||||||
mouse.up();
|
mouse.up();
|
||||||
|
|
||||||
@@ -72,6 +73,7 @@ describe("box-selection", () => {
|
|||||||
|
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
mouse.downAt(75, -20);
|
mouse.downAt(75, -20);
|
||||||
|
mouse.move(-1000, -1000);
|
||||||
mouse.moveTo(-15, 70);
|
mouse.moveTo(-15, 70);
|
||||||
mouse.up();
|
mouse.up();
|
||||||
});
|
});
|
||||||
@@ -93,6 +95,7 @@ describe("box-selection", () => {
|
|||||||
API.setElements([rect1]);
|
API.setElements([rect1]);
|
||||||
|
|
||||||
mouse.downAt(75, -20);
|
mouse.downAt(75, -20);
|
||||||
|
mouse.move(-1000, -1000);
|
||||||
mouse.moveTo(-15, 70);
|
mouse.moveTo(-15, 70);
|
||||||
|
|
||||||
assertSelectedElements([rect1.id]);
|
assertSelectedElements([rect1.id]);
|
||||||
@@ -138,6 +141,7 @@ describe("inner box-selection", () => {
|
|||||||
API.setElements([rect1, rect2, rect3]);
|
API.setElements([rect1, rect2, rect3]);
|
||||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
mouse.downAt(40, 40);
|
mouse.downAt(40, 40);
|
||||||
|
mouse.move(-1000, -1000);
|
||||||
mouse.moveTo(290, 290);
|
mouse.moveTo(290, 290);
|
||||||
mouse.up();
|
mouse.up();
|
||||||
|
|
||||||
@@ -175,6 +179,7 @@ describe("inner box-selection", () => {
|
|||||||
|
|
||||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
mouse.downAt(40, 40);
|
mouse.downAt(40, 40);
|
||||||
|
mouse.move(-1000, -1000);
|
||||||
mouse.moveTo(rect2.x + rect2.width + 10, rect2.y + rect2.height + 10);
|
mouse.moveTo(rect2.x + rect2.width + 10, rect2.y + rect2.height + 10);
|
||||||
mouse.up();
|
mouse.up();
|
||||||
|
|
||||||
@@ -212,6 +217,7 @@ describe("inner box-selection", () => {
|
|||||||
API.setElements([rect1, rect2, rect3]);
|
API.setElements([rect1, rect2, rect3]);
|
||||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
mouse.downAt(rect2.x - 20, rect2.y - 20);
|
mouse.downAt(rect2.x - 20, rect2.y - 20);
|
||||||
|
mouse.move(-1000, -1000);
|
||||||
mouse.moveTo(rect2.x + rect2.width + 10, rect2.y + rect2.height + 10);
|
mouse.moveTo(rect2.x + rect2.width + 10, rect2.y + rect2.height + 10);
|
||||||
assertSelectedElements([rect2.id, rect3.id]);
|
assertSelectedElements([rect2.id, rect3.id]);
|
||||||
expect(h.state.selectedGroupIds).toEqual({ A: true });
|
expect(h.state.selectedGroupIds).toEqual({ A: true });
|
||||||
@@ -253,9 +259,10 @@ describe("selection element", () => {
|
|||||||
|
|
||||||
const canvas = container.querySelector("canvas.interactive")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
|
fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
|
||||||
|
fireEvent.pointerMove(canvas, { clientX: -1000, clientY: -1000 });
|
||||||
fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 });
|
fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 });
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(4);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(3);
|
expect(renderStaticScene).toHaveBeenCalledTimes(3);
|
||||||
const selectionElement = h.state.selectionElement!;
|
const selectionElement = h.state.selectionElement!;
|
||||||
expect(selectionElement).not.toBeNull();
|
expect(selectionElement).not.toBeNull();
|
||||||
@@ -275,10 +282,11 @@ describe("selection element", () => {
|
|||||||
|
|
||||||
const canvas = container.querySelector("canvas.interactive")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
|
fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
|
||||||
|
fireEvent.pointerMove(canvas, { clientX: -1000, clientY: -1000 });
|
||||||
fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 });
|
fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(3);
|
expect(renderStaticScene).toHaveBeenCalledTimes(3);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
});
|
});
|
||||||
@@ -303,6 +311,7 @@ describe("select single element on the scene", () => {
|
|||||||
const tool = getByToolName("rectangle");
|
const tool = getByToolName("rectangle");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
|
fireEvent.pointerMove(canvas, { clientX: -1000, clientY: -1000 });
|
||||||
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
fireEvent.keyDown(document, {
|
fireEvent.keyDown(document, {
|
||||||
@@ -335,6 +344,7 @@ describe("select single element on the scene", () => {
|
|||||||
const tool = getByToolName("diamond");
|
const tool = getByToolName("diamond");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
|
fireEvent.pointerMove(canvas, { clientX: -1000, clientY: -1000 });
|
||||||
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
fireEvent.keyDown(document, {
|
fireEvent.keyDown(document, {
|
||||||
@@ -367,6 +377,7 @@ describe("select single element on the scene", () => {
|
|||||||
const tool = getByToolName("ellipse");
|
const tool = getByToolName("ellipse");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
|
fireEvent.pointerMove(canvas, { clientX: -1000, clientY: -1000 });
|
||||||
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
fireEvent.keyDown(document, {
|
fireEvent.keyDown(document, {
|
||||||
@@ -399,6 +410,7 @@ describe("select single element on the scene", () => {
|
|||||||
const tool = getByToolName("arrow");
|
const tool = getByToolName("arrow");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
|
fireEvent.pointerMove(canvas, { clientX: -1000, clientY: -1000 });
|
||||||
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
fireEvent.keyDown(document, {
|
fireEvent.keyDown(document, {
|
||||||
@@ -425,8 +437,8 @@ describe("select single element on the scene", () => {
|
|||||||
fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 });
|
fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(10);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
expect(renderStaticScene).toHaveBeenCalledTimes(8);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||||
@@ -443,6 +455,7 @@ describe("select single element on the scene", () => {
|
|||||||
const tool = getByToolName("line");
|
const tool = getByToolName("line");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
|
fireEvent.pointerMove(canvas, { clientX: -1000, clientY: -1000 });
|
||||||
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
fireEvent.keyDown(document, {
|
fireEvent.keyDown(document, {
|
||||||
@@ -469,8 +482,8 @@ describe("select single element on the scene", () => {
|
|||||||
fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 });
|
fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(10);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
expect(renderStaticScene).toHaveBeenCalledTimes(8);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||||
@@ -525,6 +538,7 @@ describe("selectedElementIds stability", () => {
|
|||||||
expect(h.state.selectedElementIds).toBe(selectedElementIds_1);
|
expect(h.state.selectedElementIds).toBe(selectedElementIds_1);
|
||||||
|
|
||||||
mouse.downAt(-50, -50);
|
mouse.downAt(-50, -50);
|
||||||
|
mouse.move(-1000, -1000);
|
||||||
mouse.moveTo(50, 50);
|
mouse.moveTo(50, 50);
|
||||||
|
|
||||||
const selectedElementIds_2 = h.state.selectedElementIds;
|
const selectedElementIds_2 = h.state.selectedElementIds;
|
||||||
|
|||||||
@@ -348,7 +348,6 @@ export interface AppState {
|
|||||||
currentHoveredFontFamily: FontFamilyValues | null;
|
currentHoveredFontFamily: FontFamilyValues | null;
|
||||||
currentItemRoundness: StrokeRoundness;
|
currentItemRoundness: StrokeRoundness;
|
||||||
currentItemArrowType: "sharp" | "round" | "elbow";
|
currentItemArrowType: "sharp" | "round" | "elbow";
|
||||||
currentItemContainerBehavior: ExcalidrawElement["containerBehavior"];
|
|
||||||
viewBackgroundColor: string;
|
viewBackgroundColor: string;
|
||||||
scrollX: number;
|
scrollX: number;
|
||||||
scrollY: number;
|
scrollY: number;
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
getFontFamilyString,
|
getFontFamilyString,
|
||||||
isTestEnv,
|
isTestEnv,
|
||||||
MIME_TYPES,
|
MIME_TYPES,
|
||||||
BOUND_TEXT_PADDING,
|
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -37,7 +36,6 @@ import {
|
|||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "@excalidraw/element";
|
} from "@excalidraw/element";
|
||||||
import { computeStickyNoteFontSize } from "@excalidraw/element";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
@@ -132,8 +130,6 @@ export const textWysiwyg = ({
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
let stickyNoteInitialFontSize: number | null = null;
|
|
||||||
|
|
||||||
const updateWysiwygStyle = () => {
|
const updateWysiwygStyle = () => {
|
||||||
const appState = app.state;
|
const appState = app.state;
|
||||||
const updatedTextElement = app.scene.getElement<ExcalidrawTextElement>(id);
|
const updatedTextElement = app.scene.getElement<ExcalidrawTextElement>(id);
|
||||||
@@ -161,42 +157,65 @@ export const textWysiwyg = ({
|
|||||||
let maxHeight = updatedTextElement.height;
|
let maxHeight = updatedTextElement.height;
|
||||||
|
|
||||||
if (container && updatedTextElement.containerId) {
|
if (container && updatedTextElement.containerId) {
|
||||||
if ((container as any).containerBehavior?.textFlow === "fixed") {
|
if (isArrowElement(container)) {
|
||||||
if (stickyNoteInitialFontSize == null) {
|
const boundTextCoords =
|
||||||
stickyNoteInitialFontSize = updatedTextElement.fontSize;
|
LinearElementEditor.getBoundTextElementPosition(
|
||||||
}
|
container,
|
||||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
updatedTextElement as ExcalidrawTextElementWithContainer,
|
||||||
maxWidth = getBoundTextMaxWidth(container, updatedTextElement);
|
elementsMap,
|
||||||
maxHeight = getBoundTextMaxHeight(
|
);
|
||||||
container,
|
coordX = boundTextCoords.x;
|
||||||
updatedTextElement as ExcalidrawTextElementWithContainer,
|
coordY = boundTextCoords.y;
|
||||||
);
|
}
|
||||||
const {
|
const propertiesUpdated = textPropertiesUpdated(
|
||||||
fontSize,
|
updatedTextElement,
|
||||||
width: fittedW,
|
editable,
|
||||||
height: fittedH,
|
);
|
||||||
wrappedText,
|
|
||||||
} = computeStickyNoteFontSize(
|
|
||||||
editable.value,
|
|
||||||
updatedTextElement,
|
|
||||||
container,
|
|
||||||
stickyNoteInitialFontSize,
|
|
||||||
);
|
|
||||||
const needsUpdate =
|
|
||||||
fontSize !== updatedTextElement.fontSize ||
|
|
||||||
fittedW !== updatedTextElement.width ||
|
|
||||||
fittedH !== updatedTextElement.height ||
|
|
||||||
wrappedText !== updatedTextElement.text;
|
|
||||||
|
|
||||||
if (needsUpdate) {
|
let originalContainerData;
|
||||||
app.scene.mutateElement(updatedTextElement, {
|
if (propertiesUpdated) {
|
||||||
fontSize,
|
originalContainerData = updateOriginalContainerCache(
|
||||||
width: fittedW,
|
container.id,
|
||||||
height: fittedH,
|
container.height,
|
||||||
text: wrappedText,
|
);
|
||||||
});
|
} else {
|
||||||
|
originalContainerData = originalContainerCache[container.id];
|
||||||
|
if (!originalContainerData) {
|
||||||
|
originalContainerData = updateOriginalContainerCache(
|
||||||
|
container.id,
|
||||||
|
container.height,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
maxWidth = getBoundTextMaxWidth(container, updatedTextElement);
|
||||||
|
maxHeight = getBoundTextMaxHeight(
|
||||||
|
container,
|
||||||
|
updatedTextElement as ExcalidrawTextElementWithContainer,
|
||||||
|
);
|
||||||
|
|
||||||
|
// autogrow container height if text exceeds
|
||||||
|
if (!isArrowElement(container) && height > maxHeight) {
|
||||||
|
const targetContainerHeight = computeContainerDimensionForBoundText(
|
||||||
|
height,
|
||||||
|
container.type,
|
||||||
|
);
|
||||||
|
|
||||||
|
app.scene.mutateElement(container, { height: targetContainerHeight });
|
||||||
|
return;
|
||||||
|
} else if (
|
||||||
|
// autoshrink container height until original container height
|
||||||
|
// is reached when text is removed
|
||||||
|
!isArrowElement(container) &&
|
||||||
|
container.height > originalContainerData.height &&
|
||||||
|
height < maxHeight
|
||||||
|
) {
|
||||||
|
const targetContainerHeight = computeContainerDimensionForBoundText(
|
||||||
|
height,
|
||||||
|
container.type,
|
||||||
|
);
|
||||||
|
app.scene.mutateElement(container, { height: targetContainerHeight });
|
||||||
|
} else {
|
||||||
const { x, y } = computeBoundTextPosition(
|
const { x, y } = computeBoundTextPosition(
|
||||||
container,
|
container,
|
||||||
updatedTextElement as ExcalidrawTextElementWithContainer,
|
updatedTextElement as ExcalidrawTextElementWithContainer,
|
||||||
@@ -204,87 +223,6 @@ export const textWysiwyg = ({
|
|||||||
);
|
);
|
||||||
coordX = x;
|
coordX = x;
|
||||||
coordY = y;
|
coordY = y;
|
||||||
width = fittedW;
|
|
||||||
height = fittedH;
|
|
||||||
} else {
|
|
||||||
if (isArrowElement(container)) {
|
|
||||||
const boundTextCoords =
|
|
||||||
LinearElementEditor.getBoundTextElementPosition(
|
|
||||||
container,
|
|
||||||
updatedTextElement as ExcalidrawTextElementWithContainer,
|
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
coordX = boundTextCoords.x;
|
|
||||||
coordY = boundTextCoords.y;
|
|
||||||
}
|
|
||||||
const propertiesUpdated = textPropertiesUpdated(
|
|
||||||
updatedTextElement,
|
|
||||||
editable,
|
|
||||||
);
|
|
||||||
|
|
||||||
let originalContainerData;
|
|
||||||
if (propertiesUpdated) {
|
|
||||||
originalContainerData = updateOriginalContainerCache(
|
|
||||||
container.id,
|
|
||||||
container.height,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
originalContainerData = originalContainerCache[container.id];
|
|
||||||
if (!originalContainerData) {
|
|
||||||
originalContainerData = updateOriginalContainerCache(
|
|
||||||
container.id,
|
|
||||||
container.height,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
maxWidth = getBoundTextMaxWidth(container, updatedTextElement);
|
|
||||||
maxHeight = getBoundTextMaxHeight(
|
|
||||||
container,
|
|
||||||
updatedTextElement as ExcalidrawTextElementWithContainer,
|
|
||||||
);
|
|
||||||
|
|
||||||
// autogrow / autoshrink only for non-sticky behaviors
|
|
||||||
if ((container as any).containerBehavior?.textFlow !== "fixed") {
|
|
||||||
// autogrow container height if text exceeds
|
|
||||||
if (!isArrowElement(container) && height > maxHeight) {
|
|
||||||
const targetContainerHeight =
|
|
||||||
computeContainerDimensionForBoundText(
|
|
||||||
height,
|
|
||||||
container.type,
|
|
||||||
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
|
|
||||||
);
|
|
||||||
|
|
||||||
app.scene.mutateElement(container, {
|
|
||||||
height: targetContainerHeight,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
} else if (
|
|
||||||
// autoshrink container height until original container height
|
|
||||||
// is reached when text is removed
|
|
||||||
!isArrowElement(container) &&
|
|
||||||
container.height > originalContainerData.height &&
|
|
||||||
height < maxHeight
|
|
||||||
) {
|
|
||||||
const targetContainerHeight =
|
|
||||||
computeContainerDimensionForBoundText(
|
|
||||||
height,
|
|
||||||
container.type,
|
|
||||||
container.containerBehavior?.margin ?? BOUND_TEXT_PADDING,
|
|
||||||
);
|
|
||||||
app.scene.mutateElement(container, {
|
|
||||||
height: targetContainerHeight,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const { x, y } = computeBoundTextPosition(
|
|
||||||
container,
|
|
||||||
updatedTextElement as ExcalidrawTextElementWithContainer,
|
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
coordX = x;
|
|
||||||
coordY = y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const [viewportX, viewportY] = getViewportCoords(coordX, coordY);
|
const [viewportX, viewportY] = getViewportCoords(coordX, coordY);
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"fonteditor-core": "2.4.0",
|
"fonteditor-core": "2.4.0",
|
||||||
"typescript": "4.9.4",
|
"typescript": "5.9.3",
|
||||||
"wawoff2": "2.0.1",
|
"wawoff2": "2.0.1",
|
||||||
"which": "4.0.0"
|
"which": "4.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
type LocalPoint,
|
type LocalPoint,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import type { Bounds } from "@excalidraw/element";
|
import type { Bounds } from "@excalidraw/common";
|
||||||
|
|
||||||
export type LineSegment<P extends LocalPoint | GlobalPoint> = [P, P];
|
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 { getElementBounds } from "@excalidraw/element";
|
||||||
import {
|
import {
|
||||||
isArrowElement,
|
isArrowElement,
|
||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
rangeInclusive,
|
rangeInclusive,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import type { Bounds } from "@excalidraw/element";
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawFreeDrawElement,
|
ExcalidrawFreeDrawElement,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
|
|
||||||
import type { Bounds } from "@excalidraw/element";
|
import type { Bounds } from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
elementPartiallyOverlapsWithOrContainsBBox,
|
elementPartiallyOverlapsWithOrContainsBBox,
|
||||||
|
|||||||
@@ -9290,10 +9290,10 @@ typed-array-length@^1.0.7:
|
|||||||
possible-typed-array-names "^1.0.0"
|
possible-typed-array-names "^1.0.0"
|
||||||
reflect.getprototypeof "^1.0.6"
|
reflect.getprototypeof "^1.0.6"
|
||||||
|
|
||||||
typescript@4.9.4:
|
typescript@5.9.3:
|
||||||
version "4.9.4"
|
version "5.9.3"
|
||||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78"
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
|
||||||
integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==
|
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
|
||||||
|
|
||||||
typescript@^5:
|
typescript@^5:
|
||||||
version "5.8.2"
|
version "5.8.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user