Compare commits
71 Commits
master
...
mtolmacs/f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dba41c4116 | ||
|
|
83fa9099f6 | ||
|
|
97fa922060 | ||
|
|
b8d1b8a5bd | ||
|
|
50e58abfd3 | ||
|
|
43816eb62d | ||
|
|
d6e3839d31 | ||
|
|
e0dd29aa36 | ||
|
|
f0494ced4c | ||
|
|
345e3f68f1 | ||
|
|
5b77409eff | ||
|
|
6398d14f3f | ||
|
|
32526c4d4a | ||
|
|
f2f5168355 | ||
|
|
55115d2ee4 | ||
|
|
acc1241015 | ||
|
|
e67338bff0 | ||
|
|
5a350a17c0 | ||
|
|
073f47d253 | ||
|
|
6c2f5dbd81 | ||
|
|
4e7b399927 | ||
|
|
7172006d1b | ||
|
|
b1006e2bfd | ||
|
|
4d8a1b29f6 | ||
|
|
f6978ae162 | ||
|
|
b789308798 | ||
|
|
ef2bde0d03 | ||
|
|
80706f733b | ||
|
|
737f6e08c1 | ||
|
|
c4874e9dd9 | ||
|
|
45b7cfc14b | ||
|
|
65a105e30a | ||
|
|
ee6f4d9ce5 | ||
|
|
7ae4d3aab5 | ||
|
|
017b36aeae | ||
|
|
3ab8f67bc6 | ||
|
|
fb3fe09226 | ||
|
|
e5c7a6304e | ||
|
|
434ed03f1e | ||
|
|
d73e273e63 | ||
|
|
8d7af92719 | ||
|
|
9ac0f8231c | ||
|
|
d4680df3d9 | ||
|
|
5830d518d4 | ||
|
|
b23768719d | ||
|
|
fce13ccefd | ||
|
|
35c986cbef | ||
|
|
a06b828ed2 | ||
|
|
7703cc2597 | ||
|
|
433774e892 | ||
|
|
be56e84596 | ||
|
|
eb9efc261a | ||
|
|
b01eea9eb4 | ||
|
|
109ff756f5 | ||
|
|
6ea0102b0a | ||
|
|
8a3ba853ab | ||
|
|
bcf3127fe5 | ||
|
|
5a62499e95 | ||
|
|
f8b8c0e95c | ||
|
|
364f0be815 | ||
|
|
10d38a8539 | ||
|
|
67fff43b92 | ||
|
|
62d7740c94 | ||
|
|
4f43399951 | ||
|
|
8dae900bbb | ||
|
|
9a49c8e448 | ||
|
|
8d77f1daf5 | ||
|
|
405d37e158 | ||
|
|
cba5d01460 | ||
|
|
aa7351f649 | ||
|
|
4438137a57 |
@ -662,8 +662,8 @@ const ExcalidrawWrapper = () => {
|
|||||||
debugRenderer(
|
debugRenderer(
|
||||||
debugCanvasRef.current,
|
debugCanvasRef.current,
|
||||||
appState,
|
appState,
|
||||||
|
elements,
|
||||||
window.devicePixelRatio,
|
window.devicePixelRatio,
|
||||||
() => forceRefresh((prev) => !prev),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -8,9 +8,15 @@ import {
|
|||||||
getNormalizedCanvasDimensions,
|
getNormalizedCanvasDimensions,
|
||||||
} from "@excalidraw/excalidraw/renderer/helpers";
|
} from "@excalidraw/excalidraw/renderer/helpers";
|
||||||
import { type AppState } from "@excalidraw/excalidraw/types";
|
import { type AppState } from "@excalidraw/excalidraw/types";
|
||||||
import { throttleRAF } from "@excalidraw/common";
|
import { arrayToMap, throttleRAF } from "@excalidraw/common";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getGlobalFixedPointForBindableElement,
|
||||||
|
isArrowElement,
|
||||||
|
isBindableElement,
|
||||||
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isLineSegment,
|
isLineSegment,
|
||||||
type GlobalPoint,
|
type GlobalPoint,
|
||||||
@ -21,8 +27,14 @@ import { isCurve } from "@excalidraw/math/curve";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import type { Curve } from "@excalidraw/math";
|
import type { Curve } from "@excalidraw/math";
|
||||||
|
import type { DebugElement } from "@excalidraw/common";
|
||||||
import type { DebugElement } from "@excalidraw/utils/visualdebug";
|
import type {
|
||||||
|
ElementsMap,
|
||||||
|
ExcalidrawArrowElement,
|
||||||
|
ExcalidrawBindableElement,
|
||||||
|
FixedPointBinding,
|
||||||
|
OrderedExcalidrawElement,
|
||||||
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { STORAGE_KEYS } from "../app_constants";
|
import { STORAGE_KEYS } from "../app_constants";
|
||||||
|
|
||||||
@ -75,6 +87,176 @@ const renderOrigin = (context: CanvasRenderingContext2D, zoom: number) => {
|
|||||||
context.save();
|
context.save();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _renderBinding = (
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
binding: FixedPointBinding,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
zoom: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
color: string,
|
||||||
|
) => {
|
||||||
|
if (!binding.fixedPoint) {
|
||||||
|
console.warn("Binding must have a fixedPoint");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bindable = elementsMap.get(
|
||||||
|
binding.elementId,
|
||||||
|
) as ExcalidrawBindableElement;
|
||||||
|
const [x, y] = getGlobalFixedPointForBindableElement(
|
||||||
|
binding.fixedPoint,
|
||||||
|
bindable,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
context.save();
|
||||||
|
context.strokeStyle = color;
|
||||||
|
context.lineWidth = 1;
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(x * zoom, y * zoom);
|
||||||
|
context.bezierCurveTo(
|
||||||
|
x * zoom - width,
|
||||||
|
y * zoom - height,
|
||||||
|
x * zoom - width,
|
||||||
|
y * zoom + height,
|
||||||
|
x * zoom,
|
||||||
|
y * zoom,
|
||||||
|
);
|
||||||
|
context.stroke();
|
||||||
|
context.restore();
|
||||||
|
};
|
||||||
|
|
||||||
|
const _renderBindableBinding = (
|
||||||
|
binding: FixedPointBinding,
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
zoom: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
color: string,
|
||||||
|
) => {
|
||||||
|
const bindable = elementsMap.get(
|
||||||
|
binding.elementId,
|
||||||
|
) as ExcalidrawBindableElement;
|
||||||
|
if (!binding.fixedPoint) {
|
||||||
|
console.warn("Binding must have a fixedPoint");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [x, y] = getGlobalFixedPointForBindableElement(
|
||||||
|
binding.fixedPoint,
|
||||||
|
bindable,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
context.save();
|
||||||
|
context.strokeStyle = color;
|
||||||
|
context.lineWidth = 1;
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(x * zoom, y * zoom);
|
||||||
|
context.bezierCurveTo(
|
||||||
|
x * zoom + width,
|
||||||
|
y * zoom + height,
|
||||||
|
x * zoom + width,
|
||||||
|
y * zoom - height,
|
||||||
|
x * zoom,
|
||||||
|
y * zoom,
|
||||||
|
);
|
||||||
|
context.stroke();
|
||||||
|
context.restore();
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderBindings = (
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
elements: readonly OrderedExcalidrawElement[],
|
||||||
|
zoom: number,
|
||||||
|
) => {
|
||||||
|
const elementsMap = arrayToMap(elements);
|
||||||
|
const dim = 16;
|
||||||
|
elements.forEach((element) => {
|
||||||
|
if (element.isDeleted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isArrowElement(element)) {
|
||||||
|
if (element.startBinding) {
|
||||||
|
if (
|
||||||
|
!elementsMap
|
||||||
|
.get(element.startBinding.elementId)
|
||||||
|
?.boundElements?.find((e) => e.id === element.id)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderBinding(
|
||||||
|
context,
|
||||||
|
element.startBinding,
|
||||||
|
elementsMap,
|
||||||
|
zoom,
|
||||||
|
dim,
|
||||||
|
dim,
|
||||||
|
"red",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.endBinding) {
|
||||||
|
if (
|
||||||
|
!elementsMap
|
||||||
|
.get(element.endBinding.elementId)
|
||||||
|
?.boundElements?.find((e) => e.id === element.id)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_renderBinding(
|
||||||
|
context,
|
||||||
|
element.endBinding,
|
||||||
|
elementsMap,
|
||||||
|
zoom,
|
||||||
|
dim,
|
||||||
|
dim,
|
||||||
|
"red",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBindableElement(element) && element.boundElements?.length) {
|
||||||
|
element.boundElements.forEach((boundElement) => {
|
||||||
|
if (boundElement.type !== "arrow") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrow = elementsMap.get(
|
||||||
|
boundElement.id,
|
||||||
|
) as ExcalidrawArrowElement;
|
||||||
|
|
||||||
|
if (arrow && arrow.startBinding?.elementId === element.id) {
|
||||||
|
_renderBindableBinding(
|
||||||
|
arrow.startBinding,
|
||||||
|
context,
|
||||||
|
elementsMap,
|
||||||
|
zoom,
|
||||||
|
dim,
|
||||||
|
dim,
|
||||||
|
"green",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (arrow && arrow.endBinding?.elementId === element.id) {
|
||||||
|
_renderBindableBinding(
|
||||||
|
arrow.endBinding,
|
||||||
|
context,
|
||||||
|
elementsMap,
|
||||||
|
zoom,
|
||||||
|
dim,
|
||||||
|
dim,
|
||||||
|
"green",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const render = (
|
const render = (
|
||||||
frame: DebugElement[],
|
frame: DebugElement[],
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
@ -107,8 +289,8 @@ const render = (
|
|||||||
const _debugRenderer = (
|
const _debugRenderer = (
|
||||||
canvas: HTMLCanvasElement,
|
canvas: HTMLCanvasElement,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
|
elements: readonly OrderedExcalidrawElement[],
|
||||||
scale: number,
|
scale: number,
|
||||||
refresh: () => void,
|
|
||||||
) => {
|
) => {
|
||||||
const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions(
|
const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions(
|
||||||
canvas,
|
canvas,
|
||||||
@ -131,6 +313,7 @@ const _debugRenderer = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
renderOrigin(context, appState.zoom.value);
|
renderOrigin(context, appState.zoom.value);
|
||||||
|
renderBindings(context, elements, appState.zoom.value);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
window.visualDebug?.currentFrame &&
|
window.visualDebug?.currentFrame &&
|
||||||
@ -182,10 +365,10 @@ export const debugRenderer = throttleRAF(
|
|||||||
(
|
(
|
||||||
canvas: HTMLCanvasElement,
|
canvas: HTMLCanvasElement,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
|
elements: readonly OrderedExcalidrawElement[],
|
||||||
scale: number,
|
scale: number,
|
||||||
refresh: () => void,
|
|
||||||
) => {
|
) => {
|
||||||
_debugRenderer(canvas, appState, scale, refresh);
|
_debugRenderer(canvas, appState, elements, scale);
|
||||||
},
|
},
|
||||||
{ trailing: true },
|
{ trailing: true },
|
||||||
);
|
);
|
||||||
|
|||||||
@ -16,7 +16,6 @@ import {
|
|||||||
DEFAULT_SIDEBAR,
|
DEFAULT_SIDEBAR,
|
||||||
debounce,
|
debounce,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
import { clearElementsForLocalStorage } from "@excalidraw/element";
|
|
||||||
import {
|
import {
|
||||||
createStore,
|
createStore,
|
||||||
entries,
|
entries,
|
||||||
@ -81,7 +80,7 @@ const saveDataStateToLocalStorage = (
|
|||||||
|
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
|
STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
|
||||||
JSON.stringify(clearElementsForLocalStorage(elements)),
|
JSON.stringify(elements),
|
||||||
);
|
);
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
|
STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import {
|
|||||||
clearAppStateForLocalStorage,
|
clearAppStateForLocalStorage,
|
||||||
getDefaultAppState,
|
getDefaultAppState,
|
||||||
} from "@excalidraw/excalidraw/appState";
|
} from "@excalidraw/excalidraw/appState";
|
||||||
import { clearElementsForLocalStorage } from "@excalidraw/element";
|
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||||
@ -50,7 +49,7 @@ export const importFromLocalStorage = () => {
|
|||||||
let elements: ExcalidrawElement[] = [];
|
let elements: ExcalidrawElement[] = [];
|
||||||
if (savedElements) {
|
if (savedElements) {
|
||||||
try {
|
try {
|
||||||
elements = clearElementsForLocalStorage(JSON.parse(savedElements));
|
elements = JSON.parse(savedElements);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
// Do nothing because elements array is already empty
|
// Do nothing because elements array is already empty
|
||||||
|
|||||||
@ -539,3 +539,5 @@ export enum UserIdleState {
|
|||||||
export const LINE_POLYGON_POINT_MERGE_DISTANCE = 20;
|
export const LINE_POLYGON_POINT_MERGE_DISTANCE = 20;
|
||||||
|
|
||||||
export const DOUBLE_TAP_POSITION_THRESHOLD = 35;
|
export const DOUBLE_TAP_POSITION_THRESHOLD = 35;
|
||||||
|
|
||||||
|
export const BIND_MODE_TIMEOUT = 700; // ms
|
||||||
|
|||||||
@ -10,3 +10,4 @@ export * from "./random";
|
|||||||
export * from "./url";
|
export * from "./url";
|
||||||
export * from "./utils";
|
export * from "./utils";
|
||||||
export * from "./emitter";
|
export * from "./emitter";
|
||||||
|
export * from "./visualdebug";
|
||||||
|
|||||||
@ -1,10 +1,6 @@
|
|||||||
import { average } from "@excalidraw/math";
|
import { average } from "@excalidraw/math";
|
||||||
|
|
||||||
import type {
|
import type { FontFamilyValues, FontString } from "@excalidraw/element/types";
|
||||||
ExcalidrawBindableElement,
|
|
||||||
FontFamilyValues,
|
|
||||||
FontString,
|
|
||||||
} from "@excalidraw/element/types";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ActiveTool,
|
ActiveTool,
|
||||||
@ -568,9 +564,6 @@ export const isTransparent = (color: string) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isBindingFallthroughEnabled = (el: ExcalidrawBindableElement) =>
|
|
||||||
el.fillStyle !== "solid" || isTransparent(el.backgroundColor);
|
|
||||||
|
|
||||||
export type ResolvablePromise<T> = Promise<T> & {
|
export type ResolvablePromise<T> = Promise<T> & {
|
||||||
resolve: [T] extends [undefined]
|
resolve: [T] extends [undefined]
|
||||||
? (value?: MaybePromise<Awaited<T>>) => void
|
? (value?: MaybePromise<Awaited<T>>) => void
|
||||||
|
|||||||
@ -63,6 +63,8 @@ export const debugDrawLine = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const testDebug = () => {};
|
||||||
|
|
||||||
export const debugDrawPoint = (
|
export const debugDrawPoint = (
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
opts?: {
|
opts?: {
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
|||||||
import { isTransparent } from "@excalidraw/common";
|
import { invariant, isTransparent } from "@excalidraw/common";
|
||||||
import {
|
import {
|
||||||
curveIntersectLineSegment,
|
curveIntersectLineSegment,
|
||||||
isPointWithinBounds,
|
isPointWithinBounds,
|
||||||
@ -34,10 +34,13 @@ import {
|
|||||||
elementCenterPoint,
|
elementCenterPoint,
|
||||||
getCenterForBounds,
|
getCenterForBounds,
|
||||||
getCubicBezierCurveBound,
|
getCubicBezierCurveBound,
|
||||||
|
getDiamondPoints,
|
||||||
getElementBounds,
|
getElementBounds,
|
||||||
} from "./bounds";
|
} from "./bounds";
|
||||||
import {
|
import {
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
|
isBindableElement,
|
||||||
|
isFrameLikeElement,
|
||||||
isFreeDrawElement,
|
isFreeDrawElement,
|
||||||
isIframeLikeElement,
|
isIframeLikeElement,
|
||||||
isImageElement,
|
isImageElement,
|
||||||
@ -58,12 +61,17 @@ import { distanceToElement } from "./distance";
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
|
ExcalidrawBindableElement,
|
||||||
ExcalidrawDiamondElement,
|
ExcalidrawDiamondElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawEllipseElement,
|
ExcalidrawEllipseElement,
|
||||||
ExcalidrawFreeDrawElement,
|
ExcalidrawFreeDrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawRectanguloidElement,
|
ExcalidrawRectanguloidElement,
|
||||||
|
NonDeleted,
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
NonDeletedSceneElementsMap,
|
||||||
|
Ordered,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export const shouldTestInside = (element: ExcalidrawElement) => {
|
export const shouldTestInside = (element: ExcalidrawElement) => {
|
||||||
@ -94,6 +102,7 @@ export type HitTestArgs = {
|
|||||||
threshold: number;
|
threshold: number;
|
||||||
elementsMap: ElementsMap;
|
elementsMap: ElementsMap;
|
||||||
frameNameBound?: FrameNameBounds | null;
|
frameNameBound?: FrameNameBounds | null;
|
||||||
|
overrideShouldTestInside?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hitElementItself = ({
|
export const hitElementItself = ({
|
||||||
@ -102,6 +111,7 @@ export const hitElementItself = ({
|
|||||||
threshold,
|
threshold,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
frameNameBound = null,
|
frameNameBound = null,
|
||||||
|
overrideShouldTestInside = false,
|
||||||
}: HitTestArgs) => {
|
}: HitTestArgs) => {
|
||||||
// Hit test against a frame's name
|
// Hit test against a frame's name
|
||||||
const hitFrameName = frameNameBound
|
const hitFrameName = frameNameBound
|
||||||
@ -134,7 +144,9 @@ export const hitElementItself = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Do the precise (and relatively costly) hit test
|
// Do the precise (and relatively costly) hit test
|
||||||
const hitElement = shouldTestInside(element)
|
const hitElement = (
|
||||||
|
overrideShouldTestInside ? true : shouldTestInside(element)
|
||||||
|
)
|
||||||
? // Since `inShape` tests STRICTLY againt the insides of a shape
|
? // Since `inShape` tests STRICTLY againt the insides of a shape
|
||||||
// we would need `onShape` as well to include the "borders"
|
// we would need `onShape` as well to include the "borders"
|
||||||
isPointInElement(point, element, elementsMap) ||
|
isPointInElement(point, element, elementsMap) ||
|
||||||
@ -193,6 +205,102 @@ export const hitElementBoundText = (
|
|||||||
return isPointInElement(point, boundTextElement, elementsMap);
|
return isPointInElement(point, boundTextElement, elementsMap);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const bindingBorderTest = (
|
||||||
|
element: NonDeleted<ExcalidrawBindableElement>,
|
||||||
|
[x, y]: Readonly<GlobalPoint>,
|
||||||
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
|
tolerance: number = 0,
|
||||||
|
): boolean => {
|
||||||
|
const p = pointFrom<GlobalPoint>(x, y);
|
||||||
|
const shouldTestInside =
|
||||||
|
// disable fullshape snapping for frame elements so we
|
||||||
|
// can bind to frame children
|
||||||
|
!isFrameLikeElement(element);
|
||||||
|
|
||||||
|
// PERF: Run a cheap test to see if the binding element
|
||||||
|
// is even close to the element
|
||||||
|
const t = Math.max(1, tolerance);
|
||||||
|
const bounds = [x - t, y - t, x + t, y + t] as Bounds;
|
||||||
|
const elementBounds = getElementBounds(element, elementsMap);
|
||||||
|
if (!doBoundsIntersect(bounds, elementBounds)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do the intersection test against the element since it's close enough
|
||||||
|
const intersections = intersectElementWithLineSegment(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
lineSegment(elementCenterPoint(element, elementsMap), p),
|
||||||
|
);
|
||||||
|
const distance = distanceToElement(element, elementsMap, p);
|
||||||
|
|
||||||
|
return shouldTestInside
|
||||||
|
? intersections.length === 0 || distance <= tolerance
|
||||||
|
: intersections.length > 0 && distance <= t;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAllHoveredElementAtPoint = (
|
||||||
|
point: Readonly<GlobalPoint>,
|
||||||
|
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||||
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
|
toleranceFn?: (element: ExcalidrawBindableElement) => number,
|
||||||
|
): NonDeleted<ExcalidrawBindableElement>[] => {
|
||||||
|
const candidateElements: NonDeleted<ExcalidrawBindableElement>[] = [];
|
||||||
|
// We need to to hit testing from front (end of the array) to back (beginning of the array)
|
||||||
|
// because array is ordered from lower z-index to highest and we want element z-index
|
||||||
|
// with higher z-index
|
||||||
|
for (let index = elements.length - 1; index >= 0; --index) {
|
||||||
|
const element = elements[index];
|
||||||
|
|
||||||
|
invariant(
|
||||||
|
!element.isDeleted,
|
||||||
|
"Elements in the function parameter for getAllElementsAtPositionForBinding() should not contain deleted elements",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
isBindableElement(element, false) &&
|
||||||
|
bindingBorderTest(element, point, elementsMap, toleranceFn?.(element))
|
||||||
|
) {
|
||||||
|
candidateElements.push(element);
|
||||||
|
|
||||||
|
if (!isTransparent(element.backgroundColor)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidateElements;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getHoveredElementForBinding = (
|
||||||
|
point: Readonly<GlobalPoint>,
|
||||||
|
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||||
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
|
toleranceFn?: (element: ExcalidrawBindableElement) => number,
|
||||||
|
): NonDeleted<ExcalidrawBindableElement> | null => {
|
||||||
|
const candidateElements = getAllHoveredElementAtPoint(
|
||||||
|
point,
|
||||||
|
elements,
|
||||||
|
elementsMap,
|
||||||
|
toleranceFn,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!candidateElements || candidateElements.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidateElements.length === 1) {
|
||||||
|
return candidateElements[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer smaller shapes
|
||||||
|
return candidateElements
|
||||||
|
.sort(
|
||||||
|
(a, b) => b.width ** 2 + b.height ** 2 - (a.width ** 2 + a.height ** 2),
|
||||||
|
)
|
||||||
|
.pop() as NonDeleted<ExcalidrawBindableElement>;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Intersect a line with an element for binding test
|
* Intersect a line with an element for binding test
|
||||||
*
|
*
|
||||||
@ -554,3 +662,61 @@ export const isPointInElement = (
|
|||||||
|
|
||||||
return intersections.length % 2 === 1;
|
return intersections.length % 2 === 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isBindableElementInsideOtherBindable = (
|
||||||
|
innerElement: ExcalidrawBindableElement,
|
||||||
|
outerElement: ExcalidrawBindableElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
): boolean => {
|
||||||
|
// Get corner points of the inner element based on its type
|
||||||
|
const getCornerPoints = (
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
offset: number,
|
||||||
|
): GlobalPoint[] => {
|
||||||
|
const { x, y, width, height, angle } = element;
|
||||||
|
const center = elementCenterPoint(element, elementsMap);
|
||||||
|
|
||||||
|
if (element.type === "diamond") {
|
||||||
|
// Diamond has 4 corner points at the middle of each side
|
||||||
|
const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
|
||||||
|
getDiamondPoints(element);
|
||||||
|
const corners: GlobalPoint[] = [
|
||||||
|
pointFrom(x + topX, y + topY - offset), // top
|
||||||
|
pointFrom(x + rightX + offset, y + rightY), // right
|
||||||
|
pointFrom(x + bottomX, y + bottomY + offset), // bottom
|
||||||
|
pointFrom(x + leftX - offset, y + leftY), // left
|
||||||
|
];
|
||||||
|
return corners.map((corner) => pointRotateRads(corner, center, angle));
|
||||||
|
}
|
||||||
|
if (element.type === "ellipse") {
|
||||||
|
// For ellipse, test points at the extremes (top, right, bottom, left)
|
||||||
|
const cx = x + width / 2;
|
||||||
|
const cy = y + height / 2;
|
||||||
|
const rx = width / 2;
|
||||||
|
const ry = height / 2;
|
||||||
|
const corners: GlobalPoint[] = [
|
||||||
|
pointFrom(cx, cy - ry - offset), // top
|
||||||
|
pointFrom(cx + rx + offset, cy), // right
|
||||||
|
pointFrom(cx, cy + ry + offset), // bottom
|
||||||
|
pointFrom(cx - rx - offset, cy), // left
|
||||||
|
];
|
||||||
|
return corners.map((corner) => pointRotateRads(corner, center, angle));
|
||||||
|
}
|
||||||
|
// Rectangle and other rectangular shapes (image, text, etc.)
|
||||||
|
const corners: GlobalPoint[] = [
|
||||||
|
pointFrom(x - offset, y - offset), // top-left
|
||||||
|
pointFrom(x + width + offset, y - offset), // top-right
|
||||||
|
pointFrom(x + width + offset, y + height + offset), // bottom-right
|
||||||
|
pointFrom(x - offset, y + height + offset), // bottom-left
|
||||||
|
];
|
||||||
|
return corners.map((corner) => pointRotateRads(corner, center, angle));
|
||||||
|
};
|
||||||
|
|
||||||
|
const offset = (-1 * Math.max(innerElement.width, innerElement.height)) / 20; // 5% offset
|
||||||
|
const innerCorners = getCornerPoints(innerElement, offset);
|
||||||
|
|
||||||
|
// Check if all corner points of the inner element are inside the outer element
|
||||||
|
return innerCorners.every((corner) =>
|
||||||
|
isPointInElement(corner, outerElement, elementsMap),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import {
|
|||||||
TEXT_AUTOWRAP_THRESHOLD,
|
TEXT_AUTOWRAP_THRESHOLD,
|
||||||
getGridPoint,
|
getGridPoint,
|
||||||
getFontString,
|
getFontString,
|
||||||
|
DRAGGING_THRESHOLD,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@ -13,7 +14,7 @@ import type {
|
|||||||
|
|
||||||
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { updateBoundElements } from "./binding";
|
import { unbindBindingElement, updateBoundElements } from "./binding";
|
||||||
import { getCommonBounds } from "./bounds";
|
import { getCommonBounds } from "./bounds";
|
||||||
import { getPerfectElementSize } from "./sizeHelpers";
|
import { getPerfectElementSize } from "./sizeHelpers";
|
||||||
import { getBoundTextElement } from "./textElement";
|
import { getBoundTextElement } from "./textElement";
|
||||||
@ -102,9 +103,26 @@ export const dragSelectedElements = (
|
|||||||
gridSize,
|
gridSize,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const elementsToUpdateIds = new Set(
|
||||||
|
Array.from(elementsToUpdate, (el) => el.id),
|
||||||
|
);
|
||||||
|
|
||||||
elementsToUpdate.forEach((element) => {
|
elementsToUpdate.forEach((element) => {
|
||||||
updateElementCoords(pointerDownState, element, scene, adjustedOffset);
|
const isArrow = !isArrowElement(element);
|
||||||
|
const isStartBoundElementSelected =
|
||||||
|
isArrow ||
|
||||||
|
(element.startBinding
|
||||||
|
? elementsToUpdateIds.has(element.startBinding.elementId)
|
||||||
|
: false);
|
||||||
|
const isEndBoundElementSelected =
|
||||||
|
isArrow ||
|
||||||
|
(element.endBinding
|
||||||
|
? elementsToUpdateIds.has(element.endBinding.elementId)
|
||||||
|
: false);
|
||||||
|
|
||||||
if (!isArrowElement(element)) {
|
if (!isArrowElement(element)) {
|
||||||
|
updateElementCoords(pointerDownState, element, scene, adjustedOffset);
|
||||||
|
|
||||||
// skip arrow labels since we calculate its position during render
|
// skip arrow labels since we calculate its position during render
|
||||||
const textElement = getBoundTextElement(
|
const textElement = getBoundTextElement(
|
||||||
element,
|
element,
|
||||||
@ -121,6 +139,33 @@ export const dragSelectedElements = (
|
|||||||
updateBoundElements(element, scene, {
|
updateBoundElements(element, scene, {
|
||||||
simultaneouslyUpdated: Array.from(elementsToUpdate),
|
simultaneouslyUpdated: Array.from(elementsToUpdate),
|
||||||
});
|
});
|
||||||
|
} else if (
|
||||||
|
// NOTE: Add a little initial drag to the arrow dragging when the arrow
|
||||||
|
// is the single element being dragged to avoid accidentally unbinding
|
||||||
|
// the arrow when the user just wants to select it.
|
||||||
|
|
||||||
|
elementsToUpdate.size > 1 ||
|
||||||
|
Math.max(Math.abs(adjustedOffset.x), Math.abs(adjustedOffset.y)) >
|
||||||
|
DRAGGING_THRESHOLD ||
|
||||||
|
(!element.startBinding && !element.endBinding)
|
||||||
|
) {
|
||||||
|
updateElementCoords(pointerDownState, element, scene, adjustedOffset);
|
||||||
|
|
||||||
|
const shouldUnbindStart =
|
||||||
|
element.startBinding && !isStartBoundElementSelected;
|
||||||
|
const shouldUnbindEnd = element.endBinding && !isEndBoundElementSelected;
|
||||||
|
if (shouldUnbindStart || shouldUnbindEnd) {
|
||||||
|
// NOTE: Moving the bound arrow should unbind it, otherwise we would
|
||||||
|
// have weird situations, like 0 lenght arrow when the user moves
|
||||||
|
// the arrow outside a filled shape suddenly forcing the arrow start
|
||||||
|
// and end point to jump "outside" the shape.
|
||||||
|
if (shouldUnbindStart) {
|
||||||
|
unbindBindingElement(element, "start", scene);
|
||||||
|
}
|
||||||
|
if (shouldUnbindEnd) {
|
||||||
|
unbindBindingElement(element, "end", scene);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -17,7 +17,6 @@ import {
|
|||||||
BinaryHeap,
|
BinaryHeap,
|
||||||
invariant,
|
invariant,
|
||||||
isAnyTrue,
|
isAnyTrue,
|
||||||
tupleToCoors,
|
|
||||||
getSizeFromPoints,
|
getSizeFromPoints,
|
||||||
isDevEnv,
|
isDevEnv,
|
||||||
arrayToMap,
|
arrayToMap,
|
||||||
@ -30,7 +29,7 @@ import {
|
|||||||
FIXED_BINDING_DISTANCE,
|
FIXED_BINDING_DISTANCE,
|
||||||
getHeadingForElbowArrowSnap,
|
getHeadingForElbowArrowSnap,
|
||||||
getGlobalFixedPointForBindableElement,
|
getGlobalFixedPointForBindableElement,
|
||||||
getHoveredElementForBinding,
|
getFixedBindingDistance,
|
||||||
} from "./binding";
|
} from "./binding";
|
||||||
import { distanceToElement } from "./distance";
|
import { distanceToElement } from "./distance";
|
||||||
import {
|
import {
|
||||||
@ -51,8 +50,8 @@ import {
|
|||||||
type ExcalidrawElbowArrowElement,
|
type ExcalidrawElbowArrowElement,
|
||||||
type NonDeletedSceneElementsMap,
|
type NonDeletedSceneElementsMap,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import { aabbForElement, pointInsideBounds } from "./bounds";
|
import { aabbForElement, pointInsideBounds } from "./bounds";
|
||||||
|
import { getHoveredElementForBinding } from "./collision";
|
||||||
|
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
import type { Heading } from "./heading";
|
import type { Heading } from "./heading";
|
||||||
@ -63,6 +62,7 @@ import type {
|
|||||||
FixedPointBinding,
|
FixedPointBinding,
|
||||||
FixedSegment,
|
FixedSegment,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
|
Ordered,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
type GridAddress = [number, number] & { _brand: "gridaddress" };
|
type GridAddress = [number, number] & { _brand: "gridaddress" };
|
||||||
@ -1217,19 +1217,9 @@ const getElbowArrowData = (
|
|||||||
if (options?.isDragging) {
|
if (options?.isDragging) {
|
||||||
const elements = Array.from(elementsMap.values());
|
const elements = Array.from(elementsMap.values());
|
||||||
hoveredStartElement =
|
hoveredStartElement =
|
||||||
getHoveredElement(
|
getHoveredElement(origStartGlobalPoint, elementsMap, elements) || null;
|
||||||
origStartGlobalPoint,
|
|
||||||
elementsMap,
|
|
||||||
elements,
|
|
||||||
options?.zoom,
|
|
||||||
) || null;
|
|
||||||
hoveredEndElement =
|
hoveredEndElement =
|
||||||
getHoveredElement(
|
getHoveredElement(origEndGlobalPoint, elementsMap, elements) || null;
|
||||||
origEndGlobalPoint,
|
|
||||||
elementsMap,
|
|
||||||
elements,
|
|
||||||
options?.zoom,
|
|
||||||
) || null;
|
|
||||||
} else {
|
} else {
|
||||||
hoveredStartElement = arrow.startBinding
|
hoveredStartElement = arrow.startBinding
|
||||||
? getBindableElementForId(arrow.startBinding.elementId, elementsMap) ||
|
? getBindableElementForId(arrow.startBinding.elementId, elementsMap) ||
|
||||||
@ -1301,8 +1291,8 @@ const getElbowArrowData = (
|
|||||||
offsetFromHeading(
|
offsetFromHeading(
|
||||||
startHeading,
|
startHeading,
|
||||||
arrow.startArrowhead
|
arrow.startArrowhead
|
||||||
? FIXED_BINDING_DISTANCE * 6
|
? getFixedBindingDistance(hoveredStartElement) * 6
|
||||||
: FIXED_BINDING_DISTANCE * 2,
|
: getFixedBindingDistance(hoveredStartElement) * 2,
|
||||||
1,
|
1,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -1314,8 +1304,8 @@ const getElbowArrowData = (
|
|||||||
offsetFromHeading(
|
offsetFromHeading(
|
||||||
endHeading,
|
endHeading,
|
||||||
arrow.endArrowhead
|
arrow.endArrowhead
|
||||||
? FIXED_BINDING_DISTANCE * 6
|
? getFixedBindingDistance(hoveredEndElement) * 6
|
||||||
: FIXED_BINDING_DISTANCE * 2,
|
: getFixedBindingDistance(hoveredEndElement) * 2,
|
||||||
1,
|
1,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -2262,16 +2252,13 @@ const getBindPointHeading = (
|
|||||||
const getHoveredElement = (
|
const getHoveredElement = (
|
||||||
origPoint: GlobalPoint,
|
origPoint: GlobalPoint,
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||||
zoom?: AppState["zoom"],
|
|
||||||
) => {
|
) => {
|
||||||
return getHoveredElementForBinding(
|
return getHoveredElementForBinding(
|
||||||
tupleToCoors(origPoint),
|
origPoint,
|
||||||
elements,
|
elements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
zoom,
|
(element) => getFixedBindingDistance(element) + 1,
|
||||||
true,
|
|
||||||
true,
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import type {
|
|||||||
PendingExcalidrawElements,
|
PendingExcalidrawElements,
|
||||||
} from "@excalidraw/excalidraw/types";
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import { bindLinearElement } from "./binding";
|
import { bindBindingElement } from "./binding";
|
||||||
import { updateElbowArrowPoints } from "./elbowArrow";
|
import { updateElbowArrowPoints } from "./elbowArrow";
|
||||||
import {
|
import {
|
||||||
HEADING_DOWN,
|
HEADING_DOWN,
|
||||||
@ -446,8 +446,14 @@ const createBindingArrow = (
|
|||||||
|
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
|
|
||||||
bindLinearElement(bindingArrow, startBindingElement, "start", scene);
|
bindBindingElement(
|
||||||
bindLinearElement(bindingArrow, endBindingElement, "end", scene);
|
bindingArrow,
|
||||||
|
startBindingElement,
|
||||||
|
"orbit",
|
||||||
|
"start",
|
||||||
|
scene,
|
||||||
|
);
|
||||||
|
bindBindingElement(bindingArrow, endBindingElement, "orbit", "end", scene);
|
||||||
|
|
||||||
const changedElements = new Map<string, OrderedExcalidrawElement>();
|
const changedElements = new Map<string, OrderedExcalidrawElement>();
|
||||||
changedElements.set(
|
changedElements.set(
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { toIterable } from "@excalidraw/common";
|
import { toIterable } from "@excalidraw/common";
|
||||||
|
|
||||||
import { isInvisiblySmallElement } from "./sizeHelpers";
|
import { isInvisiblySmallElement } from "./sizeHelpers";
|
||||||
import { isLinearElementType } from "./typeChecks";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
@ -52,27 +51,6 @@ export const isNonDeletedElement = <T extends ExcalidrawElement>(
|
|||||||
element: T,
|
element: T,
|
||||||
): element is NonDeleted<T> => !element.isDeleted;
|
): element is NonDeleted<T> => !element.isDeleted;
|
||||||
|
|
||||||
const _clearElements = (
|
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
): ExcalidrawElement[] =>
|
|
||||||
getNonDeletedElements(elements).map((element) =>
|
|
||||||
isLinearElementType(element.type)
|
|
||||||
? { ...element, lastCommittedPoint: null }
|
|
||||||
: element,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const clearElementsForDatabase = (
|
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
) => _clearElements(elements);
|
|
||||||
|
|
||||||
export const clearElementsForExport = (
|
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
) => _clearElements(elements);
|
|
||||||
|
|
||||||
export const clearElementsForLocalStorage = (
|
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
) => _clearElements(elements);
|
|
||||||
|
|
||||||
export * from "./align";
|
export * from "./align";
|
||||||
export * from "./binding";
|
export * from "./binding";
|
||||||
export * from "./bounds";
|
export * from "./bounds";
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -46,16 +46,13 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||||||
|
|
||||||
// casting to any because can't use `in` operator
|
// casting to any because can't use `in` operator
|
||||||
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
||||||
const { points, fixedSegments, startBinding, endBinding, fileId } =
|
const { points, fixedSegments, fileId } = updates as any;
|
||||||
updates as any;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isElbowArrow(element) &&
|
isElbowArrow(element) &&
|
||||||
(Object.keys(updates).length === 0 || // normalization case
|
(Object.keys(updates).length === 0 || // normalization case
|
||||||
typeof points !== "undefined" || // repositioning
|
typeof points !== "undefined" || // repositioning
|
||||||
typeof fixedSegments !== "undefined" || // segment fixing
|
typeof fixedSegments !== "undefined") // segment fixing
|
||||||
typeof startBinding !== "undefined" ||
|
|
||||||
typeof endBinding !== "undefined") // manual binding to element
|
|
||||||
) {
|
) {
|
||||||
updates = {
|
updates = {
|
||||||
...updates,
|
...updates,
|
||||||
|
|||||||
@ -452,7 +452,6 @@ export const newFreeDrawElement = (
|
|||||||
points: opts.points || [],
|
points: opts.points || [],
|
||||||
pressures: opts.pressures || [],
|
pressures: opts.pressures || [],
|
||||||
simulatePressure: opts.simulatePressure,
|
simulatePressure: opts.simulatePressure,
|
||||||
lastCommittedPoint: null,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -466,7 +465,7 @@ export const newLinearElement = (
|
|||||||
const element = {
|
const element = {
|
||||||
..._newElementBase<ExcalidrawLinearElement>(opts.type, opts),
|
..._newElementBase<ExcalidrawLinearElement>(opts.type, opts),
|
||||||
points: opts.points || [],
|
points: opts.points || [],
|
||||||
lastCommittedPoint: null,
|
|
||||||
startBinding: null,
|
startBinding: null,
|
||||||
endBinding: null,
|
endBinding: null,
|
||||||
startArrowhead: null,
|
startArrowhead: null,
|
||||||
@ -501,7 +500,6 @@ export const newArrowElement = <T extends boolean>(
|
|||||||
return {
|
return {
|
||||||
..._newElementBase<ExcalidrawElbowArrowElement>(opts.type, opts),
|
..._newElementBase<ExcalidrawElbowArrowElement>(opts.type, opts),
|
||||||
points: opts.points || [],
|
points: opts.points || [],
|
||||||
lastCommittedPoint: null,
|
|
||||||
startBinding: null,
|
startBinding: null,
|
||||||
endBinding: null,
|
endBinding: null,
|
||||||
startArrowhead: opts.startArrowhead || null,
|
startArrowhead: opts.startArrowhead || null,
|
||||||
@ -516,7 +514,6 @@ export const newArrowElement = <T extends boolean>(
|
|||||||
return {
|
return {
|
||||||
..._newElementBase<ExcalidrawArrowElement>(opts.type, opts),
|
..._newElementBase<ExcalidrawArrowElement>(opts.type, opts),
|
||||||
points: opts.points || [],
|
points: opts.points || [],
|
||||||
lastCommittedPoint: null,
|
|
||||||
startBinding: null,
|
startBinding: null,
|
||||||
endBinding: null,
|
endBinding: null,
|
||||||
startArrowhead: opts.startArrowhead || null,
|
startArrowhead: opts.startArrowhead || null,
|
||||||
|
|||||||
@ -90,7 +90,7 @@ const isPendingImageElement = (
|
|||||||
const shouldResetImageFilter = (
|
const shouldResetImageFilter = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
renderConfig: StaticCanvasRenderConfig,
|
renderConfig: StaticCanvasRenderConfig,
|
||||||
appState: StaticCanvasAppState,
|
appState: StaticCanvasAppState | InteractiveCanvasAppState,
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
appState.theme === THEME.DARK &&
|
appState.theme === THEME.DARK &&
|
||||||
@ -217,7 +217,7 @@ const generateElementCanvas = (
|
|||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
zoom: Zoom,
|
zoom: Zoom,
|
||||||
renderConfig: StaticCanvasRenderConfig,
|
renderConfig: StaticCanvasRenderConfig,
|
||||||
appState: StaticCanvasAppState,
|
appState: StaticCanvasAppState | InteractiveCanvasAppState,
|
||||||
): ExcalidrawElementWithCanvas | null => {
|
): ExcalidrawElementWithCanvas | null => {
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
const context = canvas.getContext("2d")!;
|
const context = canvas.getContext("2d")!;
|
||||||
@ -269,7 +269,7 @@ const generateElementCanvas = (
|
|||||||
context.filter = IMAGE_INVERT_FILTER;
|
context.filter = IMAGE_INVERT_FILTER;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
drawElementOnCanvas(element, rc, context, renderConfig);
|
||||||
|
|
||||||
context.restore();
|
context.restore();
|
||||||
|
|
||||||
@ -404,7 +404,6 @@ const drawElementOnCanvas = (
|
|||||||
rc: RoughCanvas,
|
rc: RoughCanvas,
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
renderConfig: StaticCanvasRenderConfig,
|
renderConfig: StaticCanvasRenderConfig,
|
||||||
appState: StaticCanvasAppState,
|
|
||||||
) => {
|
) => {
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
@ -550,7 +549,7 @@ const generateElementWithCanvas = (
|
|||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
renderConfig: StaticCanvasRenderConfig,
|
renderConfig: StaticCanvasRenderConfig,
|
||||||
appState: StaticCanvasAppState,
|
appState: StaticCanvasAppState | InteractiveCanvasAppState,
|
||||||
) => {
|
) => {
|
||||||
const zoom: Zoom = renderConfig
|
const zoom: Zoom = renderConfig
|
||||||
? appState.zoom
|
? appState.zoom
|
||||||
@ -607,7 +606,7 @@ const drawElementFromCanvas = (
|
|||||||
elementWithCanvas: ExcalidrawElementWithCanvas,
|
elementWithCanvas: ExcalidrawElementWithCanvas,
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
renderConfig: StaticCanvasRenderConfig,
|
renderConfig: StaticCanvasRenderConfig,
|
||||||
appState: StaticCanvasAppState,
|
appState: StaticCanvasAppState | InteractiveCanvasAppState,
|
||||||
allElementsMap: NonDeletedSceneElementsMap,
|
allElementsMap: NonDeletedSceneElementsMap,
|
||||||
) => {
|
) => {
|
||||||
const element = elementWithCanvas.element;
|
const element = elementWithCanvas.element;
|
||||||
@ -725,7 +724,7 @@ export const renderElement = (
|
|||||||
rc: RoughCanvas,
|
rc: RoughCanvas,
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
renderConfig: StaticCanvasRenderConfig,
|
renderConfig: StaticCanvasRenderConfig,
|
||||||
appState: StaticCanvasAppState,
|
appState: StaticCanvasAppState | InteractiveCanvasAppState,
|
||||||
) => {
|
) => {
|
||||||
const reduceAlphaForSelection =
|
const reduceAlphaForSelection =
|
||||||
appState.openDialog?.name === "elementLinkSelector" &&
|
appState.openDialog?.name === "elementLinkSelector" &&
|
||||||
@ -795,7 +794,7 @@ export const renderElement = (
|
|||||||
context.translate(cx, cy);
|
context.translate(cx, cy);
|
||||||
context.rotate(element.angle);
|
context.rotate(element.angle);
|
||||||
context.translate(-shiftX, -shiftY);
|
context.translate(-shiftX, -shiftY);
|
||||||
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
drawElementOnCanvas(element, rc, context, renderConfig);
|
||||||
context.restore();
|
context.restore();
|
||||||
} else {
|
} else {
|
||||||
const elementWithCanvas = generateElementWithCanvas(
|
const elementWithCanvas = generateElementWithCanvas(
|
||||||
@ -888,13 +887,7 @@ export const renderElement = (
|
|||||||
|
|
||||||
tempCanvasContext.translate(-shiftX, -shiftY);
|
tempCanvasContext.translate(-shiftX, -shiftY);
|
||||||
|
|
||||||
drawElementOnCanvas(
|
drawElementOnCanvas(element, tempRc, tempCanvasContext, renderConfig);
|
||||||
element,
|
|
||||||
tempRc,
|
|
||||||
tempCanvasContext,
|
|
||||||
renderConfig,
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
|
|
||||||
tempCanvasContext.translate(shiftX, shiftY);
|
tempCanvasContext.translate(shiftX, shiftY);
|
||||||
|
|
||||||
@ -933,7 +926,7 @@ export const renderElement = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
context.translate(-shiftX, -shiftY);
|
context.translate(-shiftX, -shiftY);
|
||||||
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
drawElementOnCanvas(element, rc, context, renderConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
context.restore();
|
context.restore();
|
||||||
@ -1054,7 +1047,7 @@ export function getFreeDrawSvgPath(element: ExcalidrawFreeDrawElement) {
|
|||||||
smoothing: 0.5,
|
smoothing: 0.5,
|
||||||
streamline: 0.5,
|
streamline: 0.5,
|
||||||
easing: (t) => Math.sin((t * Math.PI) / 2), // https://easings.net/#easeOutSine
|
easing: (t) => Math.sin((t * Math.PI) / 2), // https://easings.net/#easeOutSine
|
||||||
last: !!element.lastCommittedPoint, // LastCommittedPoint is added on pointerup
|
last: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
return getSvgPathFromStroke(getStroke(inputPoints as number[][], options));
|
return getSvgPathFromStroke(getStroke(inputPoints as number[][], options));
|
||||||
|
|||||||
@ -20,7 +20,11 @@ import type { PointerDownState } from "@excalidraw/excalidraw/types";
|
|||||||
|
|
||||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import { getArrowLocalFixedPoints, updateBoundElements } from "./binding";
|
import {
|
||||||
|
getArrowLocalFixedPoints,
|
||||||
|
unbindBindingElement,
|
||||||
|
updateBoundElements,
|
||||||
|
} from "./binding";
|
||||||
import {
|
import {
|
||||||
getElementAbsoluteCoords,
|
getElementAbsoluteCoords,
|
||||||
getCommonBounds,
|
getCommonBounds,
|
||||||
@ -46,6 +50,7 @@ import {
|
|||||||
import { wrapText } from "./textWrapping";
|
import { wrapText } from "./textWrapping";
|
||||||
import {
|
import {
|
||||||
isArrowElement,
|
isArrowElement,
|
||||||
|
isBindingElement,
|
||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
isFrameLikeElement,
|
isFrameLikeElement,
|
||||||
@ -74,7 +79,9 @@ import type {
|
|||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
|
ExcalidrawArrowElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
import type { ElementUpdate } from "./mutateElement";
|
||||||
|
|
||||||
// Returns true when transform (resizing/rotation) happened
|
// Returns true when transform (resizing/rotation) happened
|
||||||
export const transformElements = (
|
export const transformElements = (
|
||||||
@ -220,7 +227,25 @@ const rotateSingleElement = (
|
|||||||
}
|
}
|
||||||
const boundTextElementId = getBoundTextElementId(element);
|
const boundTextElementId = getBoundTextElementId(element);
|
||||||
|
|
||||||
scene.mutateElement(element, { angle });
|
let update: ElementUpdate<NonDeletedExcalidrawElement> = {
|
||||||
|
angle,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isBindingElement(element)) {
|
||||||
|
update = {
|
||||||
|
...update,
|
||||||
|
} as ElementUpdate<ExcalidrawArrowElement>;
|
||||||
|
|
||||||
|
if (element.startBinding) {
|
||||||
|
unbindBindingElement(element, "start", scene);
|
||||||
|
}
|
||||||
|
if (element.endBinding) {
|
||||||
|
unbindBindingElement(element, "end", scene);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scene.mutateElement(element, update);
|
||||||
|
|
||||||
if (boundTextElementId) {
|
if (boundTextElementId) {
|
||||||
const textElement =
|
const textElement =
|
||||||
scene.getElement<ExcalidrawTextElementWithContainer>(boundTextElementId);
|
scene.getElement<ExcalidrawTextElementWithContainer>(boundTextElementId);
|
||||||
@ -394,6 +419,11 @@ const rotateMultipleElements = (
|
|||||||
centerAngle -= centerAngle % SHIFT_LOCKING_ANGLE;
|
centerAngle -= centerAngle % SHIFT_LOCKING_ANGLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rotatedElementsMap = new Map<
|
||||||
|
ExcalidrawElement["id"],
|
||||||
|
NonDeletedExcalidrawElement
|
||||||
|
>(elements.map((element) => [element.id, element]));
|
||||||
|
|
||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
if (!isFrameLikeElement(element)) {
|
if (!isFrameLikeElement(element)) {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
||||||
@ -424,6 +454,19 @@ const rotateMultipleElements = (
|
|||||||
simultaneouslyUpdated: elements,
|
simultaneouslyUpdated: elements,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isBindingElement(element)) {
|
||||||
|
if (element.startBinding) {
|
||||||
|
if (!rotatedElementsMap.has(element.startBinding.elementId)) {
|
||||||
|
unbindBindingElement(element, "start", scene);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (element.endBinding) {
|
||||||
|
if (!rotatedElementsMap.has(element.endBinding.elementId)) {
|
||||||
|
unbindBindingElement(element, "end", scene);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const boundText = getBoundTextElement(element, elementsMap);
|
const boundText = getBoundTextElement(element, elementsMap);
|
||||||
if (boundText && !isArrowElement(element)) {
|
if (boundText && !isArrowElement(element)) {
|
||||||
const { x, y } = computeBoundTextPosition(
|
const { x, y } = computeBoundTextPosition(
|
||||||
@ -835,13 +878,32 @@ export const resizeSingleElement = (
|
|||||||
Number.isFinite(newOrigin.x) &&
|
Number.isFinite(newOrigin.x) &&
|
||||||
Number.isFinite(newOrigin.y)
|
Number.isFinite(newOrigin.y)
|
||||||
) {
|
) {
|
||||||
const updates = {
|
let updates: ElementUpdate<ExcalidrawElement> = {
|
||||||
...newOrigin,
|
...newOrigin,
|
||||||
width: Math.abs(nextWidth),
|
width: Math.abs(nextWidth),
|
||||||
height: Math.abs(nextHeight),
|
height: Math.abs(nextHeight),
|
||||||
...rescaledPoints,
|
...rescaledPoints,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isBindingElement(latestElement)) {
|
||||||
|
if (latestElement.startBinding) {
|
||||||
|
updates = {
|
||||||
|
...updates,
|
||||||
|
} as ElementUpdate<ExcalidrawArrowElement>;
|
||||||
|
|
||||||
|
if (latestElement.startBinding) {
|
||||||
|
unbindBindingElement(latestElement, "start", scene);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (latestElement.endBinding) {
|
||||||
|
updates = {
|
||||||
|
...updates,
|
||||||
|
endBinding: null,
|
||||||
|
} as ElementUpdate<ExcalidrawArrowElement>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
scene.mutateElement(latestElement, updates, {
|
scene.mutateElement(latestElement, updates, {
|
||||||
informMutation: shouldInformMutation,
|
informMutation: shouldInformMutation,
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
@ -859,10 +921,7 @@ export const resizeSingleElement = (
|
|||||||
shouldMaintainAspectRatio,
|
shouldMaintainAspectRatio,
|
||||||
);
|
);
|
||||||
|
|
||||||
updateBoundElements(latestElement, scene, {
|
updateBoundElements(latestElement, scene);
|
||||||
// TODO: confirm with MARK if this actually makes sense
|
|
||||||
newSize: { width: nextWidth, height: nextHeight },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1396,20 +1455,36 @@ export const resizeMultipleElements = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const elementsToUpdate = elementsAndUpdates.map(({ element }) => element);
|
const elementsToUpdate = elementsAndUpdates.map(({ element }) => element);
|
||||||
|
const resizedElementsMap = new Map<
|
||||||
|
ExcalidrawElement["id"],
|
||||||
|
NonDeletedExcalidrawElement
|
||||||
|
>(elementsAndUpdates.map(({ element }) => [element.id, element]));
|
||||||
|
|
||||||
for (const {
|
for (const {
|
||||||
element,
|
element,
|
||||||
update: { boundTextFontSize, ...update },
|
update: { boundTextFontSize, ...update },
|
||||||
} of elementsAndUpdates) {
|
} of elementsAndUpdates) {
|
||||||
const { width, height, angle } = update;
|
const { angle } = update;
|
||||||
|
|
||||||
scene.mutateElement(element, update);
|
scene.mutateElement(element, update);
|
||||||
|
|
||||||
updateBoundElements(element, scene, {
|
updateBoundElements(element, scene, {
|
||||||
simultaneouslyUpdated: elementsToUpdate,
|
simultaneouslyUpdated: elementsToUpdate,
|
||||||
newSize: { width, height },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isBindingElement(element)) {
|
||||||
|
if (element.startBinding) {
|
||||||
|
if (!resizedElementsMap.has(element.startBinding.elementId)) {
|
||||||
|
unbindBindingElement(element, "start", scene);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (element.endBinding) {
|
||||||
|
if (!resizedElementsMap.has(element.endBinding.elementId)) {
|
||||||
|
unbindBindingElement(element, "end", scene);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
if (boundTextElement && boundTextFontSize) {
|
if (boundTextElement && boundTextFontSize) {
|
||||||
scene.mutateElement(boundTextElement, {
|
scene.mutateElement(boundTextElement, {
|
||||||
|
|||||||
@ -28,8 +28,6 @@ import type {
|
|||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
ExcalidrawLineElement,
|
ExcalidrawLineElement,
|
||||||
PointBinding,
|
|
||||||
FixedPointBinding,
|
|
||||||
ExcalidrawFlowchartNodeElement,
|
ExcalidrawFlowchartNodeElement,
|
||||||
ExcalidrawLinearElementSubType,
|
ExcalidrawLinearElementSubType,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
@ -163,7 +161,7 @@ export const isLinearElementType = (
|
|||||||
export const isBindingElement = (
|
export const isBindingElement = (
|
||||||
element?: ExcalidrawElement | null,
|
element?: ExcalidrawElement | null,
|
||||||
includeLocked = true,
|
includeLocked = true,
|
||||||
): element is ExcalidrawLinearElement => {
|
): element is ExcalidrawArrowElement => {
|
||||||
return (
|
return (
|
||||||
element != null &&
|
element != null &&
|
||||||
(!element.locked || includeLocked === true) &&
|
(!element.locked || includeLocked === true) &&
|
||||||
@ -358,15 +356,6 @@ export const getDefaultRoundnessTypeForElement = (
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isFixedPointBinding = (
|
|
||||||
binding: PointBinding | FixedPointBinding,
|
|
||||||
): binding is FixedPointBinding => {
|
|
||||||
return (
|
|
||||||
Object.hasOwn(binding, "fixedPoint") &&
|
|
||||||
(binding as FixedPointBinding).fixedPoint != null
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Move this to @excalidraw/math
|
// TODO: Move this to @excalidraw/math
|
||||||
export const isBounds = (box: unknown): box is Bounds =>
|
export const isBounds = (box: unknown): box is Bounds =>
|
||||||
Array.isArray(box) &&
|
Array.isArray(box) &&
|
||||||
|
|||||||
@ -279,23 +279,22 @@ export type ExcalidrawTextElementWithContainer = {
|
|||||||
|
|
||||||
export type FixedPoint = [number, number];
|
export type FixedPoint = [number, number];
|
||||||
|
|
||||||
export type PointBinding = {
|
export type BindMode = "inside" | "orbit" | "skip";
|
||||||
elementId: ExcalidrawBindableElement["id"];
|
|
||||||
focus: number;
|
export type FixedPointBinding = {
|
||||||
gap: number;
|
elementId: ExcalidrawBindableElement["id"];
|
||||||
};
|
|
||||||
|
|
||||||
export type FixedPointBinding = Merge<
|
|
||||||
PointBinding,
|
|
||||||
{
|
|
||||||
// Represents the fixed point binding information in form of a vertical and
|
// Represents the fixed point binding information in form of a vertical and
|
||||||
// horizontal ratio (i.e. a percentage value in the 0.0-1.0 range). This ratio
|
// horizontal ratio (i.e. a percentage value in the 0.0-1.0 range). This ratio
|
||||||
// gives the user selected fixed point by multiplying the bound element width
|
// gives the user selected fixed point by multiplying the bound element width
|
||||||
// with fixedPoint[0] and the bound element height with fixedPoint[1] to get the
|
// with fixedPoint[0] and the bound element height with fixedPoint[1] to get the
|
||||||
// bound element-local point coordinate.
|
// bound element-local point coordinate.
|
||||||
fixedPoint: FixedPoint;
|
fixedPoint: FixedPoint;
|
||||||
}
|
|
||||||
>;
|
// Determines whether the arrow remains outside the shape or is allowed to
|
||||||
|
// go all the way inside the shape up to the exact fixed point.
|
||||||
|
mode: BindMode;
|
||||||
|
};
|
||||||
|
|
||||||
type Index = number;
|
type Index = number;
|
||||||
|
|
||||||
@ -322,9 +321,8 @@ export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
|||||||
Readonly<{
|
Readonly<{
|
||||||
type: "line" | "arrow";
|
type: "line" | "arrow";
|
||||||
points: readonly LocalPoint[];
|
points: readonly LocalPoint[];
|
||||||
lastCommittedPoint: LocalPoint | null;
|
startBinding: FixedPointBinding | null;
|
||||||
startBinding: PointBinding | null;
|
endBinding: FixedPointBinding | null;
|
||||||
endBinding: PointBinding | null;
|
|
||||||
startArrowhead: Arrowhead | null;
|
startArrowhead: Arrowhead | null;
|
||||||
endArrowhead: Arrowhead | null;
|
endArrowhead: Arrowhead | null;
|
||||||
}>;
|
}>;
|
||||||
@ -351,9 +349,9 @@ export type ExcalidrawElbowArrowElement = Merge<
|
|||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
{
|
{
|
||||||
elbowed: true;
|
elbowed: true;
|
||||||
|
fixedSegments: readonly FixedSegment[] | null;
|
||||||
startBinding: FixedPointBinding | null;
|
startBinding: FixedPointBinding | null;
|
||||||
endBinding: FixedPointBinding | null;
|
endBinding: FixedPointBinding | null;
|
||||||
fixedSegments: readonly FixedSegment[] | null;
|
|
||||||
/**
|
/**
|
||||||
* Marks that the 3rd point should be used as the 2nd point of the arrow in
|
* Marks that the 3rd point should be used as the 2nd point of the arrow in
|
||||||
* order to temporarily hide the first segment of the arrow without losing
|
* order to temporarily hide the first segment of the arrow without losing
|
||||||
@ -379,7 +377,6 @@ export type ExcalidrawFreeDrawElement = _ExcalidrawElementBase &
|
|||||||
points: readonly LocalPoint[];
|
points: readonly LocalPoint[];
|
||||||
pressures: readonly number[];
|
pressures: readonly number[];
|
||||||
simulatePressure: boolean;
|
simulatePressure: boolean;
|
||||||
lastCommittedPoint: LocalPoint | null;
|
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type FileId = string & { _brand: "FileId" };
|
export type FileId = string & { _brand: "FileId" };
|
||||||
|
|||||||
@ -1,18 +1,25 @@
|
|||||||
import { arrayToMap, findIndex, findLastIndex } from "@excalidraw/common";
|
import { arrayToMap, findIndex, findLastIndex } from "@excalidraw/common";
|
||||||
|
|
||||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||||
|
import type { GlobalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
import { isFrameLikeElement } from "./typeChecks";
|
import { isFrameLikeElement, isTextElement } from "./typeChecks";
|
||||||
|
|
||||||
import { getElementsInGroup } from "./groups";
|
import { getElementsInGroup } from "./groups";
|
||||||
|
|
||||||
import { syncMovedIndices } from "./fractionalIndex";
|
import { syncMovedIndices } from "./fractionalIndex";
|
||||||
|
|
||||||
import { getSelectedElements } from "./selection";
|
import { getSelectedElements } from "./selection";
|
||||||
|
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||||
|
import { getHoveredElementForBinding } from "./collision";
|
||||||
|
|
||||||
import type { Scene } from "./Scene";
|
import type { Scene } from "./Scene";
|
||||||
|
import type {
|
||||||
import type { ExcalidrawElement, ExcalidrawFrameLikeElement } from "./types";
|
ExcalidrawArrowElement,
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawFrameLikeElement,
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
NonDeletedSceneElementsMap,
|
||||||
|
Ordered,
|
||||||
|
OrderedExcalidrawElement,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
const isOfTargetFrame = (element: ExcalidrawElement, frameId: string) => {
|
const isOfTargetFrame = (element: ExcalidrawElement, frameId: string) => {
|
||||||
return element.frameId === frameId || element.id === frameId;
|
return element.frameId === frameId || element.id === frameId;
|
||||||
@ -139,6 +146,51 @@ const getContiguousFrameRangeElements = (
|
|||||||
return allElements.slice(rangeStart, rangeEnd + 1);
|
return allElements.slice(rangeStart, rangeEnd + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves the arrow element above any bindable elements it intersects with or
|
||||||
|
* hovers over.
|
||||||
|
*/
|
||||||
|
export const moveArrowAboveBindable = (
|
||||||
|
point: GlobalPoint,
|
||||||
|
arrow: ExcalidrawArrowElement,
|
||||||
|
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||||
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
|
scene: Scene,
|
||||||
|
): readonly OrderedExcalidrawElement[] => {
|
||||||
|
const hoveredElement = getHoveredElementForBinding(
|
||||||
|
point,
|
||||||
|
elements,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hoveredElement) {
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
const boundTextElement = getBoundTextElement(hoveredElement, elementsMap);
|
||||||
|
const containerElement = isTextElement(hoveredElement)
|
||||||
|
? getContainerElement(hoveredElement, elementsMap)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const bindableIds = [
|
||||||
|
hoveredElement.id,
|
||||||
|
boundTextElement?.id,
|
||||||
|
containerElement?.id,
|
||||||
|
].filter((id): id is NonDeletedExcalidrawElement["id"] => !!id);
|
||||||
|
const bindableIdx = elements.findIndex((el) => bindableIds.includes(el.id));
|
||||||
|
const arrowIdx = elements.findIndex((el) => el.id === arrow.id);
|
||||||
|
|
||||||
|
if (arrowIdx !== -1 && bindableIdx !== -1 && arrowIdx < bindableIdx) {
|
||||||
|
const updatedElements = Array.from(elements);
|
||||||
|
const arrow = updatedElements.splice(arrowIdx, 1)[0];
|
||||||
|
updatedElements.splice(bindableIdx, 0, arrow);
|
||||||
|
|
||||||
|
scene.replaceAllElements(updatedElements);
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns next candidate index that's available to be moved to. Currently that
|
* Returns next candidate index that's available to be moved to. Currently that
|
||||||
* is a non-deleted element, and not inside a group (unless we're editing it).
|
* is a non-deleted element, and not inside a group (unless we're editing it).
|
||||||
|
|||||||
@ -8,7 +8,13 @@ import { Excalidraw, isLinearElement } from "@excalidraw/excalidraw";
|
|||||||
|
|
||||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
import { UI, Pointer, Keyboard } from "@excalidraw/excalidraw/tests/helpers/ui";
|
import { UI, Pointer, Keyboard } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||||
import { fireEvent, render } from "@excalidraw/excalidraw/tests/test-utils";
|
import {
|
||||||
|
act,
|
||||||
|
fireEvent,
|
||||||
|
render,
|
||||||
|
} from "@excalidraw/excalidraw/tests/test-utils";
|
||||||
|
|
||||||
|
import { defaultLang, setLanguage } from "@excalidraw/excalidraw/i18n";
|
||||||
|
|
||||||
import { getTransformHandles } from "../src/transformHandles";
|
import { getTransformHandles } from "../src/transformHandles";
|
||||||
import {
|
import {
|
||||||
@ -16,123 +22,306 @@ import {
|
|||||||
TEXT_EDITOR_SELECTOR,
|
TEXT_EDITOR_SELECTOR,
|
||||||
} from "../../excalidraw/tests/queries/dom";
|
} from "../../excalidraw/tests/queries/dom";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ExcalidrawArrowElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
|
FixedPointBinding,
|
||||||
|
} from "../src/types";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
const mouse = new Pointer("mouse");
|
const mouse = new Pointer("mouse");
|
||||||
|
|
||||||
describe("element binding", () => {
|
describe("binding for simple arrows", () => {
|
||||||
|
describe("when both endpoints are bound inside the same element", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
mouse.reset();
|
||||||
|
|
||||||
|
await act(() => {
|
||||||
|
return setLanguage(defaultLang);
|
||||||
|
});
|
||||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should create valid binding if duplicate start/end points", async () => {
|
it("should create an `inside` binding", () => {
|
||||||
const rect = API.createElement({
|
// Create a rectangle
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.reset();
|
||||||
|
mouse.downAt(100, 100);
|
||||||
|
mouse.moveTo(200, 200);
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
const rect = API.getSelectedElement();
|
||||||
|
|
||||||
|
// Draw arrow with endpoint inside the filled rectangle
|
||||||
|
UI.clickTool("arrow");
|
||||||
|
mouse.downAt(110, 110);
|
||||||
|
mouse.moveTo(160, 160);
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
const arrow = API.getSelectedElement() as ExcalidrawLinearElement;
|
||||||
|
expect(arrow.x).toBe(110);
|
||||||
|
expect(arrow.y).toBe(110);
|
||||||
|
|
||||||
|
// Should bind to the rectangle since endpoint is inside
|
||||||
|
expect(arrow.startBinding?.elementId).toBe(rect.id);
|
||||||
|
expect(arrow.endBinding?.elementId).toBe(rect.id);
|
||||||
|
|
||||||
|
const startBinding = arrow.startBinding as FixedPointBinding;
|
||||||
|
expect(startBinding.fixedPoint[0]).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(startBinding.fixedPoint[0]).toBeLessThanOrEqual(1);
|
||||||
|
expect(startBinding.fixedPoint[1]).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(startBinding.fixedPoint[1]).toBeLessThanOrEqual(1);
|
||||||
|
expect(startBinding.mode).toBe("inside");
|
||||||
|
|
||||||
|
const endBinding = arrow.endBinding as FixedPointBinding;
|
||||||
|
expect(endBinding.fixedPoint[0]).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(endBinding.fixedPoint[0]).toBeLessThanOrEqual(1);
|
||||||
|
expect(endBinding.fixedPoint[1]).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(endBinding.fixedPoint[1]).toBeLessThanOrEqual(1);
|
||||||
|
expect(endBinding.mode).toBe("inside");
|
||||||
|
|
||||||
|
// Move the bindable
|
||||||
|
mouse.downAt(100, 150);
|
||||||
|
mouse.moveTo(280, 110);
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
// Check if the arrow moved
|
||||||
|
expect(arrow.x).toBe(290);
|
||||||
|
expect(arrow.y).toBe(70);
|
||||||
|
|
||||||
|
// Restore bindable
|
||||||
|
mouse.reset();
|
||||||
|
mouse.downAt(280, 110);
|
||||||
|
mouse.moveTo(130, 110);
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
// Move the start point of the arrow to check if
|
||||||
|
// the behavior remains the same for old arrows
|
||||||
|
mouse.reset();
|
||||||
|
mouse.downAt(110, 110);
|
||||||
|
mouse.moveTo(120, 120);
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
// Move the bindable again
|
||||||
|
mouse.reset();
|
||||||
|
mouse.downAt(130, 110);
|
||||||
|
mouse.moveTo(280, 110);
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
// Check if the arrow moved
|
||||||
|
expect(arrow.x).toBe(290);
|
||||||
|
expect(arrow.y).toBe(70);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("3+ point arrow should be dragged along with the bindable", () => {
|
||||||
|
// Create two rectangles as binding targets
|
||||||
|
const rectLeft = API.createElement({
|
||||||
type: "rectangle",
|
type: "rectangle",
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
width: 50,
|
width: 100,
|
||||||
height: 50,
|
height: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const rectRight = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 300,
|
||||||
|
y: 0,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a non-elbowed arrow with inner points bound to different elements
|
||||||
const arrow = API.createElement({
|
const arrow = API.createElement({
|
||||||
type: "arrow",
|
type: "arrow",
|
||||||
x: 100,
|
x: 100,
|
||||||
y: 0,
|
y: 50,
|
||||||
width: 100,
|
width: 200,
|
||||||
height: 1,
|
height: 0,
|
||||||
points: [
|
points: [
|
||||||
pointFrom(0, 0),
|
pointFrom(0, 0), // start point
|
||||||
pointFrom(0, 0),
|
pointFrom(50, -20), // first inner point
|
||||||
pointFrom(100, 0),
|
pointFrom(150, 20), // second inner point
|
||||||
pointFrom(100, 0),
|
pointFrom(200, 0), // end point
|
||||||
],
|
],
|
||||||
});
|
startBinding: {
|
||||||
API.setElements([rect, arrow]);
|
elementId: rectLeft.id,
|
||||||
expect(arrow.startBinding).toBe(null);
|
fixedPoint: [0.5, 0.5],
|
||||||
|
mode: "orbit",
|
||||||
// select arrow
|
},
|
||||||
mouse.clickAt(150, 0);
|
endBinding: {
|
||||||
|
elementId: rectRight.id,
|
||||||
// move arrow start to potential binding position
|
fixedPoint: [0.5, 0.5],
|
||||||
mouse.downAt(100, 0);
|
mode: "orbit",
|
||||||
mouse.moveTo(55, 0);
|
},
|
||||||
mouse.up(0, 0);
|
|
||||||
|
|
||||||
// Point selection is evaluated like the points are rendered,
|
|
||||||
// from right to left. So clicking on the first point should move the joint,
|
|
||||||
// not the start point.
|
|
||||||
expect(arrow.startBinding).toBe(null);
|
|
||||||
|
|
||||||
// Now that the start point is free, move it into overlapping position
|
|
||||||
mouse.downAt(100, 0);
|
|
||||||
mouse.moveTo(55, 0);
|
|
||||||
mouse.up(0, 0);
|
|
||||||
|
|
||||||
expect(API.getSelectedElements()).toEqual([arrow]);
|
|
||||||
|
|
||||||
expect(arrow.startBinding).toEqual({
|
|
||||||
elementId: rect.id,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
|
||||||
gap: expect.toBeNonNaNNumber(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Move the end point to the overlapping binding position
|
API.setElements([rectLeft, rectRight, arrow]);
|
||||||
mouse.downAt(200, 0);
|
|
||||||
mouse.moveTo(55, 0);
|
|
||||||
mouse.up(0, 0);
|
|
||||||
|
|
||||||
// Both the start and the end points should be bound
|
// Store original inner point positions
|
||||||
expect(arrow.startBinding).toEqual({
|
const originalInnerPoint1 = [...arrow.points[1]];
|
||||||
elementId: rect.id,
|
const originalInnerPoint2 = [...arrow.points[2]];
|
||||||
focus: expect.toBeNonNaNNumber(),
|
|
||||||
gap: expect.toBeNonNaNNumber(),
|
|
||||||
});
|
|
||||||
expect(arrow.endBinding).toEqual({
|
|
||||||
elementId: rect.id,
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
|
||||||
gap: expect.toBeNonNaNNumber(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
//@TODO fix the test with rotation
|
// Move the right rectangle down by 50 pixels
|
||||||
it.skip("rotation of arrow should rebind both ends", () => {
|
mouse.reset();
|
||||||
const rectLeft = UI.createElement("rectangle", {
|
mouse.downAt(350, 50); // Click on the right rectangle
|
||||||
x: 0,
|
mouse.moveTo(350, 100); // Move it down
|
||||||
width: 200,
|
|
||||||
height: 500,
|
|
||||||
});
|
|
||||||
const rectRight = UI.createElement("rectangle", {
|
|
||||||
x: 400,
|
|
||||||
width: 200,
|
|
||||||
height: 500,
|
|
||||||
});
|
|
||||||
const arrow = UI.createElement("arrow", {
|
|
||||||
x: 210,
|
|
||||||
y: 250,
|
|
||||||
width: 180,
|
|
||||||
height: 1,
|
|
||||||
});
|
|
||||||
expect(arrow.startBinding?.elementId).toBe(rectLeft.id);
|
|
||||||
expect(arrow.endBinding?.elementId).toBe(rectRight.id);
|
|
||||||
|
|
||||||
const rotation = getTransformHandles(
|
|
||||||
arrow,
|
|
||||||
h.state.zoom,
|
|
||||||
arrayToMap(h.elements),
|
|
||||||
"mouse",
|
|
||||||
).rotation!;
|
|
||||||
const rotationHandleX = rotation[0] + rotation[2] / 2;
|
|
||||||
const rotationHandleY = rotation[1] + rotation[3] / 2;
|
|
||||||
mouse.down(rotationHandleX, rotationHandleY);
|
|
||||||
mouse.move(300, 400);
|
|
||||||
mouse.up();
|
mouse.up();
|
||||||
expect(arrow.angle).toBeGreaterThan(0.7 * Math.PI);
|
|
||||||
expect(arrow.angle).toBeLessThan(1.3 * Math.PI);
|
// Verify that inner points did NOT move when bound to different elements
|
||||||
expect(arrow.startBinding?.elementId).toBe(rectRight.id);
|
// The arrow should NOT translate inner points proportionally when only one end moves
|
||||||
expect(arrow.endBinding?.elementId).toBe(rectLeft.id);
|
expect(arrow.points[1][0]).toBe(originalInnerPoint1[0]);
|
||||||
|
expect(arrow.points[1][1]).toBe(originalInnerPoint1[1]);
|
||||||
|
expect(arrow.points[2][0]).toBe(originalInnerPoint2[0]);
|
||||||
|
expect(arrow.points[2][1]).toBe(originalInnerPoint2[1]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO fix & reenable once we rewrite tests to work with concurrency
|
describe("when arrow is outside of shape", () => {
|
||||||
it.skip(
|
beforeEach(async () => {
|
||||||
|
mouse.reset();
|
||||||
|
|
||||||
|
await act(() => {
|
||||||
|
return setLanguage(defaultLang);
|
||||||
|
});
|
||||||
|
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle new arrow start point binding", () => {
|
||||||
|
// Create a rectangle
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.downAt(100, 100);
|
||||||
|
mouse.moveTo(200, 200);
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
const rectangle = API.getSelectedElement();
|
||||||
|
|
||||||
|
// Create arrow with arrow tool
|
||||||
|
UI.clickTool("arrow");
|
||||||
|
mouse.downAt(150, 150); // Start inside rectangle
|
||||||
|
mouse.moveTo(250, 150); // End outside
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
const arrow = API.getSelectedElement() as ExcalidrawLinearElement;
|
||||||
|
|
||||||
|
// Arrow should have start binding to rectangle
|
||||||
|
expect(arrow.startBinding?.elementId).toBe(rectangle.id);
|
||||||
|
expect(arrow.startBinding?.mode).toBe("orbit"); // Default is orbit, not inside
|
||||||
|
expect(arrow.endBinding).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle new arrow end point binding", () => {
|
||||||
|
// Create a rectangle
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.downAt(100, 100);
|
||||||
|
mouse.moveTo(200, 200);
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
const rectangle = API.getSelectedElement();
|
||||||
|
|
||||||
|
// Create arrow with end point in binding zone
|
||||||
|
UI.clickTool("arrow");
|
||||||
|
mouse.downAt(50, 150); // Start outside
|
||||||
|
mouse.moveTo(190, 190); // End near rectangle edge (should bind as orbit)
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
const arrow = API.getSelectedElement() as ExcalidrawLinearElement;
|
||||||
|
|
||||||
|
// Arrow should have end binding to rectangle
|
||||||
|
expect(arrow.endBinding?.elementId).toBe(rectangle.id);
|
||||||
|
expect(arrow.endBinding?.mode).toBe("orbit");
|
||||||
|
expect(arrow.startBinding).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create orbit binding when one of the cursor is inside rectangle", () => {
|
||||||
|
// Create a filled solid rectangle
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.downAt(100, 100);
|
||||||
|
mouse.moveTo(200, 200);
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
const rect = API.getSelectedElement();
|
||||||
|
API.updateElement(rect, {
|
||||||
|
fillStyle: "solid",
|
||||||
|
backgroundColor: "#a5d8ff",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw arrow with endpoint inside the filled rectangle, since only
|
||||||
|
// filled bindables bind inside the shape
|
||||||
|
UI.clickTool("arrow");
|
||||||
|
mouse.downAt(10, 10);
|
||||||
|
mouse.moveTo(160, 160);
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
const arrow = API.getSelectedElement() as ExcalidrawLinearElement;
|
||||||
|
expect(arrow.x).toBe(10);
|
||||||
|
expect(arrow.y).toBe(10);
|
||||||
|
expect(arrow.width).toBeCloseTo(85.75985931287957);
|
||||||
|
expect(arrow.height).toBeCloseTo(85.75985931288186);
|
||||||
|
|
||||||
|
// Should bind to the rectangle since endpoint is inside
|
||||||
|
expect(arrow.startBinding).toBe(null);
|
||||||
|
expect(arrow.endBinding?.elementId).toBe(rect.id);
|
||||||
|
|
||||||
|
const endBinding = arrow.endBinding as FixedPointBinding;
|
||||||
|
expect(endBinding.fixedPoint[0]).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(endBinding.fixedPoint[0]).toBeLessThanOrEqual(1);
|
||||||
|
expect(endBinding.fixedPoint[1]).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(endBinding.fixedPoint[1]).toBeLessThanOrEqual(1);
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
|
|
||||||
|
// Move the bindable
|
||||||
|
mouse.downAt(130, 110);
|
||||||
|
mouse.moveTo(280, 110);
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
// Check if the arrow moved
|
||||||
|
expect(arrow.x).toBe(10);
|
||||||
|
expect(arrow.y).toBe(10);
|
||||||
|
expect(arrow.width).toBeCloseTo(234);
|
||||||
|
expect(arrow.height).toBeCloseTo(117);
|
||||||
|
|
||||||
|
// Restore bindable
|
||||||
|
mouse.reset();
|
||||||
|
mouse.downAt(280, 110);
|
||||||
|
mouse.moveTo(130, 110);
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
// Move the arrow out
|
||||||
|
mouse.reset();
|
||||||
|
mouse.click(10, 10);
|
||||||
|
mouse.downAt(96.466, 96.466);
|
||||||
|
mouse.moveTo(50, 50);
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
expect(arrow.startBinding).toBe(null);
|
||||||
|
expect(arrow.endBinding).toBe(null);
|
||||||
|
|
||||||
|
// Re-bind the arrow by moving the cursor inside the rectangle
|
||||||
|
mouse.reset();
|
||||||
|
mouse.downAt(50, 50);
|
||||||
|
mouse.moveTo(150, 150);
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
|
// Check if the arrow is still on the outside
|
||||||
|
expect(arrow.width).toBeCloseTo(86, 0);
|
||||||
|
expect(arrow.height).toBeCloseTo(86, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("additional binding behavior", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
mouse.reset();
|
||||||
|
|
||||||
|
await act(() => {
|
||||||
|
return setLanguage(defaultLang);
|
||||||
|
});
|
||||||
|
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(
|
||||||
"editing arrow and moving its head to bind it to element A, finalizing the" +
|
"editing arrow and moving its head to bind it to element A, finalizing the" +
|
||||||
"editing by clicking on element A should end up selecting A",
|
"editing by clicking on element A should end up selecting A",
|
||||||
async () => {
|
async () => {
|
||||||
@ -145,61 +334,34 @@ describe("element binding", () => {
|
|||||||
mouse.down(50, -100);
|
mouse.down(50, -100);
|
||||||
mouse.up(0, 80);
|
mouse.up(0, 80);
|
||||||
|
|
||||||
// Edit arrow with multi-point
|
// Edit arrow
|
||||||
mouse.doubleClick();
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
});
|
||||||
|
|
||||||
// move arrow head
|
// move arrow head
|
||||||
mouse.down();
|
mouse.down();
|
||||||
mouse.up(0, 10);
|
mouse.up(0, 10);
|
||||||
expect(API.getSelectedElement().type).toBe("arrow");
|
expect(API.getSelectedElement().type).toBe("arrow");
|
||||||
|
|
||||||
// NOTE this mouse down/up + await needs to be done in order to repro
|
|
||||||
// the issue, due to https://github.com/excalidraw/excalidraw/blob/46bff3daceb602accf60c40a84610797260fca94/src/components/App.tsx#L740
|
|
||||||
mouse.reset();
|
|
||||||
expect(h.state.selectedLinearElement?.isEditing).toBe(true);
|
expect(h.state.selectedLinearElement?.isEditing).toBe(true);
|
||||||
mouse.down(0, 0);
|
mouse.reset();
|
||||||
await new Promise((r) => setTimeout(r, 100));
|
mouse.clickAt(-50, -50);
|
||||||
expect(h.state.selectedLinearElement?.isEditing).toBe(false);
|
expect(h.state.selectedLinearElement?.isEditing).toBe(false);
|
||||||
expect(API.getSelectedElement().type).toBe("rectangle");
|
expect(API.getSelectedElement().type).toBe("arrow");
|
||||||
mouse.up();
|
|
||||||
|
// Edit arrow
|
||||||
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
});
|
||||||
|
expect(h.state.selectedLinearElement?.isEditing).toBe(true);
|
||||||
|
mouse.reset();
|
||||||
|
mouse.clickAt(0, 0);
|
||||||
|
expect(h.state.selectedLinearElement).toBeNull();
|
||||||
expect(API.getSelectedElement().type).toBe("rectangle");
|
expect(API.getSelectedElement().type).toBe("rectangle");
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
it("should unbind arrow when moving it with keyboard", () => {
|
|
||||||
const rectangle = UI.createElement("rectangle", {
|
|
||||||
x: 75,
|
|
||||||
y: 0,
|
|
||||||
size: 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Creates arrow 1px away from bidding with rectangle
|
|
||||||
const arrow = UI.createElement("arrow", {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
size: 49,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(arrow.endBinding).toBe(null);
|
|
||||||
|
|
||||||
mouse.downAt(49, 49);
|
|
||||||
mouse.moveTo(51, 0);
|
|
||||||
mouse.up(0, 0);
|
|
||||||
|
|
||||||
// Test sticky connection
|
|
||||||
expect(API.getSelectedElement().type).toBe("arrow");
|
|
||||||
Keyboard.keyPress(KEYS.ARROW_RIGHT);
|
|
||||||
expect(arrow.endBinding?.elementId).toBe(rectangle.id);
|
|
||||||
Keyboard.keyPress(KEYS.ARROW_LEFT);
|
|
||||||
expect(arrow.endBinding?.elementId).toBe(rectangle.id);
|
|
||||||
|
|
||||||
// Sever connection
|
|
||||||
expect(API.getSelectedElement().type).toBe("arrow");
|
|
||||||
Keyboard.keyPress(KEYS.ARROW_LEFT);
|
|
||||||
expect(arrow.endBinding).toBe(null);
|
|
||||||
Keyboard.keyPress(KEYS.ARROW_RIGHT);
|
|
||||||
expect(arrow.endBinding).toBe(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should unbind on bound element deletion", () => {
|
it("should unbind on bound element deletion", () => {
|
||||||
const rectangle = UI.createElement("rectangle", {
|
const rectangle = UI.createElement("rectangle", {
|
||||||
x: 60,
|
x: 60,
|
||||||
@ -209,8 +371,8 @@ describe("element binding", () => {
|
|||||||
|
|
||||||
const arrow = UI.createElement("arrow", {
|
const arrow = UI.createElement("arrow", {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 5,
|
||||||
size: 50,
|
size: 70,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(arrow.endBinding?.elementId).toBe(rectangle.id);
|
expect(arrow.endBinding?.elementId).toBe(rectangle.id);
|
||||||
@ -221,77 +383,141 @@ describe("element binding", () => {
|
|||||||
expect(arrow.endBinding).toBe(null);
|
expect(arrow.endBinding).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should unbind on text element deletion by submitting empty text", async () => {
|
it("should unbind arrow when arrow is resized", () => {
|
||||||
const text = API.createElement({
|
const rectLeft = UI.createElement("rectangle", {
|
||||||
type: "text",
|
|
||||||
text: "ola",
|
|
||||||
x: 60,
|
|
||||||
y: 0,
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
API.setElements([text]);
|
|
||||||
|
|
||||||
const arrow = UI.createElement("arrow", {
|
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
width: 200,
|
||||||
size: 50,
|
height: 500,
|
||||||
});
|
});
|
||||||
|
const rectRight = UI.createElement("rectangle", {
|
||||||
|
x: 400,
|
||||||
|
width: 200,
|
||||||
|
height: 500,
|
||||||
|
});
|
||||||
|
const arrow = UI.createElement("arrow", {
|
||||||
|
x: 190,
|
||||||
|
y: 250,
|
||||||
|
width: 220,
|
||||||
|
height: 1,
|
||||||
|
});
|
||||||
|
expect(arrow.startBinding?.elementId).toBe(rectLeft.id);
|
||||||
|
expect(arrow.endBinding?.elementId).toBe(rectRight.id);
|
||||||
|
|
||||||
expect(arrow.endBinding?.elementId).toBe(text.id);
|
// Drag arrow off of bound rectangle range
|
||||||
|
const handles = getTransformHandles(
|
||||||
|
arrow,
|
||||||
|
h.state.zoom,
|
||||||
|
arrayToMap(h.elements),
|
||||||
|
"mouse",
|
||||||
|
).se!;
|
||||||
|
|
||||||
// edit text element and submit
|
const elX = handles[0] + handles[2] / 2;
|
||||||
// -------------------------------------------------------------------------
|
const elY = handles[1] + handles[3] / 2;
|
||||||
|
mouse.downAt(elX, elY);
|
||||||
|
mouse.moveTo(300, 400);
|
||||||
|
mouse.up();
|
||||||
|
|
||||||
UI.clickTool("text");
|
expect(arrow.startBinding).toBe(null);
|
||||||
|
|
||||||
mouse.clickAt(text.x + 50, text.y + 50);
|
|
||||||
|
|
||||||
const editor = await getTextEditor();
|
|
||||||
|
|
||||||
fireEvent.change(editor, { target: { value: "" } });
|
|
||||||
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
|
|
||||||
|
|
||||||
expect(document.querySelector(TEXT_EDITOR_SELECTOR)).toBe(null);
|
|
||||||
expect(arrow.endBinding).toBe(null);
|
expect(arrow.endBinding).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should keep binding on text update", async () => {
|
it("should unbind arrow when arrow is rotated", () => {
|
||||||
const text = API.createElement({
|
const rectLeft = UI.createElement("rectangle", {
|
||||||
type: "text",
|
|
||||||
text: "ola",
|
|
||||||
x: 60,
|
|
||||||
y: 0,
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
API.setElements([text]);
|
|
||||||
|
|
||||||
const arrow = UI.createElement("arrow", {
|
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
width: 200,
|
||||||
size: 50,
|
height: 500,
|
||||||
|
});
|
||||||
|
const rectRight = UI.createElement("rectangle", {
|
||||||
|
x: 400,
|
||||||
|
width: 200,
|
||||||
|
height: 500,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(arrow.endBinding?.elementId).toBe(text.id);
|
UI.clickTool("arrow");
|
||||||
|
mouse.reset();
|
||||||
|
mouse.clickAt(190, 250);
|
||||||
|
mouse.moveTo(300, 200);
|
||||||
|
mouse.clickAt(300, 200);
|
||||||
|
mouse.moveTo(410, 251);
|
||||||
|
mouse.clickAt(410, 251);
|
||||||
|
|
||||||
// delete text element by submitting empty text
|
const arrow = API.getSelectedElement() as ExcalidrawArrowElement;
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UI.clickTool("text");
|
expect(arrow.startBinding?.elementId).toBe(rectLeft.id);
|
||||||
|
expect(arrow.endBinding?.elementId).toBe(rectRight.id);
|
||||||
|
|
||||||
mouse.clickAt(text.x + 50, text.y + 50);
|
const rotation = getTransformHandles(
|
||||||
const editor = await getTextEditor();
|
arrow,
|
||||||
|
h.state.zoom,
|
||||||
|
arrayToMap(h.elements),
|
||||||
|
"mouse",
|
||||||
|
).rotation!;
|
||||||
|
const rotationHandleX = rotation[0] + rotation[2] / 2;
|
||||||
|
const rotationHandleY = rotation[1] + rotation[3] / 2;
|
||||||
|
mouse.reset();
|
||||||
|
mouse.down(rotationHandleX, rotationHandleY);
|
||||||
|
mouse.move(300, 400);
|
||||||
|
mouse.up();
|
||||||
|
expect(arrow.angle).toBeGreaterThan(0.7 * Math.PI);
|
||||||
|
expect(arrow.angle).toBeLessThan(1.3 * Math.PI);
|
||||||
|
expect(arrow.startBinding).toBeNull();
|
||||||
|
expect(arrow.endBinding).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
expect(editor).not.toBe(null);
|
it("should not unbind when duplicating via selection group", () => {
|
||||||
|
const rectLeft = UI.createElement("rectangle", {
|
||||||
|
x: 0,
|
||||||
|
width: 200,
|
||||||
|
height: 500,
|
||||||
|
});
|
||||||
|
const rectRight = UI.createElement("rectangle", {
|
||||||
|
x: 400,
|
||||||
|
y: 200,
|
||||||
|
width: 200,
|
||||||
|
height: 500,
|
||||||
|
});
|
||||||
|
const arrow = UI.createElement("arrow", {
|
||||||
|
x: 190,
|
||||||
|
y: 250,
|
||||||
|
width: 217,
|
||||||
|
height: 1,
|
||||||
|
});
|
||||||
|
expect(arrow.startBinding?.elementId).toBe(rectLeft.id);
|
||||||
|
expect(arrow.endBinding?.elementId).toBe(rectRight.id);
|
||||||
|
|
||||||
fireEvent.change(editor, { target: { value: "asdasdasdasdas" } });
|
mouse.downAt(-100, -100);
|
||||||
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
|
mouse.moveTo(650, 750);
|
||||||
|
mouse.up(0, 0);
|
||||||
|
|
||||||
expect(document.querySelector(TEXT_EDITOR_SELECTOR)).toBe(null);
|
expect(API.getSelectedElements().length).toBe(3);
|
||||||
expect(arrow.endBinding?.elementId).toBe(text.id);
|
|
||||||
|
mouse.moveTo(5, 5);
|
||||||
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
|
mouse.downAt(5, 5);
|
||||||
|
mouse.moveTo(1000, 1000);
|
||||||
|
mouse.up(0, 0);
|
||||||
|
|
||||||
|
expect(window.h.elements.length).toBe(6);
|
||||||
|
window.h.elements.forEach((element) => {
|
||||||
|
if (isLinearElement(element)) {
|
||||||
|
expect(element.startBinding).not.toBe(null);
|
||||||
|
expect(element.endBinding).not.toBe(null);
|
||||||
|
} else {
|
||||||
|
expect(element.boundElements).not.toBe(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("to text elements", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
mouse.reset();
|
||||||
|
|
||||||
|
await act(() => {
|
||||||
|
return setLanguage(defaultLang);
|
||||||
|
});
|
||||||
|
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should update binding when text containerized", async () => {
|
it("should update binding when text containerized", async () => {
|
||||||
@ -312,15 +538,13 @@ describe("element binding", () => {
|
|||||||
points: [pointFrom(0, 0), pointFrom(0, -87.45777932247563)],
|
points: [pointFrom(0, 0), pointFrom(0, -87.45777932247563)],
|
||||||
startBinding: {
|
startBinding: {
|
||||||
elementId: "rectangle1",
|
elementId: "rectangle1",
|
||||||
focus: 0.2,
|
|
||||||
gap: 7,
|
|
||||||
fixedPoint: [0.5, 1],
|
fixedPoint: [0.5, 1],
|
||||||
|
mode: "orbit",
|
||||||
},
|
},
|
||||||
endBinding: {
|
endBinding: {
|
||||||
elementId: "text1",
|
elementId: "text1",
|
||||||
focus: 0.2,
|
|
||||||
gap: 7,
|
|
||||||
fixedPoint: [1, 0.5],
|
fixedPoint: [1, 0.5],
|
||||||
|
mode: "orbit",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -330,15 +554,13 @@ describe("element binding", () => {
|
|||||||
points: [pointFrom(0, 0), pointFrom(0, -87.45777932247563)],
|
points: [pointFrom(0, 0), pointFrom(0, -87.45777932247563)],
|
||||||
startBinding: {
|
startBinding: {
|
||||||
elementId: "text1",
|
elementId: "text1",
|
||||||
focus: 0.2,
|
|
||||||
gap: 7,
|
|
||||||
fixedPoint: [0.5, 1],
|
fixedPoint: [0.5, 1],
|
||||||
|
mode: "orbit",
|
||||||
},
|
},
|
||||||
endBinding: {
|
endBinding: {
|
||||||
elementId: "rectangle1",
|
elementId: "rectangle1",
|
||||||
focus: 0.2,
|
|
||||||
gap: 7,
|
|
||||||
fixedPoint: [1, 0.5],
|
fixedPoint: [1, 0.5],
|
||||||
|
mode: "orbit",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -391,88 +613,77 @@ describe("element binding", () => {
|
|||||||
expect(arrow2.endBinding?.elementId).toBe(rectangle1.id);
|
expect(arrow2.endBinding?.elementId).toBe(rectangle1.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
// #6459
|
it("should keep binding on text update", async () => {
|
||||||
it("should unbind arrow only from the latest element", () => {
|
const text = API.createElement({
|
||||||
const rectLeft = UI.createElement("rectangle", {
|
type: "text",
|
||||||
x: 0,
|
text: "ola",
|
||||||
width: 200,
|
x: 60,
|
||||||
height: 500,
|
y: 0,
|
||||||
});
|
width: 100,
|
||||||
const rectRight = UI.createElement("rectangle", {
|
height: 100,
|
||||||
x: 400,
|
|
||||||
width: 200,
|
|
||||||
height: 500,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
API.setElements([text]);
|
||||||
|
|
||||||
const arrow = UI.createElement("arrow", {
|
const arrow = UI.createElement("arrow", {
|
||||||
x: 210,
|
x: 0,
|
||||||
y: 250,
|
y: 0,
|
||||||
width: 180,
|
size: 65,
|
||||||
height: 1,
|
|
||||||
});
|
});
|
||||||
expect(arrow.startBinding?.elementId).toBe(rectLeft.id);
|
|
||||||
expect(arrow.endBinding?.elementId).toBe(rectRight.id);
|
|
||||||
|
|
||||||
// Drag arrow off of bound rectangle range
|
expect(arrow.endBinding?.elementId).toBe(text.id);
|
||||||
const handles = getTransformHandles(
|
|
||||||
arrow,
|
|
||||||
h.state.zoom,
|
|
||||||
arrayToMap(h.elements),
|
|
||||||
"mouse",
|
|
||||||
).se!;
|
|
||||||
|
|
||||||
Keyboard.keyDown(KEYS.CTRL_OR_CMD);
|
// delete text element by submitting empty text
|
||||||
const elX = handles[0] + handles[2] / 2;
|
// -------------------------------------------------------------------------
|
||||||
const elY = handles[1] + handles[3] / 2;
|
|
||||||
mouse.downAt(elX, elY);
|
|
||||||
mouse.moveTo(300, 400);
|
|
||||||
mouse.up();
|
|
||||||
|
|
||||||
expect(arrow.startBinding).not.toBe(null);
|
UI.clickTool("text");
|
||||||
|
|
||||||
|
mouse.clickAt(text.x + 50, text.y + 50);
|
||||||
|
const editor = await getTextEditor();
|
||||||
|
|
||||||
|
expect(editor).not.toBe(null);
|
||||||
|
|
||||||
|
fireEvent.change(editor, { target: { value: "asdasdasdasdas" } });
|
||||||
|
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
|
||||||
|
|
||||||
|
expect(document.querySelector(TEXT_EDITOR_SELECTOR)).toBe(null);
|
||||||
|
expect(arrow.endBinding?.elementId).toBe(text.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should unbind on text element deletion by submitting empty text", async () => {
|
||||||
|
const text = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
text: "¡olá!",
|
||||||
|
x: 60,
|
||||||
|
y: 0,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([text]);
|
||||||
|
|
||||||
|
const arrow = UI.createElement("arrow", {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
size: 65,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(arrow.endBinding?.elementId).toBe(text.id);
|
||||||
|
|
||||||
|
// edit text element and submit
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UI.clickTool("text");
|
||||||
|
|
||||||
|
mouse.clickAt(text.x + 50, text.y + 50);
|
||||||
|
|
||||||
|
const editor = await getTextEditor();
|
||||||
|
|
||||||
|
fireEvent.change(editor, { target: { value: "" } });
|
||||||
|
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
|
||||||
|
|
||||||
|
expect(document.querySelector(TEXT_EDITOR_SELECTOR)).toBe(null);
|
||||||
expect(arrow.endBinding).toBe(null);
|
expect(arrow.endBinding).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not unbind when duplicating via selection group", () => {
|
|
||||||
const rectLeft = UI.createElement("rectangle", {
|
|
||||||
x: 0,
|
|
||||||
width: 200,
|
|
||||||
height: 500,
|
|
||||||
});
|
|
||||||
const rectRight = UI.createElement("rectangle", {
|
|
||||||
x: 400,
|
|
||||||
y: 200,
|
|
||||||
width: 200,
|
|
||||||
height: 500,
|
|
||||||
});
|
|
||||||
const arrow = UI.createElement("arrow", {
|
|
||||||
x: 210,
|
|
||||||
y: 250,
|
|
||||||
width: 177,
|
|
||||||
height: 1,
|
|
||||||
});
|
|
||||||
expect(arrow.startBinding?.elementId).toBe(rectLeft.id);
|
|
||||||
expect(arrow.endBinding?.elementId).toBe(rectRight.id);
|
|
||||||
|
|
||||||
mouse.downAt(-100, -100);
|
|
||||||
mouse.moveTo(650, 750);
|
|
||||||
mouse.up(0, 0);
|
|
||||||
|
|
||||||
expect(API.getSelectedElements().length).toBe(3);
|
|
||||||
|
|
||||||
mouse.moveTo(5, 5);
|
|
||||||
Keyboard.withModifierKeys({ alt: true }, () => {
|
|
||||||
mouse.downAt(5, 5);
|
|
||||||
mouse.moveTo(1000, 1000);
|
|
||||||
mouse.up(0, 0);
|
|
||||||
|
|
||||||
expect(window.h.elements.length).toBe(6);
|
|
||||||
window.h.elements.forEach((element) => {
|
|
||||||
if (isLinearElement(element)) {
|
|
||||||
expect(element.startBinding).not.toBe(null);
|
|
||||||
expect(element.endBinding).not.toBe(null);
|
|
||||||
} else {
|
|
||||||
expect(element.boundElements).not.toBe(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -144,9 +144,8 @@ describe("duplicating multiple elements", () => {
|
|||||||
id: "arrow1",
|
id: "arrow1",
|
||||||
startBinding: {
|
startBinding: {
|
||||||
elementId: "rectangle1",
|
elementId: "rectangle1",
|
||||||
focus: 0.2,
|
|
||||||
gap: 7,
|
|
||||||
fixedPoint: [0.5, 1],
|
fixedPoint: [0.5, 1],
|
||||||
|
mode: "orbit",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -155,9 +154,8 @@ describe("duplicating multiple elements", () => {
|
|||||||
id: "arrow2",
|
id: "arrow2",
|
||||||
endBinding: {
|
endBinding: {
|
||||||
elementId: "rectangle1",
|
elementId: "rectangle1",
|
||||||
focus: 0.2,
|
|
||||||
gap: 7,
|
|
||||||
fixedPoint: [0.5, 1],
|
fixedPoint: [0.5, 1],
|
||||||
|
mode: "orbit",
|
||||||
},
|
},
|
||||||
boundElements: [{ id: "text2", type: "text" }],
|
boundElements: [{ id: "text2", type: "text" }],
|
||||||
});
|
});
|
||||||
@ -276,9 +274,8 @@ describe("duplicating multiple elements", () => {
|
|||||||
id: "arrow1",
|
id: "arrow1",
|
||||||
startBinding: {
|
startBinding: {
|
||||||
elementId: "rectangle1",
|
elementId: "rectangle1",
|
||||||
focus: 0.2,
|
|
||||||
gap: 7,
|
|
||||||
fixedPoint: [0.5, 1],
|
fixedPoint: [0.5, 1],
|
||||||
|
mode: "orbit",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -293,15 +290,13 @@ describe("duplicating multiple elements", () => {
|
|||||||
id: "arrow2",
|
id: "arrow2",
|
||||||
startBinding: {
|
startBinding: {
|
||||||
elementId: "rectangle1",
|
elementId: "rectangle1",
|
||||||
focus: 0.2,
|
|
||||||
gap: 7,
|
|
||||||
fixedPoint: [0.5, 1],
|
fixedPoint: [0.5, 1],
|
||||||
|
mode: "orbit",
|
||||||
},
|
},
|
||||||
endBinding: {
|
endBinding: {
|
||||||
elementId: "rectangle-not-exists",
|
elementId: "rectangle-not-exists",
|
||||||
focus: 0.2,
|
|
||||||
gap: 7,
|
|
||||||
fixedPoint: [0.5, 1],
|
fixedPoint: [0.5, 1],
|
||||||
|
mode: "orbit",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -310,15 +305,13 @@ describe("duplicating multiple elements", () => {
|
|||||||
id: "arrow3",
|
id: "arrow3",
|
||||||
startBinding: {
|
startBinding: {
|
||||||
elementId: "rectangle-not-exists",
|
elementId: "rectangle-not-exists",
|
||||||
focus: 0.2,
|
|
||||||
gap: 7,
|
|
||||||
fixedPoint: [0.5, 1],
|
fixedPoint: [0.5, 1],
|
||||||
|
mode: "orbit",
|
||||||
},
|
},
|
||||||
endBinding: {
|
endBinding: {
|
||||||
elementId: "rectangle1",
|
elementId: "rectangle1",
|
||||||
focus: 0.2,
|
|
||||||
gap: 7,
|
|
||||||
fixedPoint: [0.5, 1],
|
fixedPoint: [0.5, 1],
|
||||||
|
mode: "orbit",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -821,7 +814,7 @@ describe("duplication z-order", () => {
|
|||||||
const arrow = UI.createElement("arrow", {
|
const arrow = UI.createElement("arrow", {
|
||||||
x: -100,
|
x: -100,
|
||||||
y: 50,
|
y: 50,
|
||||||
width: 95,
|
width: 115,
|
||||||
height: 0,
|
height: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,10 @@
|
|||||||
import { ARROW_TYPE } from "@excalidraw/common";
|
import { ARROW_TYPE } from "@excalidraw/common";
|
||||||
import { pointFrom } from "@excalidraw/math";
|
import { pointFrom } from "@excalidraw/math";
|
||||||
import { Excalidraw } from "@excalidraw/excalidraw";
|
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
import { actionSelectAll } from "@excalidraw/excalidraw/actions";
|
import { actionSelectAll } from "@excalidraw/excalidraw/actions";
|
||||||
import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions/actionDuplicateSelection";
|
import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions/actionDuplicateSelection";
|
||||||
|
|
||||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
import { Pointer, UI } from "@excalidraw/excalidraw/tests/helpers/ui";
|
import { Pointer, UI } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
act,
|
act,
|
||||||
fireEvent,
|
fireEvent,
|
||||||
@ -15,13 +12,11 @@ import {
|
|||||||
queryByTestId,
|
queryByTestId,
|
||||||
render,
|
render,
|
||||||
} from "@excalidraw/excalidraw/tests/test-utils";
|
} from "@excalidraw/excalidraw/tests/test-utils";
|
||||||
|
|
||||||
import "@excalidraw/utils/test-utils";
|
import "@excalidraw/utils/test-utils";
|
||||||
|
import { bindBindingElement } from "@excalidraw/element";
|
||||||
|
|
||||||
import type { LocalPoint } from "@excalidraw/math";
|
import type { LocalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
import { bindLinearElement } from "../src/binding";
|
|
||||||
|
|
||||||
import { Scene } from "../src/Scene";
|
import { Scene } from "../src/Scene";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@ -136,6 +131,11 @@ describe("elbow arrow segment move", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("elbow arrow routing", () => {
|
describe("elbow arrow routing", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
localStorage.clear();
|
||||||
|
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||||
|
});
|
||||||
|
|
||||||
it("can properly generate orthogonal arrow points", () => {
|
it("can properly generate orthogonal arrow points", () => {
|
||||||
const scene = new Scene();
|
const scene = new Scene();
|
||||||
const arrow = API.createElement({
|
const arrow = API.createElement({
|
||||||
@ -160,8 +160,8 @@ describe("elbow arrow routing", () => {
|
|||||||
expect(arrow.width).toEqual(90);
|
expect(arrow.width).toEqual(90);
|
||||||
expect(arrow.height).toEqual(200);
|
expect(arrow.height).toEqual(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can generate proper points for bound elbow arrow", () => {
|
it("can generate proper points for bound elbow arrow", () => {
|
||||||
const scene = new Scene();
|
|
||||||
const rectangle1 = API.createElement({
|
const rectangle1 = API.createElement({
|
||||||
type: "rectangle",
|
type: "rectangle",
|
||||||
x: -150,
|
x: -150,
|
||||||
@ -185,25 +185,23 @@ describe("elbow arrow routing", () => {
|
|||||||
height: 200,
|
height: 200,
|
||||||
points: [pointFrom(0, 0), pointFrom(90, 200)],
|
points: [pointFrom(0, 0), pointFrom(90, 200)],
|
||||||
}) as ExcalidrawElbowArrowElement;
|
}) as ExcalidrawElbowArrowElement;
|
||||||
scene.insertElement(rectangle1);
|
API.setElements([rectangle1, rectangle2, arrow]);
|
||||||
scene.insertElement(rectangle2);
|
|
||||||
scene.insertElement(arrow);
|
|
||||||
|
|
||||||
bindLinearElement(arrow, rectangle1, "start", scene);
|
bindBindingElement(arrow, rectangle1, "orbit", "start", h.scene);
|
||||||
bindLinearElement(arrow, rectangle2, "end", scene);
|
bindBindingElement(arrow, rectangle2, "orbit", "end", h.scene);
|
||||||
|
|
||||||
expect(arrow.startBinding).not.toBe(null);
|
expect(arrow.startBinding).not.toBe(null);
|
||||||
expect(arrow.endBinding).not.toBe(null);
|
expect(arrow.endBinding).not.toBe(null);
|
||||||
|
|
||||||
h.app.scene.mutateElement(arrow, {
|
h.scene.mutateElement(arrow, {
|
||||||
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)],
|
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(arrow.points).toEqual([
|
expect(arrow.points).toEqual([
|
||||||
[0, 0],
|
[0, 0],
|
||||||
[45, 0],
|
[44, 0],
|
||||||
[45, 200],
|
[44, 200],
|
||||||
[90, 200],
|
[88, 200],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -242,9 +240,9 @@ describe("elbow arrow ui", () => {
|
|||||||
expect(h.state.currentItemArrowType).toBe(ARROW_TYPE.elbow);
|
expect(h.state.currentItemArrowType).toBe(ARROW_TYPE.elbow);
|
||||||
|
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
mouse.moveTo(-43, -99);
|
mouse.moveTo(-53, -99);
|
||||||
mouse.click();
|
mouse.click();
|
||||||
mouse.moveTo(43, 99);
|
mouse.moveTo(53, 99);
|
||||||
mouse.click();
|
mouse.click();
|
||||||
|
|
||||||
const arrow = h.scene.getSelectedElements(
|
const arrow = h.scene.getSelectedElements(
|
||||||
@ -255,9 +253,9 @@ describe("elbow arrow ui", () => {
|
|||||||
expect(arrow.elbowed).toBe(true);
|
expect(arrow.elbowed).toBe(true);
|
||||||
expect(arrow.points).toEqual([
|
expect(arrow.points).toEqual([
|
||||||
[0, 0],
|
[0, 0],
|
||||||
[45, 0],
|
[44, 0],
|
||||||
[45, 200],
|
[44, 200],
|
||||||
[90, 200],
|
[88, 200],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -279,9 +277,9 @@ describe("elbow arrow ui", () => {
|
|||||||
UI.clickOnTestId("elbow-arrow");
|
UI.clickOnTestId("elbow-arrow");
|
||||||
|
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
mouse.moveTo(-43, -99);
|
mouse.moveTo(-53, -99);
|
||||||
mouse.click();
|
mouse.click();
|
||||||
mouse.moveTo(43, 99);
|
mouse.moveTo(53, 99);
|
||||||
mouse.click();
|
mouse.click();
|
||||||
|
|
||||||
const arrow = h.scene.getSelectedElements(
|
const arrow = h.scene.getSelectedElements(
|
||||||
@ -297,9 +295,11 @@ describe("elbow arrow ui", () => {
|
|||||||
|
|
||||||
expect(arrow.points.map((point) => point.map(Math.round))).toEqual([
|
expect(arrow.points.map((point) => point.map(Math.round))).toEqual([
|
||||||
[0, 0],
|
[0, 0],
|
||||||
[35, 0],
|
[36, 0],
|
||||||
[35, 165],
|
[36, 90],
|
||||||
[103, 165],
|
[28, 90],
|
||||||
|
[28, 164],
|
||||||
|
[101, 164],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -321,9 +321,9 @@ describe("elbow arrow ui", () => {
|
|||||||
UI.clickOnTestId("elbow-arrow");
|
UI.clickOnTestId("elbow-arrow");
|
||||||
|
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
mouse.moveTo(-43, -99);
|
mouse.moveTo(-53, -99);
|
||||||
mouse.click();
|
mouse.click();
|
||||||
mouse.moveTo(43, 99);
|
mouse.moveTo(53, 99);
|
||||||
mouse.click();
|
mouse.click();
|
||||||
|
|
||||||
const arrow = h.scene.getSelectedElements(
|
const arrow = h.scene.getSelectedElements(
|
||||||
@ -353,9 +353,9 @@ describe("elbow arrow ui", () => {
|
|||||||
expect(duplicatedArrow.elbowed).toBe(true);
|
expect(duplicatedArrow.elbowed).toBe(true);
|
||||||
expect(duplicatedArrow.points).toEqual([
|
expect(duplicatedArrow.points).toEqual([
|
||||||
[0, 0],
|
[0, 0],
|
||||||
[45, 0],
|
[44, 0],
|
||||||
[45, 200],
|
[44, 200],
|
||||||
[90, 200],
|
[88, 200],
|
||||||
]);
|
]);
|
||||||
expect(arrow.startBinding).not.toBe(null);
|
expect(arrow.startBinding).not.toBe(null);
|
||||||
expect(arrow.endBinding).not.toBe(null);
|
expect(arrow.endBinding).not.toBe(null);
|
||||||
@ -379,9 +379,9 @@ describe("elbow arrow ui", () => {
|
|||||||
UI.clickOnTestId("elbow-arrow");
|
UI.clickOnTestId("elbow-arrow");
|
||||||
|
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
mouse.moveTo(-43, -99);
|
mouse.moveTo(-53, -99);
|
||||||
mouse.click();
|
mouse.click();
|
||||||
mouse.moveTo(43, 99);
|
mouse.moveTo(53, 99);
|
||||||
mouse.click();
|
mouse.click();
|
||||||
|
|
||||||
const arrow = h.scene.getSelectedElements(
|
const arrow = h.scene.getSelectedElements(
|
||||||
@ -408,8 +408,8 @@ describe("elbow arrow ui", () => {
|
|||||||
expect(duplicatedArrow.points).toEqual([
|
expect(duplicatedArrow.points).toEqual([
|
||||||
[0, 0],
|
[0, 0],
|
||||||
[0, 100],
|
[0, 100],
|
||||||
[90, 100],
|
[88, 100],
|
||||||
[90, 200],
|
[88, 200],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -217,7 +217,7 @@ describe("Test Linear Elements", () => {
|
|||||||
|
|
||||||
// drag line from midpoint
|
// drag line from midpoint
|
||||||
drag(midpoint, pointFrom(midpoint[0] + delta, midpoint[1] + delta));
|
drag(midpoint, pointFrom(midpoint[0] + delta, midpoint[1] + delta));
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`9`);
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||||
expect(line.points.length).toEqual(3);
|
expect(line.points.length).toEqual(3);
|
||||||
expect(line.points).toMatchInlineSnapshot(`
|
expect(line.points).toMatchInlineSnapshot(`
|
||||||
@ -329,7 +329,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(h.state.selectedLinearElement?.isEditing).toBe(false);
|
expect(h.state.selectedLinearElement?.isEditing).toBe(false);
|
||||||
|
|
||||||
mouse.doubleClick();
|
mouse.doubleClick();
|
||||||
expect(h.state.selectedLinearElement).toBe(null);
|
expect(h.state.selectedLinearElement?.isEditing).toBe(false);
|
||||||
await getTextEditor();
|
await getTextEditor();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -357,6 +357,7 @@ describe("Test Linear Elements", () => {
|
|||||||
const originalY = line.y;
|
const originalY = line.y;
|
||||||
enterLineEditingMode(line);
|
enterLineEditingMode(line);
|
||||||
|
|
||||||
|
expect(h.state.selectedLinearElement?.isEditing).toBe(true);
|
||||||
expect(line.points.length).toEqual(2);
|
expect(line.points.length).toEqual(2);
|
||||||
|
|
||||||
mouse.clickAt(midpoint[0], midpoint[1]);
|
mouse.clickAt(midpoint[0], midpoint[1]);
|
||||||
@ -379,7 +380,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`11`,
|
`11`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||||
|
|
||||||
expect(line.points.length).toEqual(3);
|
expect(line.points.length).toEqual(3);
|
||||||
expect(line.points).toMatchInlineSnapshot(`
|
expect(line.points).toMatchInlineSnapshot(`
|
||||||
@ -549,7 +550,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`14`,
|
`14`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`9`);
|
||||||
|
|
||||||
expect(line.points.length).toEqual(5);
|
expect(line.points.length).toEqual(5);
|
||||||
|
|
||||||
@ -600,7 +601,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`11`,
|
`11`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||||
|
|
||||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
||||||
line,
|
line,
|
||||||
@ -641,7 +642,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`11`,
|
`11`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||||
|
|
||||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
||||||
line,
|
line,
|
||||||
@ -689,7 +690,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`17`,
|
`17`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`10`);
|
||||||
|
|
||||||
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
||||||
line,
|
line,
|
||||||
@ -747,7 +748,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`14`,
|
`14`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`9`);
|
||||||
expect(line.points.length).toEqual(5);
|
expect(line.points.length).toEqual(5);
|
||||||
|
|
||||||
expect((h.elements[0] as ExcalidrawLinearElement).points)
|
expect((h.elements[0] as ExcalidrawLinearElement).points)
|
||||||
@ -845,7 +846,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`11`,
|
`11`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||||
|
|
||||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
||||||
line,
|
line,
|
||||||
@ -1303,7 +1304,7 @@ describe("Test Linear Elements", () => {
|
|||||||
const arrow = UI.createElement("arrow", {
|
const arrow = UI.createElement("arrow", {
|
||||||
x: -10,
|
x: -10,
|
||||||
y: 250,
|
y: 250,
|
||||||
width: 400,
|
width: 410,
|
||||||
height: 1,
|
height: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1316,7 +1317,7 @@ describe("Test Linear Elements", () => {
|
|||||||
const textElement = h.elements[2] as ExcalidrawTextElementWithContainer;
|
const textElement = h.elements[2] as ExcalidrawTextElementWithContainer;
|
||||||
|
|
||||||
expect(arrow.endBinding?.elementId).toBe(rect.id);
|
expect(arrow.endBinding?.elementId).toBe(rect.id);
|
||||||
expect(arrow.width).toBe(400);
|
expect(arrow.width).toBeCloseTo(404);
|
||||||
expect(rect.x).toBe(400);
|
expect(rect.x).toBe(400);
|
||||||
expect(rect.y).toBe(0);
|
expect(rect.y).toBe(0);
|
||||||
expect(
|
expect(
|
||||||
@ -1335,7 +1336,7 @@ describe("Test Linear Elements", () => {
|
|||||||
mouse.downAt(rect.x, rect.y);
|
mouse.downAt(rect.x, rect.y);
|
||||||
mouse.moveTo(200, 0);
|
mouse.moveTo(200, 0);
|
||||||
mouse.upAt(200, 0);
|
mouse.upAt(200, 0);
|
||||||
expect(arrow.width).toBeCloseTo(200, 0);
|
expect(arrow.width).toBeCloseTo(204);
|
||||||
expect(rect.x).toBe(200);
|
expect(rect.x).toBe(200);
|
||||||
expect(rect.y).toBe(0);
|
expect(rect.y).toBe(0);
|
||||||
expect(handleBindTextResizeSpy).toHaveBeenCalledWith(
|
expect(handleBindTextResizeSpy).toHaveBeenCalledWith(
|
||||||
|
|||||||
@ -174,29 +174,29 @@ describe("generic element", () => {
|
|||||||
expect(rectangle.angle).toBeCloseTo(0);
|
expect(rectangle.angle).toBeCloseTo(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resizes with bound arrow", async () => {
|
// it("resizes with bound arrow", async () => {
|
||||||
const rectangle = UI.createElement("rectangle", {
|
// const rectangle = UI.createElement("rectangle", {
|
||||||
width: 200,
|
// width: 200,
|
||||||
height: 100,
|
// height: 100,
|
||||||
});
|
// });
|
||||||
const arrow = UI.createElement("arrow", {
|
// const arrow = UI.createElement("arrow", {
|
||||||
x: -30,
|
// x: -30,
|
||||||
y: 50,
|
// y: 50,
|
||||||
width: 28,
|
// width: 28,
|
||||||
height: 5,
|
// height: 5,
|
||||||
});
|
// });
|
||||||
|
|
||||||
expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
|
// expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
|
||||||
|
|
||||||
UI.resize(rectangle, "e", [40, 0]);
|
// UI.resize(rectangle, "e", [40, 0]);
|
||||||
|
|
||||||
expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(30, 0);
|
// expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(30, 0);
|
||||||
|
|
||||||
UI.resize(rectangle, "w", [50, 0]);
|
// UI.resize(rectangle, "w", [50, 0]);
|
||||||
|
|
||||||
expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
|
// expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
|
||||||
expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(80, 0);
|
// expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(80, 0);
|
||||||
});
|
// });
|
||||||
|
|
||||||
it("resizes with a label", async () => {
|
it("resizes with a label", async () => {
|
||||||
const rectangle = UI.createElement("rectangle", {
|
const rectangle = UI.createElement("rectangle", {
|
||||||
@ -510,12 +510,12 @@ describe("arrow element", () => {
|
|||||||
h.state,
|
h.state,
|
||||||
)[0] as ExcalidrawElbowArrowElement;
|
)[0] as ExcalidrawElbowArrowElement;
|
||||||
|
|
||||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.06);
|
||||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||||
|
|
||||||
UI.resize(rectangle, "se", [-200, -150]);
|
UI.resize(rectangle, "se", [-200, -150]);
|
||||||
|
|
||||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.06);
|
||||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -538,11 +538,11 @@ describe("arrow element", () => {
|
|||||||
h.state,
|
h.state,
|
||||||
)[0] as ExcalidrawElbowArrowElement;
|
)[0] as ExcalidrawElbowArrowElement;
|
||||||
|
|
||||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.06);
|
||||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||||
|
|
||||||
UI.resize([rectangle, arrow], "nw", [300, 350]);
|
UI.resize([rectangle, arrow], "nw", [300, 350]);
|
||||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(-0.05);
|
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(-0.06);
|
||||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.25);
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.25);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -595,31 +595,31 @@ describe("text element", () => {
|
|||||||
expect(text.fontSize).toBeCloseTo(fontSize * scale);
|
expect(text.fontSize).toBeCloseTo(fontSize * scale);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resizes with bound arrow", async () => {
|
// it("resizes with bound arrow", async () => {
|
||||||
const text = UI.createElement("text");
|
// const text = UI.createElement("text");
|
||||||
await UI.editText(text, "hello\nworld");
|
// await UI.editText(text, "hello\nworld");
|
||||||
const boundArrow = UI.createElement("arrow", {
|
// const boundArrow = UI.createElement("arrow", {
|
||||||
x: -30,
|
// x: -30,
|
||||||
y: 25,
|
// y: 25,
|
||||||
width: 28,
|
// width: 28,
|
||||||
height: 5,
|
// height: 5,
|
||||||
});
|
// });
|
||||||
|
|
||||||
expect(boundArrow.endBinding?.elementId).toEqual(text.id);
|
// expect(boundArrow.endBinding?.elementId).toEqual(text.id);
|
||||||
|
|
||||||
UI.resize(text, "ne", [40, 0]);
|
// UI.resize(text, "ne", [40, 0]);
|
||||||
|
|
||||||
expect(boundArrow.width + boundArrow.endBinding!.gap).toBeCloseTo(30);
|
// expect(boundArrow.width + boundArrow.endBinding!.gap).toBeCloseTo(30);
|
||||||
|
|
||||||
const textWidth = text.width;
|
// const textWidth = text.width;
|
||||||
const scale = 20 / text.height;
|
// const scale = 20 / text.height;
|
||||||
UI.resize(text, "nw", [50, 20]);
|
// UI.resize(text, "nw", [50, 20]);
|
||||||
|
|
||||||
expect(boundArrow.endBinding?.elementId).toEqual(text.id);
|
// expect(boundArrow.endBinding?.elementId).toEqual(text.id);
|
||||||
expect(boundArrow.width + boundArrow.endBinding!.gap).toBeCloseTo(
|
// expect(boundArrow.width + boundArrow.endBinding!.gap).toBeCloseTo(
|
||||||
30 + textWidth * scale,
|
// 30 + textWidth * scale,
|
||||||
);
|
// );
|
||||||
});
|
// });
|
||||||
|
|
||||||
it("updates font size via keyboard", async () => {
|
it("updates font size via keyboard", async () => {
|
||||||
const text = UI.createElement("text");
|
const text = UI.createElement("text");
|
||||||
@ -801,36 +801,36 @@ describe("image element", () => {
|
|||||||
expect(image.scale).toEqual([1, 1]);
|
expect(image.scale).toEqual([1, 1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resizes with bound arrow", async () => {
|
// it("resizes with bound arrow", async () => {
|
||||||
const image = API.createElement({
|
// const image = API.createElement({
|
||||||
type: "image",
|
// type: "image",
|
||||||
width: 100,
|
// width: 100,
|
||||||
height: 100,
|
// height: 100,
|
||||||
});
|
// });
|
||||||
API.setElements([image]);
|
// API.setElements([image]);
|
||||||
const arrow = UI.createElement("arrow", {
|
// const arrow = UI.createElement("arrow", {
|
||||||
x: -30,
|
// x: -30,
|
||||||
y: 50,
|
// y: 50,
|
||||||
width: 28,
|
// width: 28,
|
||||||
height: 5,
|
// height: 5,
|
||||||
});
|
// });
|
||||||
|
|
||||||
expect(arrow.endBinding?.elementId).toEqual(image.id);
|
// expect(arrow.endBinding?.elementId).toEqual(image.id);
|
||||||
|
|
||||||
UI.resize(image, "ne", [40, 0]);
|
// UI.resize(image, "ne", [40, 0]);
|
||||||
|
|
||||||
expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(30, 0);
|
// expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(30, 0);
|
||||||
|
|
||||||
const imageWidth = image.width;
|
// const imageWidth = image.width;
|
||||||
const scale = 20 / image.height;
|
// const scale = 20 / image.height;
|
||||||
UI.resize(image, "nw", [50, 20]);
|
// UI.resize(image, "nw", [50, 20]);
|
||||||
|
|
||||||
expect(arrow.endBinding?.elementId).toEqual(image.id);
|
// expect(arrow.endBinding?.elementId).toEqual(image.id);
|
||||||
expect(Math.floor(arrow.width + arrow.endBinding!.gap)).toBeCloseTo(
|
// expect(Math.floor(arrow.width + arrow.endBinding!.gap)).toBeCloseTo(
|
||||||
30 + imageWidth * scale,
|
// 30 + imageWidth * scale,
|
||||||
0,
|
// 0,
|
||||||
);
|
// );
|
||||||
});
|
// });
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("multiple selection", () => {
|
describe("multiple selection", () => {
|
||||||
@ -997,68 +997,80 @@ describe("multiple selection", () => {
|
|||||||
expect(diagLine.angle).toEqual(0);
|
expect(diagLine.angle).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resizes with bound arrows", async () => {
|
// it("resizes with bound arrows", async () => {
|
||||||
const rectangle = UI.createElement("rectangle", {
|
// const rectangle = UI.createElement("rectangle", {
|
||||||
position: 0,
|
// position: 0,
|
||||||
size: 100,
|
// size: 100,
|
||||||
});
|
// });
|
||||||
const leftBoundArrow = UI.createElement("arrow", {
|
// const leftBoundArrow = UI.createElement("arrow", {
|
||||||
x: -110,
|
// x: -110,
|
||||||
y: 50,
|
// y: 50,
|
||||||
width: 100,
|
// width: 100,
|
||||||
height: 0,
|
// height: 0,
|
||||||
});
|
// });
|
||||||
|
|
||||||
const rightBoundArrow = UI.createElement("arrow", {
|
// const rightBoundArrow = UI.createElement("arrow", {
|
||||||
x: 210,
|
// x: 210,
|
||||||
y: 50,
|
// y: 50,
|
||||||
width: -100,
|
// width: -100,
|
||||||
height: 0,
|
// height: 0,
|
||||||
});
|
// });
|
||||||
|
|
||||||
const selectionWidth = 210;
|
// const selectionWidth = 210;
|
||||||
const selectionHeight = 100;
|
// const selectionHeight = 100;
|
||||||
const move = [40, 40] as [number, number];
|
// const move = [40, 40] as [number, number];
|
||||||
const scale = Math.max(
|
// const scale = Math.max(
|
||||||
1 - move[0] / selectionWidth,
|
// 1 - move[0] / selectionWidth,
|
||||||
1 - move[1] / selectionHeight,
|
// 1 - move[1] / selectionHeight,
|
||||||
);
|
// );
|
||||||
const leftArrowBinding = { ...leftBoundArrow.endBinding };
|
// const leftArrowBinding: {
|
||||||
const rightArrowBinding = { ...rightBoundArrow.endBinding };
|
// elementId: string;
|
||||||
delete rightArrowBinding.gap;
|
// gap?: number;
|
||||||
|
// focus?: number;
|
||||||
|
// } = {
|
||||||
|
// ...leftBoundArrow.endBinding,
|
||||||
|
// } as PointBinding;
|
||||||
|
// const rightArrowBinding: {
|
||||||
|
// elementId: string;
|
||||||
|
// gap?: number;
|
||||||
|
// focus?: number;
|
||||||
|
// } = {
|
||||||
|
// ...rightBoundArrow.endBinding,
|
||||||
|
// } as PointBinding;
|
||||||
|
// delete rightArrowBinding.gap;
|
||||||
|
|
||||||
UI.resize([rectangle, rightBoundArrow], "nw", move, {
|
// UI.resize([rectangle, rightBoundArrow], "nw", move, {
|
||||||
shift: true,
|
// shift: true,
|
||||||
});
|
// });
|
||||||
|
|
||||||
expect(leftBoundArrow.x).toBeCloseTo(-110);
|
// expect(leftBoundArrow.x).toBeCloseTo(-110);
|
||||||
expect(leftBoundArrow.y).toBeCloseTo(50);
|
// expect(leftBoundArrow.y).toBeCloseTo(50);
|
||||||
expect(leftBoundArrow.width).toBeCloseTo(140, 0);
|
// expect(leftBoundArrow.width).toBeCloseTo(140, 0);
|
||||||
expect(leftBoundArrow.height).toBeCloseTo(7, 0);
|
// expect(leftBoundArrow.height).toBeCloseTo(7, 0);
|
||||||
expect(leftBoundArrow.angle).toEqual(0);
|
// expect(leftBoundArrow.angle).toEqual(0);
|
||||||
expect(leftBoundArrow.startBinding).toBeNull();
|
// expect(leftBoundArrow.startBinding).toBeNull();
|
||||||
expect(leftBoundArrow.endBinding?.gap).toBeCloseTo(10);
|
// expect(leftBoundArrow.endBinding?.gap).toBeCloseTo(10);
|
||||||
expect(leftBoundArrow.endBinding?.elementId).toBe(
|
// expect(leftBoundArrow.endBinding?.elementId).toBe(
|
||||||
leftArrowBinding.elementId,
|
// leftArrowBinding.elementId,
|
||||||
);
|
// );
|
||||||
expect(leftBoundArrow.endBinding?.focus).toBe(leftArrowBinding.focus);
|
// expect(leftBoundArrow.endBinding?.focus).toBe(leftArrowBinding.focus);
|
||||||
|
|
||||||
expect(rightBoundArrow.x).toBeCloseTo(210);
|
// expect(rightBoundArrow.x).toBeCloseTo(210);
|
||||||
expect(rightBoundArrow.y).toBeCloseTo(
|
// expect(rightBoundArrow.y).toBeCloseTo(
|
||||||
(selectionHeight - 50) * (1 - scale) + 50,
|
// (selectionHeight - 50) * (1 - scale) + 50,
|
||||||
);
|
// );
|
||||||
expect(rightBoundArrow.width).toBeCloseTo(100 * scale);
|
// expect(rightBoundArrow.width).toBeCloseTo(100 * scale);
|
||||||
expect(rightBoundArrow.height).toBeCloseTo(0);
|
// expect(rightBoundArrow.height).toBeCloseTo(0);
|
||||||
expect(rightBoundArrow.angle).toEqual(0);
|
// expect(rightBoundArrow.angle).toEqual(0);
|
||||||
expect(rightBoundArrow.startBinding).toBeNull();
|
// expect(rightBoundArrow.startBinding).toBeNull();
|
||||||
expect(rightBoundArrow.endBinding?.gap).toBeCloseTo(8.0952);
|
// expect(rightBoundArrow.endBinding?.gap).toBeCloseTo(8.0952);
|
||||||
expect(rightBoundArrow.endBinding?.elementId).toBe(
|
// expect(rightBoundArrow.endBinding?.elementId).toBe(
|
||||||
rightArrowBinding.elementId,
|
// rightArrowBinding.elementId,
|
||||||
);
|
// );
|
||||||
expect(rightBoundArrow.endBinding?.focus).toBeCloseTo(
|
// expect(rightBoundArrow.endBinding?.focus).toBeCloseTo(
|
||||||
rightArrowBinding.focus!,
|
// rightArrowBinding.focus!,
|
||||||
);
|
// );
|
||||||
});
|
// });
|
||||||
|
|
||||||
it("resizes with labeled arrows", async () => {
|
it("resizes with labeled arrows", async () => {
|
||||||
const topArrow = UI.createElement("arrow", {
|
const topArrow = UI.createElement("arrow", {
|
||||||
@ -1338,8 +1350,8 @@ describe("multiple selection", () => {
|
|||||||
|
|
||||||
expect(boundArrow.x).toBeCloseTo(380 * scaleX);
|
expect(boundArrow.x).toBeCloseTo(380 * scaleX);
|
||||||
expect(boundArrow.y).toBeCloseTo(240 * scaleY);
|
expect(boundArrow.y).toBeCloseTo(240 * scaleY);
|
||||||
expect(boundArrow.points[1][0]).toBeCloseTo(-60 * scaleX);
|
expect(boundArrow.points[1][0]).toBeCloseTo(66.3157);
|
||||||
expect(boundArrow.points[1][1]).toBeCloseTo(-80 * scaleY);
|
expect(boundArrow.points[1][1]).toBeCloseTo(-88.421);
|
||||||
|
|
||||||
expect(arrowLabelPos.x + arrowLabel.width / 2).toBeCloseTo(
|
expect(arrowLabelPos.x + arrowLabel.width / 2).toBeCloseTo(
|
||||||
boundArrow.x + boundArrow.points[1][0] / 2,
|
boundArrow.x + boundArrow.points[1][0] / 2,
|
||||||
|
|||||||
@ -51,7 +51,7 @@ import { register } from "./register";
|
|||||||
|
|
||||||
import type { AppState, Offsets } from "../types";
|
import type { AppState, Offsets } from "../types";
|
||||||
|
|
||||||
export const actionChangeViewBackgroundColor = register({
|
export const actionChangeViewBackgroundColor = register<Partial<AppState>>({
|
||||||
name: "changeViewBackgroundColor",
|
name: "changeViewBackgroundColor",
|
||||||
label: "labels.canvasBackground",
|
label: "labels.canvasBackground",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
@ -64,7 +64,7 @@ export const actionChangeViewBackgroundColor = register({
|
|||||||
perform: (_, appState, value) => {
|
perform: (_, appState, value) => {
|
||||||
return {
|
return {
|
||||||
appState: { ...appState, ...value },
|
appState: { ...appState, ...value },
|
||||||
captureUpdate: !!value.viewBackgroundColor
|
captureUpdate: !!value?.viewBackgroundColor
|
||||||
? CaptureUpdateAction.IMMEDIATELY
|
? CaptureUpdateAction.IMMEDIATELY
|
||||||
: CaptureUpdateAction.EVENTUALLY,
|
: CaptureUpdateAction.EVENTUALLY,
|
||||||
};
|
};
|
||||||
@ -464,7 +464,7 @@ export const actionZoomToFit = register({
|
|||||||
!event[KEYS.CTRL_OR_CMD],
|
!event[KEYS.CTRL_OR_CMD],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionToggleTheme = register({
|
export const actionToggleTheme = register<AppState["theme"]>({
|
||||||
name: "toggleTheme",
|
name: "toggleTheme",
|
||||||
label: (_, appState) => {
|
label: (_, appState) => {
|
||||||
return appState.theme === THEME.DARK
|
return appState.theme === THEME.DARK
|
||||||
@ -472,7 +472,8 @@ export const actionToggleTheme = register({
|
|||||||
: "buttons.darkMode";
|
: "buttons.darkMode";
|
||||||
},
|
},
|
||||||
keywords: ["toggle", "dark", "light", "mode", "theme"],
|
keywords: ["toggle", "dark", "light", "mode", "theme"],
|
||||||
icon: (appState) => (appState.theme === THEME.LIGHT ? MoonIcon : SunIcon),
|
icon: (appState, elements) =>
|
||||||
|
appState.theme === THEME.LIGHT ? MoonIcon : SunIcon,
|
||||||
viewMode: true,
|
viewMode: true,
|
||||||
trackEvent: { category: "canvas" },
|
trackEvent: { category: "canvas" },
|
||||||
perform: (_, appState, value) => {
|
perform: (_, appState, value) => {
|
||||||
|
|||||||
@ -20,12 +20,12 @@ import { t } from "../i18n";
|
|||||||
import { actionDeleteSelected } from "./actionDeleteSelected";
|
import { actionDeleteSelected } from "./actionDeleteSelected";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
export const actionCopy = register({
|
export const actionCopy = register<ClipboardEvent | null>({
|
||||||
name: "copy",
|
name: "copy",
|
||||||
label: "labels.copy",
|
label: "labels.copy",
|
||||||
icon: DuplicateIcon,
|
icon: DuplicateIcon,
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: async (elements, appState, event: ClipboardEvent | null, app) => {
|
perform: async (elements, appState, event, app) => {
|
||||||
const elementsToCopy = app.scene.getSelectedElements({
|
const elementsToCopy = app.scene.getSelectedElements({
|
||||||
selectedElementIds: appState.selectedElementIds,
|
selectedElementIds: appState.selectedElementIds,
|
||||||
includeBoundTextElement: true,
|
includeBoundTextElement: true,
|
||||||
@ -109,12 +109,12 @@ export const actionPaste = register({
|
|||||||
keyTest: undefined,
|
keyTest: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionCut = register({
|
export const actionCut = register<ClipboardEvent | null>({
|
||||||
name: "cut",
|
name: "cut",
|
||||||
label: "labels.cut",
|
label: "labels.cut",
|
||||||
icon: cutIcon,
|
icon: cutIcon,
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState, event: ClipboardEvent | null, app) => {
|
perform: (elements, appState, event, app) => {
|
||||||
actionCopy.perform(elements, appState, event, app);
|
actionCopy.perform(elements, appState, event, app);
|
||||||
return actionDeleteSelected.perform(elements, appState, null, app);
|
return actionDeleteSelected.perform(elements, appState, null, app);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -206,12 +206,8 @@ export const actionDeleteSelected = register({
|
|||||||
trackEvent: { category: "element", action: "delete" },
|
trackEvent: { category: "element", action: "delete" },
|
||||||
perform: (elements, appState, formData, app) => {
|
perform: (elements, appState, formData, app) => {
|
||||||
if (appState.selectedLinearElement?.isEditing) {
|
if (appState.selectedLinearElement?.isEditing) {
|
||||||
const {
|
const { elementId, selectedPointsIndices } =
|
||||||
elementId,
|
appState.selectedLinearElement;
|
||||||
selectedPointsIndices,
|
|
||||||
startBindingElement,
|
|
||||||
endBindingElement,
|
|
||||||
} = appState.selectedLinearElement;
|
|
||||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||||
const linearElement = LinearElementEditor.getElement(
|
const linearElement = LinearElementEditor.getElement(
|
||||||
elementId,
|
elementId,
|
||||||
@ -248,19 +244,6 @@ export const actionDeleteSelected = register({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// We cannot do this inside `movePoint` because it is also called
|
|
||||||
// when deleting the uncommitted point (which hasn't caused any binding)
|
|
||||||
const binding = {
|
|
||||||
startBindingElement: selectedPointsIndices?.includes(0)
|
|
||||||
? null
|
|
||||||
: startBindingElement,
|
|
||||||
endBindingElement: selectedPointsIndices?.includes(
|
|
||||||
linearElement.points.length - 1,
|
|
||||||
)
|
|
||||||
? null
|
|
||||||
: endBindingElement,
|
|
||||||
};
|
|
||||||
|
|
||||||
LinearElementEditor.deletePoints(
|
LinearElementEditor.deletePoints(
|
||||||
linearElement,
|
linearElement,
|
||||||
app,
|
app,
|
||||||
@ -273,7 +256,6 @@ export const actionDeleteSelected = register({
|
|||||||
...appState,
|
...appState,
|
||||||
selectedLinearElement: {
|
selectedLinearElement: {
|
||||||
...appState.selectedLinearElement,
|
...appState.selectedLinearElement,
|
||||||
...binding,
|
|
||||||
selectedPointsIndices:
|
selectedPointsIndices:
|
||||||
selectedPointsIndices?.[0] > 0
|
selectedPointsIndices?.[0] > 0
|
||||||
? [selectedPointsIndices[0] - 1]
|
? [selectedPointsIndices[0] - 1]
|
||||||
@ -302,6 +284,7 @@ export const actionDeleteSelected = register({
|
|||||||
type: app.defaultSelectionTool,
|
type: app.defaultSelectionTool,
|
||||||
}),
|
}),
|
||||||
multiElement: null,
|
multiElement: null,
|
||||||
|
newElement: null,
|
||||||
activeEmbeddable: null,
|
activeEmbeddable: null,
|
||||||
selectedLinearElement: null,
|
selectedLinearElement: null,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -31,7 +31,9 @@ import "../components/ToolIcon.scss";
|
|||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
export const actionChangeProjectName = register({
|
import type { AppState } from "../types";
|
||||||
|
|
||||||
|
export const actionChangeProjectName = register<AppState["name"]>({
|
||||||
name: "changeProjectName",
|
name: "changeProjectName",
|
||||||
label: "labels.fileTitle",
|
label: "labels.fileTitle",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
@ -51,7 +53,7 @@ export const actionChangeProjectName = register({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionChangeExportScale = register({
|
export const actionChangeExportScale = register<AppState["exportScale"]>({
|
||||||
name: "changeExportScale",
|
name: "changeExportScale",
|
||||||
label: "imageExportDialog.scale",
|
label: "imageExportDialog.scale",
|
||||||
trackEvent: { category: "export", action: "scale" },
|
trackEvent: { category: "export", action: "scale" },
|
||||||
@ -101,7 +103,9 @@ export const actionChangeExportScale = register({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionChangeExportBackground = register({
|
export const actionChangeExportBackground = register<
|
||||||
|
AppState["exportBackground"]
|
||||||
|
>({
|
||||||
name: "changeExportBackground",
|
name: "changeExportBackground",
|
||||||
label: "imageExportDialog.label.withBackground",
|
label: "imageExportDialog.label.withBackground",
|
||||||
trackEvent: { category: "export", action: "toggleBackground" },
|
trackEvent: { category: "export", action: "toggleBackground" },
|
||||||
@ -121,7 +125,9 @@ export const actionChangeExportBackground = register({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionChangeExportEmbedScene = register({
|
export const actionChangeExportEmbedScene = register<
|
||||||
|
AppState["exportEmbedScene"]
|
||||||
|
>({
|
||||||
name: "changeExportEmbedScene",
|
name: "changeExportEmbedScene",
|
||||||
label: "imageExportDialog.tooltip.embedScene",
|
label: "imageExportDialog.tooltip.embedScene",
|
||||||
trackEvent: { category: "export", action: "embedScene" },
|
trackEvent: { category: "export", action: "embedScene" },
|
||||||
@ -288,7 +294,9 @@ export const actionLoadScene = register({
|
|||||||
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.O,
|
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.O,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionExportWithDarkMode = register({
|
export const actionExportWithDarkMode = register<
|
||||||
|
AppState["exportWithDarkMode"]
|
||||||
|
>({
|
||||||
name: "exportWithDarkMode",
|
name: "exportWithDarkMode",
|
||||||
label: "imageExportDialog.label.darkMode",
|
label: "imageExportDialog.label.darkMode",
|
||||||
trackEvent: { category: "export", action: "toggleTheme" },
|
trackEvent: { category: "export", action: "toggleTheme" },
|
||||||
|
|||||||
@ -1,10 +1,6 @@
|
|||||||
import { pointFrom } from "@excalidraw/math";
|
import { pointFrom } from "@excalidraw/math";
|
||||||
|
|
||||||
import {
|
import { bindOrUnbindBindingElement } from "@excalidraw/element/binding";
|
||||||
maybeBindLinearElement,
|
|
||||||
bindOrUnbindLinearElement,
|
|
||||||
isBindingEnabled,
|
|
||||||
} from "@excalidraw/element/binding";
|
|
||||||
import {
|
import {
|
||||||
isValidPolygon,
|
isValidPolygon,
|
||||||
LinearElementEditor,
|
LinearElementEditor,
|
||||||
@ -21,7 +17,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
KEYS,
|
KEYS,
|
||||||
arrayToMap,
|
arrayToMap,
|
||||||
tupleToCoors,
|
invariant,
|
||||||
updateActiveTool,
|
updateActiveTool,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
import { isPathALoop } from "@excalidraw/element";
|
import { isPathALoop } from "@excalidraw/element";
|
||||||
@ -30,11 +26,12 @@ import { isInvisiblySmallElement } from "@excalidraw/element";
|
|||||||
|
|
||||||
import { CaptureUpdateAction } from "@excalidraw/element";
|
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||||
|
|
||||||
import type { LocalPoint } from "@excalidraw/math";
|
import type { GlobalPoint, LocalPoint } from "@excalidraw/math";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
|
PointsPositionUpdates,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
@ -46,20 +43,37 @@ import { register } from "./register";
|
|||||||
|
|
||||||
import type { AppState } from "../types";
|
import type { AppState } from "../types";
|
||||||
|
|
||||||
export const actionFinalize = register({
|
type FormData = {
|
||||||
|
event: PointerEvent;
|
||||||
|
sceneCoords: { x: number; y: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actionFinalize = register<FormData>({
|
||||||
name: "finalize",
|
name: "finalize",
|
||||||
label: "",
|
label: "",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
perform: (elements, appState, data, app) => {
|
perform: (elements, appState, data, app) => {
|
||||||
|
let newElements = elements;
|
||||||
const { interactiveCanvas, focusContainer, scene } = app;
|
const { interactiveCanvas, focusContainer, scene } = app;
|
||||||
const { event, sceneCoords } =
|
|
||||||
(data as {
|
|
||||||
event?: PointerEvent;
|
|
||||||
sceneCoords?: { x: number; y: number };
|
|
||||||
}) ?? {};
|
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
|
|
||||||
if (event && appState.selectedLinearElement) {
|
if (data && appState.selectedLinearElement) {
|
||||||
|
const { event, sceneCoords } = data;
|
||||||
|
const element = LinearElementEditor.getElement(
|
||||||
|
appState.selectedLinearElement.elementId,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
invariant(
|
||||||
|
element,
|
||||||
|
"Arrow element should exist if selectedLinearElement is set",
|
||||||
|
);
|
||||||
|
|
||||||
|
invariant(
|
||||||
|
sceneCoords,
|
||||||
|
"sceneCoords should be defined if actionFinalize is called with event",
|
||||||
|
);
|
||||||
|
|
||||||
const linearElementEditor = LinearElementEditor.handlePointerUp(
|
const linearElementEditor = LinearElementEditor.handlePointerUp(
|
||||||
event,
|
event,
|
||||||
appState.selectedLinearElement,
|
appState.selectedLinearElement,
|
||||||
@ -67,19 +81,47 @@ export const actionFinalize = register({
|
|||||||
app.scene,
|
app.scene,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { startBindingElement, endBindingElement } = linearElementEditor;
|
|
||||||
const element = app.scene.getElement(linearElementEditor.elementId);
|
|
||||||
if (isBindingElement(element)) {
|
if (isBindingElement(element)) {
|
||||||
bindOrUnbindLinearElement(
|
const newArrow = !!appState.newElement;
|
||||||
|
|
||||||
|
const selectedPointsIndices =
|
||||||
|
newArrow || !appState.selectedLinearElement.selectedPointsIndices
|
||||||
|
? [element.points.length - 1] // New arrow creation
|
||||||
|
: appState.selectedLinearElement.selectedPointsIndices;
|
||||||
|
|
||||||
|
const draggedPoints: PointsPositionUpdates =
|
||||||
|
selectedPointsIndices.reduce((map, index) => {
|
||||||
|
map.set(index, {
|
||||||
|
point: LinearElementEditor.pointFromAbsoluteCoords(
|
||||||
element,
|
element,
|
||||||
startBindingElement,
|
pointFrom<GlobalPoint>(sceneCoords.x, sceneCoords.y),
|
||||||
endBindingElement,
|
elementsMap,
|
||||||
app.scene,
|
),
|
||||||
);
|
});
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}, new Map()) ?? new Map();
|
||||||
|
|
||||||
|
bindOrUnbindBindingElement(element, draggedPoints, scene, appState, {
|
||||||
|
newArrow,
|
||||||
|
});
|
||||||
|
} else if (isLineElement(element)) {
|
||||||
|
if (
|
||||||
|
appState.selectedLinearElement?.isEditing &&
|
||||||
|
!appState.newElement &&
|
||||||
|
!isValidPolygon(element.points)
|
||||||
|
) {
|
||||||
|
scene.mutateElement(element, {
|
||||||
|
polygon: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (linearElementEditor !== appState.selectedLinearElement) {
|
if (linearElementEditor !== appState.selectedLinearElement) {
|
||||||
let newElements = elements;
|
// `handlePointerUp()` updated the linear element instance,
|
||||||
|
// so filter out this element if it is too small,
|
||||||
|
// but do an update to all new elements anyway for undo/redo purposes.
|
||||||
|
|
||||||
if (element && isInvisiblySmallElement(element)) {
|
if (element && isInvisiblySmallElement(element)) {
|
||||||
// TODO: #7348 in theory this gets recorded by the store, so the invisible elements could be restored by the undo/redo, which might be not what we would want
|
// TODO: #7348 in theory this gets recorded by the store, so the invisible elements could be restored by the undo/redo, which might be not what we would want
|
||||||
newElements = newElements.map((el) => {
|
newElements = newElements.map((el) => {
|
||||||
@ -91,39 +133,8 @@ export const actionFinalize = register({
|
|||||||
return el;
|
return el;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return {
|
|
||||||
elements: newElements,
|
|
||||||
appState: {
|
|
||||||
selectedLinearElement: {
|
|
||||||
...linearElementEditor,
|
|
||||||
selectedPointsIndices: null,
|
|
||||||
},
|
|
||||||
suggestedBindings: [],
|
|
||||||
},
|
|
||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appState.selectedLinearElement?.isEditing) {
|
const activeToolLocked = appState.activeTool?.locked;
|
||||||
const { elementId, startBindingElement, endBindingElement } =
|
|
||||||
appState.selectedLinearElement;
|
|
||||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
|
||||||
|
|
||||||
if (element) {
|
|
||||||
if (isBindingElement(element)) {
|
|
||||||
bindOrUnbindLinearElement(
|
|
||||||
element,
|
|
||||||
startBindingElement,
|
|
||||||
endBindingElement,
|
|
||||||
scene,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (isLineElement(element) && !isValidPolygon(element.points)) {
|
|
||||||
scene.mutateElement(element, {
|
|
||||||
polygon: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements:
|
elements:
|
||||||
@ -134,23 +145,31 @@ export const actionFinalize = register({
|
|||||||
}
|
}
|
||||||
return el;
|
return el;
|
||||||
})
|
})
|
||||||
: undefined,
|
: newElements,
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
cursorButton: "up",
|
cursorButton: "up",
|
||||||
selectedLinearElement: new LinearElementEditor(
|
selectedLinearElement: activeToolLocked
|
||||||
element,
|
? null
|
||||||
arrayToMap(elementsMap),
|
: {
|
||||||
false, // exit editing mode
|
...linearElementEditor,
|
||||||
),
|
selectedPointsIndices: null,
|
||||||
|
isEditing: false,
|
||||||
|
initialState: {
|
||||||
|
...linearElementEditor.initialState,
|
||||||
|
lastClickedPoint: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selectionElement: null,
|
||||||
|
suggestedBinding: null,
|
||||||
|
newElement: null,
|
||||||
|
multiElement: null,
|
||||||
},
|
},
|
||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let newElements = elements;
|
|
||||||
|
|
||||||
if (window.document.activeElement instanceof HTMLElement) {
|
if (window.document.activeElement instanceof HTMLElement) {
|
||||||
focusContainer();
|
focusContainer();
|
||||||
}
|
}
|
||||||
@ -174,8 +193,14 @@ export const actionFinalize = register({
|
|||||||
|
|
||||||
if (element) {
|
if (element) {
|
||||||
// pen and mouse have hover
|
// pen and mouse have hover
|
||||||
if (appState.multiElement && element.type !== "freedraw") {
|
if (
|
||||||
const { points, lastCommittedPoint } = element;
|
appState.selectedLinearElement &&
|
||||||
|
appState.multiElement &&
|
||||||
|
element.type !== "freedraw" &&
|
||||||
|
appState.lastPointerDownWith !== "touch"
|
||||||
|
) {
|
||||||
|
const { points } = element;
|
||||||
|
const { lastCommittedPoint } = appState.selectedLinearElement;
|
||||||
if (
|
if (
|
||||||
!lastCommittedPoint ||
|
!lastCommittedPoint ||
|
||||||
points[points.length - 1] !== lastCommittedPoint
|
points[points.length - 1] !== lastCommittedPoint
|
||||||
@ -227,25 +252,6 @@ export const actionFinalize = register({
|
|||||||
polygon: false,
|
polygon: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
isBindingElement(element) &&
|
|
||||||
!isLoop &&
|
|
||||||
element.points.length > 1 &&
|
|
||||||
isBindingEnabled(appState)
|
|
||||||
) {
|
|
||||||
const coords =
|
|
||||||
sceneCoords ??
|
|
||||||
tupleToCoors(
|
|
||||||
LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
|
||||||
element,
|
|
||||||
-1,
|
|
||||||
arrayToMap(elements),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
maybeBindLinearElement(element, appState, coords, scene);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,6 +277,25 @@ export const actionFinalize = register({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let selectedLinearElement =
|
||||||
|
element && isLinearElement(element)
|
||||||
|
? new LinearElementEditor(element, arrayToMap(newElements)) // To select the linear element when user has finished mutipoint editing
|
||||||
|
: appState.selectedLinearElement;
|
||||||
|
|
||||||
|
selectedLinearElement = selectedLinearElement
|
||||||
|
? {
|
||||||
|
...selectedLinearElement,
|
||||||
|
isEditing: appState.newElement
|
||||||
|
? false
|
||||||
|
: selectedLinearElement.isEditing,
|
||||||
|
initialState: {
|
||||||
|
...selectedLinearElement.initialState,
|
||||||
|
lastClickedPoint: -1,
|
||||||
|
origin: null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: selectedLinearElement;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements: newElements,
|
elements: newElements,
|
||||||
appState: {
|
appState: {
|
||||||
@ -288,7 +313,7 @@ export const actionFinalize = register({
|
|||||||
multiElement: null,
|
multiElement: null,
|
||||||
editingTextElement: null,
|
editingTextElement: null,
|
||||||
startBoundElement: null,
|
startBoundElement: null,
|
||||||
suggestedBindings: [],
|
suggestedBinding: null,
|
||||||
selectedElementIds:
|
selectedElementIds:
|
||||||
element &&
|
element &&
|
||||||
!appState.activeTool.locked &&
|
!appState.activeTool.locked &&
|
||||||
@ -298,11 +323,8 @@ export const actionFinalize = register({
|
|||||||
[element.id]: true,
|
[element.id]: true,
|
||||||
}
|
}
|
||||||
: appState.selectedElementIds,
|
: appState.selectedElementIds,
|
||||||
// To select the linear element when user has finished mutipoint editing
|
|
||||||
selectedLinearElement:
|
selectedLinearElement,
|
||||||
element && isLinearElement(element)
|
|
||||||
? new LinearElementEditor(element, arrayToMap(newElements))
|
|
||||||
: appState.selectedLinearElement,
|
|
||||||
},
|
},
|
||||||
// TODO: #7348 we should not capture everything, but if we don't, it leads to incosistencies -> revisit
|
// TODO: #7348 we should not capture everything, but if we don't, it leads to incosistencies -> revisit
|
||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
|
|||||||
@ -38,15 +38,13 @@ describe("flipping re-centers selection", () => {
|
|||||||
height: 239.9,
|
height: 239.9,
|
||||||
startBinding: {
|
startBinding: {
|
||||||
elementId: "rec1",
|
elementId: "rec1",
|
||||||
focus: 0,
|
|
||||||
gap: 5,
|
|
||||||
fixedPoint: [0.49, -0.05],
|
fixedPoint: [0.49, -0.05],
|
||||||
|
mode: "orbit",
|
||||||
},
|
},
|
||||||
endBinding: {
|
endBinding: {
|
||||||
elementId: "rec2",
|
elementId: "rec2",
|
||||||
focus: 0,
|
|
||||||
gap: 5,
|
|
||||||
fixedPoint: [-0.05, 0.49],
|
fixedPoint: [-0.05, 0.49],
|
||||||
|
mode: "orbit",
|
||||||
},
|
},
|
||||||
startArrowhead: null,
|
startArrowhead: null,
|
||||||
endArrowhead: "arrow",
|
endArrowhead: "arrow",
|
||||||
@ -74,11 +72,11 @@ describe("flipping re-centers selection", () => {
|
|||||||
|
|
||||||
const rec1 = h.elements.find((el) => el.id === "rec1")!;
|
const rec1 = h.elements.find((el) => el.id === "rec1")!;
|
||||||
expect(rec1.x).toBeCloseTo(100, 0);
|
expect(rec1.x).toBeCloseTo(100, 0);
|
||||||
expect(rec1.y).toBeCloseTo(100, 0);
|
expect(rec1.y).toBeCloseTo(101, 0);
|
||||||
|
|
||||||
const rec2 = h.elements.find((el) => el.id === "rec2")!;
|
const rec2 = h.elements.find((el) => el.id === "rec2")!;
|
||||||
expect(rec2.x).toBeCloseTo(220, 0);
|
expect(rec2.x).toBeCloseTo(220, 0);
|
||||||
expect(rec2.y).toBeCloseTo(250, 0);
|
expect(rec2.y).toBeCloseTo(251, 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -99,8 +97,8 @@ describe("flipping arrowheads", () => {
|
|||||||
endArrowhead: null,
|
endArrowhead: null,
|
||||||
endBinding: {
|
endBinding: {
|
||||||
elementId: rect.id,
|
elementId: rect.id,
|
||||||
focus: 0.5,
|
fixedPoint: [0.5, 0.5],
|
||||||
gap: 5,
|
mode: "orbit",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -139,13 +137,13 @@ describe("flipping arrowheads", () => {
|
|||||||
endArrowhead: "circle",
|
endArrowhead: "circle",
|
||||||
startBinding: {
|
startBinding: {
|
||||||
elementId: rect.id,
|
elementId: rect.id,
|
||||||
focus: 0.5,
|
fixedPoint: [0.5, 0.5],
|
||||||
gap: 5,
|
mode: "orbit",
|
||||||
},
|
},
|
||||||
endBinding: {
|
endBinding: {
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
focus: 0.5,
|
fixedPoint: [0.5, 0.5],
|
||||||
gap: 5,
|
mode: "orbit",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -195,8 +193,8 @@ describe("flipping arrowheads", () => {
|
|||||||
endArrowhead: null,
|
endArrowhead: null,
|
||||||
endBinding: {
|
endBinding: {
|
||||||
elementId: rect.id,
|
elementId: rect.id,
|
||||||
focus: 0.5,
|
fixedPoint: [0.5, 0.5],
|
||||||
gap: 5,
|
mode: "orbit",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,17 +1,10 @@
|
|||||||
import { getNonDeletedElements } from "@excalidraw/element";
|
import { getNonDeletedElements } from "@excalidraw/element";
|
||||||
import {
|
import { bindOrUnbindBindingElements } from "@excalidraw/element";
|
||||||
bindOrUnbindLinearElements,
|
|
||||||
isBindingEnabled,
|
|
||||||
} from "@excalidraw/element";
|
|
||||||
import { getCommonBoundingBox } from "@excalidraw/element";
|
import { getCommonBoundingBox } from "@excalidraw/element";
|
||||||
import { newElementWith } from "@excalidraw/element";
|
import { newElementWith } from "@excalidraw/element";
|
||||||
import { deepCopyElement } from "@excalidraw/element";
|
import { deepCopyElement } from "@excalidraw/element";
|
||||||
import { resizeMultipleElements } from "@excalidraw/element";
|
import { resizeMultipleElements } from "@excalidraw/element";
|
||||||
import {
|
import { isArrowElement, isElbowArrow } from "@excalidraw/element";
|
||||||
isArrowElement,
|
|
||||||
isElbowArrow,
|
|
||||||
isLinearElement,
|
|
||||||
} from "@excalidraw/element";
|
|
||||||
import { updateFrameMembershipOfSelectedElements } from "@excalidraw/element";
|
import { updateFrameMembershipOfSelectedElements } from "@excalidraw/element";
|
||||||
import { CODES, KEYS, arrayToMap } from "@excalidraw/common";
|
import { CODES, KEYS, arrayToMap } from "@excalidraw/common";
|
||||||
|
|
||||||
@ -103,7 +96,6 @@ const flipSelectedElements = (
|
|||||||
const updatedElements = flipElements(
|
const updatedElements = flipElements(
|
||||||
selectedElements,
|
selectedElements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
appState,
|
|
||||||
flipDirection,
|
flipDirection,
|
||||||
app,
|
app,
|
||||||
);
|
);
|
||||||
@ -118,7 +110,6 @@ const flipSelectedElements = (
|
|||||||
const flipElements = (
|
const flipElements = (
|
||||||
selectedElements: NonDeleted<ExcalidrawElement>[],
|
selectedElements: NonDeleted<ExcalidrawElement>[],
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
appState: AppState,
|
|
||||||
flipDirection: "horizontal" | "vertical",
|
flipDirection: "horizontal" | "vertical",
|
||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
): ExcalidrawElement[] => {
|
): ExcalidrawElement[] => {
|
||||||
@ -158,12 +149,10 @@ const flipElements = (
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
bindOrUnbindLinearElements(
|
bindOrUnbindBindingElements(
|
||||||
selectedElements.filter(isLinearElement),
|
selectedElements.filter(isArrowElement),
|
||||||
isBindingEnabled(appState),
|
|
||||||
[],
|
|
||||||
app.scene,
|
app.scene,
|
||||||
appState.zoom,
|
app.state,
|
||||||
);
|
);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import clsx from "clsx";
|
|||||||
|
|
||||||
import { CaptureUpdateAction } from "@excalidraw/element";
|
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import { invariant } from "@excalidraw/common";
|
||||||
|
|
||||||
import { getClientColor } from "../clients";
|
import { getClientColor } from "../clients";
|
||||||
import { Avatar } from "../components/Avatar";
|
import { Avatar } from "../components/Avatar";
|
||||||
import {
|
import {
|
||||||
@ -16,12 +18,17 @@ import { register } from "./register";
|
|||||||
import type { GoToCollaboratorComponentProps } from "../components/UserList";
|
import type { GoToCollaboratorComponentProps } from "../components/UserList";
|
||||||
import type { Collaborator } from "../types";
|
import type { Collaborator } from "../types";
|
||||||
|
|
||||||
export const actionGoToCollaborator = register({
|
export const actionGoToCollaborator = register<Collaborator>({
|
||||||
name: "goToCollaborator",
|
name: "goToCollaborator",
|
||||||
label: "Go to a collaborator",
|
label: "Go to a collaborator",
|
||||||
viewMode: true,
|
viewMode: true,
|
||||||
trackEvent: { category: "collab" },
|
trackEvent: { category: "collab" },
|
||||||
perform: (_elements, appState, collaborator: Collaborator) => {
|
perform: (_elements, appState, collaborator) => {
|
||||||
|
invariant(
|
||||||
|
collaborator,
|
||||||
|
"actionGoToCollaborator: collaborator should be defined when actionGoToCollaborator is called",
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!collaborator.socketId ||
|
!collaborator.socketId ||
|
||||||
appState.userToFollow?.socketId === collaborator.socketId ||
|
appState.userToFollow?.socketId === collaborator.socketId ||
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { pointFrom } from "@excalidraw/math";
|
import { pointFrom } from "@excalidraw/math";
|
||||||
|
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -21,12 +22,13 @@ import {
|
|||||||
getLineHeight,
|
getLineHeight,
|
||||||
isTransparent,
|
isTransparent,
|
||||||
reduceToCommonValue,
|
reduceToCommonValue,
|
||||||
|
invariant,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import { canBecomePolygon, getNonDeletedElements } from "@excalidraw/element";
|
import { canBecomePolygon, getNonDeletedElements } from "@excalidraw/element";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
bindLinearElement,
|
bindBindingElement,
|
||||||
calculateFixedPointForElbowArrowBinding,
|
calculateFixedPointForElbowArrowBinding,
|
||||||
updateBoundElements,
|
updateBoundElements,
|
||||||
} from "@excalidraw/element";
|
} from "@excalidraw/element";
|
||||||
@ -297,13 +299,15 @@ const changeFontSize = (
|
|||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
export const actionChangeStrokeColor = register({
|
export const actionChangeStrokeColor = register<
|
||||||
|
Pick<AppState, "currentItemStrokeColor">
|
||||||
|
>({
|
||||||
name: "changeStrokeColor",
|
name: "changeStrokeColor",
|
||||||
label: "labels.stroke",
|
label: "labels.stroke",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value) => {
|
||||||
return {
|
return {
|
||||||
...(value.currentItemStrokeColor && {
|
...(value?.currentItemStrokeColor && {
|
||||||
elements: changeProperty(
|
elements: changeProperty(
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
@ -321,7 +325,7 @@ export const actionChangeStrokeColor = register({
|
|||||||
...appState,
|
...appState,
|
||||||
...value,
|
...value,
|
||||||
},
|
},
|
||||||
captureUpdate: !!value.currentItemStrokeColor
|
captureUpdate: !!value?.currentItemStrokeColor
|
||||||
? CaptureUpdateAction.IMMEDIATELY
|
? CaptureUpdateAction.IMMEDIATELY
|
||||||
: CaptureUpdateAction.EVENTUALLY,
|
: CaptureUpdateAction.EVENTUALLY,
|
||||||
};
|
};
|
||||||
@ -354,12 +358,14 @@ export const actionChangeStrokeColor = register({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionChangeBackgroundColor = register({
|
export const actionChangeBackgroundColor = register<
|
||||||
|
Pick<AppState, "currentItemBackgroundColor" | "viewBackgroundColor">
|
||||||
|
>({
|
||||||
name: "changeBackgroundColor",
|
name: "changeBackgroundColor",
|
||||||
label: "labels.changeBackground",
|
label: "labels.changeBackground",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
perform: (elements, appState, value, app) => {
|
perform: (elements, appState, value, app) => {
|
||||||
if (!value.currentItemBackgroundColor) {
|
if (!value?.currentItemBackgroundColor) {
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
@ -434,7 +440,7 @@ export const actionChangeBackgroundColor = register({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionChangeFillStyle = register({
|
export const actionChangeFillStyle = register<ExcalidrawElement["fillStyle"]>({
|
||||||
name: "changeFillStyle",
|
name: "changeFillStyle",
|
||||||
label: "labels.fill",
|
label: "labels.fill",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
@ -514,7 +520,9 @@ export const actionChangeFillStyle = register({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionChangeStrokeWidth = register({
|
export const actionChangeStrokeWidth = register<
|
||||||
|
ExcalidrawElement["strokeWidth"]
|
||||||
|
>({
|
||||||
name: "changeStrokeWidth",
|
name: "changeStrokeWidth",
|
||||||
label: "labels.strokeWidth",
|
label: "labels.strokeWidth",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
@ -572,7 +580,7 @@ export const actionChangeStrokeWidth = register({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionChangeSloppiness = register({
|
export const actionChangeSloppiness = register<ExcalidrawElement["roughness"]>({
|
||||||
name: "changeSloppiness",
|
name: "changeSloppiness",
|
||||||
label: "labels.sloppiness",
|
label: "labels.sloppiness",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
@ -628,7 +636,9 @@ export const actionChangeSloppiness = register({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionChangeStrokeStyle = register({
|
export const actionChangeStrokeStyle = register<
|
||||||
|
ExcalidrawElement["strokeStyle"]
|
||||||
|
>({
|
||||||
name: "changeStrokeStyle",
|
name: "changeStrokeStyle",
|
||||||
label: "labels.strokeStyle",
|
label: "labels.strokeStyle",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
@ -683,7 +693,7 @@ export const actionChangeStrokeStyle = register({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionChangeOpacity = register({
|
export const actionChangeOpacity = register<ExcalidrawElement["opacity"]>({
|
||||||
name: "changeOpacity",
|
name: "changeOpacity",
|
||||||
label: "labels.opacity",
|
label: "labels.opacity",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
@ -707,14 +717,24 @@ export const actionChangeOpacity = register({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionChangeFontSize = register({
|
export const actionChangeFontSize = register<ExcalidrawTextElement["fontSize"]>(
|
||||||
|
{
|
||||||
name: "changeFontSize",
|
name: "changeFontSize",
|
||||||
label: "labels.fontSize",
|
label: "labels.fontSize",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
perform: (elements, appState, value, app) => {
|
perform: (elements, appState, value, app) => {
|
||||||
return changeFontSize(elements, appState, app, () => value, value);
|
return changeFontSize(
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
app,
|
||||||
|
() => {
|
||||||
|
invariant(value, "actionChangeFontSize: Expected a font size value");
|
||||||
|
return value;
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData, app, data }) => (
|
value,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.fontSize")}</legend>
|
<legend>{t("labels.fontSize")}</legend>
|
||||||
<div className="buttonList">
|
<div className="buttonList">
|
||||||
@ -773,19 +793,13 @@ export const actionChangeFontSize = register({
|
|||||||
? null
|
? null
|
||||||
: appState.currentItemFontSize || DEFAULT_FONT_SIZE,
|
: appState.currentItemFontSize || DEFAULT_FONT_SIZE,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => {
|
onChange={(value) => updateData(value)}
|
||||||
withCaretPositionPreservation(
|
|
||||||
() => updateData(value),
|
|
||||||
appState.stylesPanelMode === "compact",
|
|
||||||
!!appState.editingTextElement,
|
|
||||||
data?.onPreventClose,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
),
|
),
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const actionDecreaseFontSize = register({
|
export const actionDecreaseFontSize = register({
|
||||||
name: "decreaseFontSize",
|
name: "decreaseFontSize",
|
||||||
@ -845,7 +859,10 @@ type ChangeFontFamilyData = Partial<
|
|||||||
resetContainers?: true;
|
resetContainers?: true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actionChangeFontFamily = register({
|
export const actionChangeFontFamily = register<{
|
||||||
|
currentItemFontFamily: any;
|
||||||
|
currentHoveredFontFamily: any;
|
||||||
|
}>({
|
||||||
name: "changeFontFamily",
|
name: "changeFontFamily",
|
||||||
label: "labels.fontFamily",
|
label: "labels.fontFamily",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
@ -882,6 +899,8 @@ export const actionChangeFontFamily = register({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invariant(value, "actionChangeFontFamily: value must be defined");
|
||||||
|
|
||||||
const { currentItemFontFamily, currentHoveredFontFamily } = value;
|
const { currentItemFontFamily, currentHoveredFontFamily } = value;
|
||||||
|
|
||||||
let nextCaptureUpdateAction: CaptureUpdateActionType =
|
let nextCaptureUpdateAction: CaptureUpdateActionType =
|
||||||
@ -1226,7 +1245,7 @@ export const actionChangeFontFamily = register({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionChangeTextAlign = register({
|
export const actionChangeTextAlign = register<TextAlign>({
|
||||||
name: "changeTextAlign",
|
name: "changeTextAlign",
|
||||||
label: "Change text alignment",
|
label: "Change text alignment",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
@ -1326,7 +1345,7 @@ export const actionChangeTextAlign = register({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionChangeVerticalAlign = register({
|
export const actionChangeVerticalAlign = register<VerticalAlign>({
|
||||||
name: "changeVerticalAlign",
|
name: "changeVerticalAlign",
|
||||||
label: "Change vertical alignment",
|
label: "Change vertical alignment",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
@ -1425,7 +1444,7 @@ export const actionChangeVerticalAlign = register({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionChangeRoundness = register({
|
export const actionChangeRoundness = register<"sharp" | "round">({
|
||||||
name: "changeRoundness",
|
name: "changeRoundness",
|
||||||
label: "Change edge roundness",
|
label: "Change edge roundness",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
@ -1582,15 +1601,16 @@ const getArrowheadOptions = (flip: boolean) => {
|
|||||||
] as const;
|
] as const;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actionChangeArrowhead = register({
|
export const actionChangeArrowhead = register<{
|
||||||
|
position: "start" | "end";
|
||||||
|
type: Arrowhead;
|
||||||
|
}>({
|
||||||
name: "changeArrowhead",
|
name: "changeArrowhead",
|
||||||
label: "Change arrowheads",
|
label: "Change arrowheads",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
perform: (
|
perform: (elements, appState, value) => {
|
||||||
elements,
|
invariant(value, "actionChangeArrowhead: value must be defined");
|
||||||
appState,
|
|
||||||
value: { position: "start" | "end"; type: Arrowhead },
|
|
||||||
) => {
|
|
||||||
return {
|
return {
|
||||||
elements: changeProperty(elements, appState, (el) => {
|
elements: changeProperty(elements, appState, (el) => {
|
||||||
if (isLinearElement(el)) {
|
if (isLinearElement(el)) {
|
||||||
@ -1685,7 +1705,7 @@ export const actionChangeArrowProperties = register({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionChangeArrowType = register({
|
export const actionChangeArrowType = register<keyof typeof ARROW_TYPE>({
|
||||||
name: "changeArrowType",
|
name: "changeArrowType",
|
||||||
label: "Change arrow types",
|
label: "Change arrow types",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
@ -1786,7 +1806,13 @@ export const actionChangeArrowType = register({
|
|||||||
newElement.startBinding.elementId,
|
newElement.startBinding.elementId,
|
||||||
) as ExcalidrawBindableElement;
|
) as ExcalidrawBindableElement;
|
||||||
if (startElement) {
|
if (startElement) {
|
||||||
bindLinearElement(newElement, startElement, "start", app.scene);
|
bindBindingElement(
|
||||||
|
newElement,
|
||||||
|
startElement,
|
||||||
|
appState.bindMode === "inside" ? "inside" : "orbit",
|
||||||
|
"start",
|
||||||
|
app.scene,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (newElement.endBinding) {
|
if (newElement.endBinding) {
|
||||||
@ -1794,7 +1820,13 @@ export const actionChangeArrowType = register({
|
|||||||
newElement.endBinding.elementId,
|
newElement.endBinding.elementId,
|
||||||
) as ExcalidrawBindableElement;
|
) as ExcalidrawBindableElement;
|
||||||
if (endElement) {
|
if (endElement) {
|
||||||
bindLinearElement(newElement, endElement, "end", app.scene);
|
bindBindingElement(
|
||||||
|
newElement,
|
||||||
|
endElement,
|
||||||
|
appState.bindMode === "inside" ? "inside" : "orbit",
|
||||||
|
"end",
|
||||||
|
app.scene,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,12 @@ import type { Action } from "./types";
|
|||||||
|
|
||||||
export let actions: readonly Action[] = [];
|
export let actions: readonly Action[] = [];
|
||||||
|
|
||||||
export const register = <T extends Action>(action: T) => {
|
export const register = <
|
||||||
|
TData extends any,
|
||||||
|
T extends Action<TData> = Action<TData>,
|
||||||
|
>(
|
||||||
|
action: T,
|
||||||
|
) => {
|
||||||
actions = actions.concat(action);
|
actions = actions.concat(action);
|
||||||
return action as T & {
|
return action as T & {
|
||||||
keyTest?: unknown extends T["keyTest"] ? never : T["keyTest"];
|
keyTest?: unknown extends T["keyTest"] ? never : T["keyTest"];
|
||||||
|
|||||||
@ -32,10 +32,10 @@ export type ActionResult =
|
|||||||
}
|
}
|
||||||
| false;
|
| false;
|
||||||
|
|
||||||
type ActionFn = (
|
type ActionFn<TData = any> = (
|
||||||
elements: readonly OrderedExcalidrawElement[],
|
elements: readonly OrderedExcalidrawElement[],
|
||||||
appState: Readonly<AppState>,
|
appState: Readonly<AppState>,
|
||||||
formData: any,
|
formData: TData | undefined,
|
||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
) => ActionResult | Promise<ActionResult>;
|
) => ActionResult | Promise<ActionResult>;
|
||||||
|
|
||||||
@ -159,7 +159,7 @@ export type PanelComponentProps = {
|
|||||||
) => React.JSX.Element | null;
|
) => React.JSX.Element | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Action {
|
export interface Action<TData = any> {
|
||||||
name: ActionName;
|
name: ActionName;
|
||||||
label:
|
label:
|
||||||
| string
|
| string
|
||||||
@ -176,7 +176,7 @@ export interface Action {
|
|||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
) => React.ReactNode);
|
) => React.ReactNode);
|
||||||
PanelComponent?: React.FC<PanelComponentProps>;
|
PanelComponent?: React.FC<PanelComponentProps>;
|
||||||
perform: ActionFn;
|
perform: ActionFn<TData>;
|
||||||
keyPriority?: number;
|
keyPriority?: number;
|
||||||
keyTest?: (
|
keyTest?: (
|
||||||
event: React.KeyboardEvent | KeyboardEvent,
|
event: React.KeyboardEvent | KeyboardEvent,
|
||||||
|
|||||||
@ -96,7 +96,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
panels: STATS_PANELS.generalStats | STATS_PANELS.elementProperties,
|
panels: STATS_PANELS.generalStats | STATS_PANELS.elementProperties,
|
||||||
},
|
},
|
||||||
startBoundElement: null,
|
startBoundElement: null,
|
||||||
suggestedBindings: [],
|
suggestedBinding: null,
|
||||||
frameRendering: { enabled: true, clip: true, name: true, outline: true },
|
frameRendering: { enabled: true, clip: true, name: true, outline: true },
|
||||||
frameToHighlight: null,
|
frameToHighlight: null,
|
||||||
editingFrame: null,
|
editingFrame: null,
|
||||||
@ -123,6 +123,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
searchMatches: null,
|
searchMatches: null,
|
||||||
lockedMultiSelections: {},
|
lockedMultiSelections: {},
|
||||||
activeLockedId: null,
|
activeLockedId: null,
|
||||||
|
bindMode: "orbit",
|
||||||
stylesPanelMode: "full",
|
stylesPanelMode: "full",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -225,7 +226,7 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
shouldCacheIgnoreZoom: { browser: true, export: false, server: false },
|
shouldCacheIgnoreZoom: { browser: true, export: false, server: false },
|
||||||
stats: { browser: true, export: false, server: false },
|
stats: { browser: true, export: false, server: false },
|
||||||
startBoundElement: { browser: false, export: false, server: false },
|
startBoundElement: { browser: false, export: false, server: false },
|
||||||
suggestedBindings: { browser: false, export: false, server: false },
|
suggestedBinding: { browser: false, export: false, server: false },
|
||||||
frameRendering: { browser: false, export: false, server: false },
|
frameRendering: { browser: false, export: false, server: false },
|
||||||
frameToHighlight: { browser: false, export: false, server: false },
|
frameToHighlight: { browser: false, export: false, server: false },
|
||||||
editingFrame: { browser: false, export: false, server: false },
|
editingFrame: { browser: false, export: false, server: false },
|
||||||
@ -248,6 +249,7 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
searchMatches: { browser: false, export: false, server: false },
|
searchMatches: { browser: false, export: false, server: false },
|
||||||
lockedMultiSelections: { browser: true, export: true, server: true },
|
lockedMultiSelections: { browser: true, export: true, server: true },
|
||||||
activeLockedId: { browser: false, export: false, server: false },
|
activeLockedId: { browser: false, export: false, server: false },
|
||||||
|
bindMode: { browser: true, export: false, server: false },
|
||||||
stylesPanelMode: { browser: true, export: false, server: false },
|
stylesPanelMode: { browser: true, export: false, server: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -961,7 +961,7 @@ const CommandItem = ({
|
|||||||
<InlineIcon
|
<InlineIcon
|
||||||
icon={
|
icon={
|
||||||
typeof command.icon === "function"
|
typeof command.icon === "function"
|
||||||
? command.icon(appState)
|
? command.icon(appState, [])
|
||||||
: command.icon
|
: command.icon
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import type { ActionManager } from "../../actions/manager";
|
import type { ActionManager } from "../../actions/manager";
|
||||||
import type { Action } from "../../actions/types";
|
import type { Action } from "../../actions/types";
|
||||||
import type { UIAppState } from "../../types";
|
|
||||||
|
|
||||||
export type CommandPaletteItem = {
|
export type CommandPaletteItem = {
|
||||||
label: string;
|
label: string;
|
||||||
@ -12,7 +11,7 @@ export type CommandPaletteItem = {
|
|||||||
* (deburred name + keywords)
|
* (deburred name + keywords)
|
||||||
*/
|
*/
|
||||||
haystack?: string;
|
haystack?: string;
|
||||||
icon?: React.ReactNode | ((appState: UIAppState) => React.ReactNode);
|
icon?: Action["icon"];
|
||||||
category: string;
|
category: string;
|
||||||
order?: number;
|
order?: number;
|
||||||
predicate?: boolean | Action["predicate"];
|
predicate?: boolean | Action["predicate"];
|
||||||
|
|||||||
@ -844,7 +844,7 @@ const convertElementType = <
|
|||||||
}),
|
}),
|
||||||
) as typeof element;
|
) as typeof element;
|
||||||
|
|
||||||
updateBindings(nextElement, app.scene);
|
updateBindings(nextElement, app.scene, app.state);
|
||||||
|
|
||||||
return nextElement;
|
return nextElement;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { CANVAS_SEARCH_TAB, DEFAULT_SIDEBAR } from "@excalidraw/common";
|
import { CANVAS_SEARCH_TAB, DEFAULT_SIDEBAR } from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
isArrowElement,
|
||||||
isFlowchartNodeElement,
|
isFlowchartNodeElement,
|
||||||
isImageElement,
|
isImageElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
@ -37,6 +38,13 @@ const getHints = ({
|
|||||||
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
|
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
|
||||||
const multiMode = appState.multiElement !== null;
|
const multiMode = appState.multiElement !== null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
appState.selectedLinearElement?.isDragging ||
|
||||||
|
isArrowElement(appState.newElement)
|
||||||
|
) {
|
||||||
|
return t("hints.arrowBindModifiers");
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
appState.openSidebar?.name === DEFAULT_SIDEBAR.name &&
|
appState.openSidebar?.name === DEFAULT_SIDEBAR.name &&
|
||||||
appState.openSidebar.tab === CANVAS_SEARCH_TAB &&
|
appState.openSidebar.tab === CANVAS_SEARCH_TAB &&
|
||||||
|
|||||||
@ -646,7 +646,7 @@ const LayerUI = ({
|
|||||||
|
|
||||||
const stripIrrelevantAppStateProps = (appState: AppState): UIAppState => {
|
const stripIrrelevantAppStateProps = (appState: AppState): UIAppState => {
|
||||||
const {
|
const {
|
||||||
suggestedBindings,
|
suggestedBinding,
|
||||||
startBoundElement,
|
startBoundElement,
|
||||||
cursorButton,
|
cursorButton,
|
||||||
scrollX,
|
scrollX,
|
||||||
|
|||||||
@ -34,6 +34,7 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
|||||||
shouldChangeByStepSize,
|
shouldChangeByStepSize,
|
||||||
nextValue,
|
nextValue,
|
||||||
scene,
|
scene,
|
||||||
|
app,
|
||||||
}) => {
|
}) => {
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const origElement = originalElements[0];
|
const origElement = originalElements[0];
|
||||||
@ -48,7 +49,7 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
|||||||
scene.mutateElement(latestElement, {
|
scene.mutateElement(latestElement, {
|
||||||
angle: nextAngle,
|
angle: nextAngle,
|
||||||
});
|
});
|
||||||
updateBindings(latestElement, scene);
|
updateBindings(latestElement, scene, app.state);
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||||
@ -74,7 +75,7 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
|||||||
scene.mutateElement(latestElement, {
|
scene.mutateElement(latestElement, {
|
||||||
angle: nextAngle,
|
angle: nextAngle,
|
||||||
});
|
});
|
||||||
updateBindings(latestElement, scene);
|
updateBindings(latestElement, scene, app.state);
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||||
|
|||||||
@ -94,9 +94,7 @@ const resizeElementInGroup = (
|
|||||||
);
|
);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
const newFontSize = boundTextElement.fontSize * scale;
|
const newFontSize = boundTextElement.fontSize * scale;
|
||||||
updateBoundElements(latestElement, scene, {
|
updateBoundElements(latestElement, scene);
|
||||||
newSize: { width: updates.width, height: updates.height },
|
|
||||||
});
|
|
||||||
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
|
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
|
||||||
if (latestBoundTextElement && isTextElement(latestBoundTextElement)) {
|
if (latestBoundTextElement && isTextElement(latestBoundTextElement)) {
|
||||||
scene.mutateElement(latestBoundTextElement, {
|
scene.mutateElement(latestBoundTextElement, {
|
||||||
|
|||||||
@ -38,6 +38,7 @@ const moveElements = (
|
|||||||
originalElements: readonly ExcalidrawElement[],
|
originalElements: readonly ExcalidrawElement[],
|
||||||
originalElementsMap: ElementsMap,
|
originalElementsMap: ElementsMap,
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
|
appState: AppState,
|
||||||
) => {
|
) => {
|
||||||
for (let i = 0; i < originalElements.length; i++) {
|
for (let i = 0; i < originalElements.length; i++) {
|
||||||
const origElement = originalElements[i];
|
const origElement = originalElements[i];
|
||||||
@ -63,6 +64,7 @@ const moveElements = (
|
|||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
origElement,
|
origElement,
|
||||||
scene,
|
scene,
|
||||||
|
appState,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
@ -75,6 +77,7 @@ const moveGroupTo = (
|
|||||||
originalElements: ExcalidrawElement[],
|
originalElements: ExcalidrawElement[],
|
||||||
originalElementsMap: ElementsMap,
|
originalElementsMap: ElementsMap,
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
|
appState: AppState,
|
||||||
) => {
|
) => {
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const [x1, y1, ,] = getCommonBounds(originalElements);
|
const [x1, y1, ,] = getCommonBounds(originalElements);
|
||||||
@ -107,6 +110,7 @@ const moveGroupTo = (
|
|||||||
topLeftY + offsetY,
|
topLeftY + offsetY,
|
||||||
origElement,
|
origElement,
|
||||||
scene,
|
scene,
|
||||||
|
appState,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
@ -125,6 +129,7 @@ const handlePositionChange: DragInputCallbackType<
|
|||||||
property,
|
property,
|
||||||
scene,
|
scene,
|
||||||
originalAppState,
|
originalAppState,
|
||||||
|
app,
|
||||||
}) => {
|
}) => {
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
|
|
||||||
@ -152,6 +157,7 @@ const handlePositionChange: DragInputCallbackType<
|
|||||||
elementsInUnit.map((el) => el.original),
|
elementsInUnit.map((el) => el.original),
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
scene,
|
scene,
|
||||||
|
app.state,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const origElement = elementsInUnit[0]?.original;
|
const origElement = elementsInUnit[0]?.original;
|
||||||
@ -178,6 +184,7 @@ const handlePositionChange: DragInputCallbackType<
|
|||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
origElement,
|
origElement,
|
||||||
scene,
|
scene,
|
||||||
|
app.state,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
@ -203,6 +210,7 @@ const handlePositionChange: DragInputCallbackType<
|
|||||||
originalElements,
|
originalElements,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
scene,
|
scene,
|
||||||
|
app.state,
|
||||||
);
|
);
|
||||||
|
|
||||||
scene.triggerUpdate();
|
scene.triggerUpdate();
|
||||||
|
|||||||
@ -34,6 +34,7 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||||||
property,
|
property,
|
||||||
scene,
|
scene,
|
||||||
originalAppState,
|
originalAppState,
|
||||||
|
app,
|
||||||
}) => {
|
}) => {
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const origElement = originalElements[0];
|
const origElement = originalElements[0];
|
||||||
@ -131,6 +132,7 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
origElement,
|
origElement,
|
||||||
scene,
|
scene,
|
||||||
|
app.state,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@ -162,6 +164,7 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
origElement,
|
origElement,
|
||||||
scene,
|
scene,
|
||||||
|
app.state,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,9 +4,9 @@ import throttle from "lodash.throttle";
|
|||||||
import { useEffect, useMemo, useState, memo } from "react";
|
import { useEffect, useMemo, useState, memo } from "react";
|
||||||
|
|
||||||
import { STATS_PANELS } from "@excalidraw/common";
|
import { STATS_PANELS } from "@excalidraw/common";
|
||||||
import { getCommonBounds } from "@excalidraw/element";
|
import { getCommonBounds, isBindingElement } from "@excalidraw/element";
|
||||||
import { getUncroppedWidthAndHeight } from "@excalidraw/element";
|
import { getUncroppedWidthAndHeight } from "@excalidraw/element";
|
||||||
import { isElbowArrow, isImageElement } from "@excalidraw/element";
|
import { isImageElement } from "@excalidraw/element";
|
||||||
|
|
||||||
import { frameAndChildrenSelectedTogether } from "@excalidraw/element";
|
import { frameAndChildrenSelectedTogether } from "@excalidraw/element";
|
||||||
|
|
||||||
@ -333,7 +333,7 @@ export const StatsInner = memo(
|
|||||||
appState={appState}
|
appState={appState}
|
||||||
/>
|
/>
|
||||||
</StatsRow>
|
</StatsRow>
|
||||||
{!isElbowArrow(singleElement) && (
|
{!isBindingElement(singleElement) && (
|
||||||
<StatsRow>
|
<StatsRow>
|
||||||
<Angle
|
<Angle
|
||||||
property="angle"
|
property="angle"
|
||||||
|
|||||||
@ -114,7 +114,7 @@ describe("binding with linear elements", () => {
|
|||||||
mouse.up(200, 100);
|
mouse.up(200, 100);
|
||||||
|
|
||||||
UI.clickTool("arrow");
|
UI.clickTool("arrow");
|
||||||
mouse.down(5, 0);
|
mouse.down(-5, 0);
|
||||||
mouse.up(300, 50);
|
mouse.up(300, 50);
|
||||||
|
|
||||||
elementStats = stats?.querySelector("#elementStats");
|
elementStats = stats?.querySelector("#elementStats");
|
||||||
@ -135,18 +135,7 @@ describe("binding with linear elements", () => {
|
|||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
expect(linear.startBinding).not.toBe(null);
|
expect(linear.startBinding).not.toBe(null);
|
||||||
expect(inputX).not.toBeNull();
|
expect(inputX).not.toBeNull();
|
||||||
UI.updateInput(inputX, String("204"));
|
UI.updateInput(inputX, String("186"));
|
||||||
expect(linear.startBinding).not.toBe(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remain bound to linear element on small angle change", async () => {
|
|
||||||
const linear = h.elements[1] as ExcalidrawLinearElement;
|
|
||||||
const inputAngle = UI.queryStatsProperty("A")?.querySelector(
|
|
||||||
".drag-input",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
|
|
||||||
expect(linear.startBinding).not.toBe(null);
|
|
||||||
UI.updateInput(inputAngle, String("1"));
|
|
||||||
expect(linear.startBinding).not.toBe(null);
|
expect(linear.startBinding).not.toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -161,17 +150,6 @@ describe("binding with linear elements", () => {
|
|||||||
UI.updateInput(inputX, String("254"));
|
UI.updateInput(inputX, String("254"));
|
||||||
expect(linear.startBinding).toBe(null);
|
expect(linear.startBinding).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should remain bound to linear element on small angle change", async () => {
|
|
||||||
const linear = h.elements[1] as ExcalidrawLinearElement;
|
|
||||||
const inputAngle = UI.queryStatsProperty("A")?.querySelector(
|
|
||||||
".drag-input",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
|
|
||||||
expect(linear.startBinding).not.toBe(null);
|
|
||||||
UI.updateInput(inputAngle, String("45"));
|
|
||||||
expect(linear.startBinding).toBe(null);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// single element
|
// single element
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
import { pointFrom, pointRotateRads } from "@excalidraw/math";
|
import { pointFrom, pointRotateRads } from "@excalidraw/math";
|
||||||
|
|
||||||
import { getBoundTextElement } from "@excalidraw/element";
|
import {
|
||||||
|
getBoundTextElement,
|
||||||
|
isBindingElement,
|
||||||
|
unbindBindingElement,
|
||||||
|
} from "@excalidraw/element";
|
||||||
import { isFrameLikeElement } from "@excalidraw/element";
|
import { isFrameLikeElement } from "@excalidraw/element";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -12,6 +16,7 @@ import {
|
|||||||
import { getFrameChildren } from "@excalidraw/element";
|
import { getFrameChildren } from "@excalidraw/element";
|
||||||
|
|
||||||
import { updateBindings } from "@excalidraw/element";
|
import { updateBindings } from "@excalidraw/element";
|
||||||
|
import { DRAGGING_THRESHOLD } from "@excalidraw/common";
|
||||||
|
|
||||||
import type { Radians } from "@excalidraw/math";
|
import type { Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
@ -110,9 +115,25 @@ export const moveElement = (
|
|||||||
newTopLeftY: number,
|
newTopLeftY: number,
|
||||||
originalElement: ExcalidrawElement,
|
originalElement: ExcalidrawElement,
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
|
appState: AppState,
|
||||||
originalElementsMap: ElementsMap,
|
originalElementsMap: ElementsMap,
|
||||||
shouldInformMutation = true,
|
shouldInformMutation = true,
|
||||||
) => {
|
) => {
|
||||||
|
if (
|
||||||
|
isBindingElement(originalElement) &&
|
||||||
|
(originalElement.startBinding || originalElement.endBinding)
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
Math.abs(newTopLeftX - originalElement.x) < DRAGGING_THRESHOLD &&
|
||||||
|
Math.abs(newTopLeftY - originalElement.y) < DRAGGING_THRESHOLD
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
unbindBindingElement(originalElement, "start", scene);
|
||||||
|
unbindBindingElement(originalElement, "end", scene);
|
||||||
|
}
|
||||||
|
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const latestElement = elementsMap.get(originalElement.id);
|
const latestElement = elementsMap.get(originalElement.id);
|
||||||
if (!latestElement) {
|
if (!latestElement) {
|
||||||
@ -145,7 +166,7 @@ export const moveElement = (
|
|||||||
},
|
},
|
||||||
{ informMutation: shouldInformMutation, isDragging: false },
|
{ informMutation: shouldInformMutation, isDragging: false },
|
||||||
);
|
);
|
||||||
updateBindings(latestElement, scene);
|
updateBindings(latestElement, scene, appState);
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(
|
const boundTextElement = getBoundTextElement(
|
||||||
originalElement,
|
originalElement,
|
||||||
@ -203,7 +224,7 @@ export const moveElement = (
|
|||||||
},
|
},
|
||||||
{ informMutation: shouldInformMutation, isDragging: false },
|
{ informMutation: shouldInformMutation, isDragging: false },
|
||||||
);
|
);
|
||||||
updateBindings(latestChildElement, scene, {
|
updateBindings(latestChildElement, scene, appState, {
|
||||||
simultaneouslyUpdated: originalChildren,
|
simultaneouslyUpdated: originalChildren,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import {
|
|||||||
isShallowEqual,
|
isShallowEqual,
|
||||||
sceneCoordsToViewportCoords,
|
sceneCoordsToViewportCoords,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
import { AnimationController } from "@excalidraw/excalidraw/renderer/animation";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
@ -12,15 +13,21 @@ import type {
|
|||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
import { isRenderThrottlingEnabled } from "../../reactUtils";
|
|
||||||
import { renderInteractiveScene } from "../../renderer/interactiveScene";
|
import { renderInteractiveScene } from "../../renderer/interactiveScene";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
InteractiveCanvasRenderConfig,
|
InteractiveCanvasRenderConfig,
|
||||||
|
InteractiveSceneRenderAnimationState,
|
||||||
|
InteractiveSceneRenderConfig,
|
||||||
RenderableElementsMap,
|
RenderableElementsMap,
|
||||||
RenderInteractiveSceneCallback,
|
RenderInteractiveSceneCallback,
|
||||||
} from "../../scene/types";
|
} from "../../scene/types";
|
||||||
import type { AppState, Device, InteractiveCanvasAppState } from "../../types";
|
import type {
|
||||||
|
AppClassProperties,
|
||||||
|
AppState,
|
||||||
|
Device,
|
||||||
|
InteractiveCanvasAppState,
|
||||||
|
} from "../../types";
|
||||||
import type { DOMAttributes } from "react";
|
import type { DOMAttributes } from "react";
|
||||||
|
|
||||||
type InteractiveCanvasProps = {
|
type InteractiveCanvasProps = {
|
||||||
@ -36,6 +43,7 @@ type InteractiveCanvasProps = {
|
|||||||
appState: InteractiveCanvasAppState;
|
appState: InteractiveCanvasAppState;
|
||||||
renderScrollbars: boolean;
|
renderScrollbars: boolean;
|
||||||
device: Device;
|
device: Device;
|
||||||
|
app: AppClassProperties;
|
||||||
renderInteractiveSceneCallback: (
|
renderInteractiveSceneCallback: (
|
||||||
data: RenderInteractiveSceneCallback,
|
data: RenderInteractiveSceneCallback,
|
||||||
) => void;
|
) => void;
|
||||||
@ -70,8 +78,11 @@ type InteractiveCanvasProps = {
|
|||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const INTERACTIVE_SCENE_ANIMATION_KEY = "animateInteractiveScene";
|
||||||
|
|
||||||
const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
||||||
const isComponentMounted = useRef(false);
|
const isComponentMounted = useRef(false);
|
||||||
|
const rendererParams = useRef(null as InteractiveSceneRenderConfig | null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isComponentMounted.current) {
|
if (!isComponentMounted.current) {
|
||||||
@ -128,8 +139,8 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
|||||||
)) ||
|
)) ||
|
||||||
"#6965db";
|
"#6965db";
|
||||||
|
|
||||||
renderInteractiveScene(
|
rendererParams.current = {
|
||||||
{
|
app: props.app,
|
||||||
canvas: props.canvas,
|
canvas: props.canvas,
|
||||||
elementsMap: props.elementsMap,
|
elementsMap: props.elementsMap,
|
||||||
visibleElements: props.visibleElements,
|
visibleElements: props.visibleElements,
|
||||||
@ -145,12 +156,46 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
|||||||
remotePointerUserStates,
|
remotePointerUserStates,
|
||||||
selectionColor,
|
selectionColor,
|
||||||
renderScrollbars: props.renderScrollbars,
|
renderScrollbars: props.renderScrollbars,
|
||||||
|
// NOTE not memoized on so we don't rerender on cursor move
|
||||||
|
lastViewportPosition: props.app.lastViewportPosition,
|
||||||
},
|
},
|
||||||
device: props.device,
|
device: props.device,
|
||||||
callback: props.renderInteractiveSceneCallback,
|
callback: props.renderInteractiveSceneCallback,
|
||||||
|
animationState: {
|
||||||
|
bindingHighlight: undefined,
|
||||||
|
},
|
||||||
|
deltaTime: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!AnimationController.running(INTERACTIVE_SCENE_ANIMATION_KEY)) {
|
||||||
|
AnimationController.start<InteractiveSceneRenderAnimationState>(
|
||||||
|
INTERACTIVE_SCENE_ANIMATION_KEY,
|
||||||
|
({ deltaTime, state }) => {
|
||||||
|
const nextAnimationState = renderInteractiveScene(
|
||||||
|
{
|
||||||
|
...rendererParams.current!,
|
||||||
|
deltaTime,
|
||||||
|
animationState: state,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
).animationState;
|
||||||
|
|
||||||
|
if (nextAnimationState) {
|
||||||
|
for (const key in nextAnimationState) {
|
||||||
|
if (
|
||||||
|
nextAnimationState[
|
||||||
|
key as keyof InteractiveSceneRenderAnimationState
|
||||||
|
] !== undefined
|
||||||
|
) {
|
||||||
|
return nextAnimationState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
},
|
},
|
||||||
isRenderThrottlingEnabled(),
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -201,8 +246,9 @@ const getRelevantAppStateProps = (
|
|||||||
selectedGroupIds: appState.selectedGroupIds,
|
selectedGroupIds: appState.selectedGroupIds,
|
||||||
selectedLinearElement: appState.selectedLinearElement,
|
selectedLinearElement: appState.selectedLinearElement,
|
||||||
multiElement: appState.multiElement,
|
multiElement: appState.multiElement,
|
||||||
|
newElement: appState.newElement,
|
||||||
isBindingEnabled: appState.isBindingEnabled,
|
isBindingEnabled: appState.isBindingEnabled,
|
||||||
suggestedBindings: appState.suggestedBindings,
|
suggestedBinding: appState.suggestedBinding,
|
||||||
isRotating: appState.isRotating,
|
isRotating: appState.isRotating,
|
||||||
elementsToHighlight: appState.elementsToHighlight,
|
elementsToHighlight: appState.elementsToHighlight,
|
||||||
collaborators: appState.collaborators, // Necessary for collab. sessions
|
collaborators: appState.collaborators, // Necessary for collab. sessions
|
||||||
@ -214,6 +260,10 @@ const getRelevantAppStateProps = (
|
|||||||
croppingElementId: appState.croppingElementId,
|
croppingElementId: appState.croppingElementId,
|
||||||
searchMatches: appState.searchMatches,
|
searchMatches: appState.searchMatches,
|
||||||
activeLockedId: appState.activeLockedId,
|
activeLockedId: appState.activeLockedId,
|
||||||
|
hoveredElementIds: appState.hoveredElementIds,
|
||||||
|
frameRendering: appState.frameRendering,
|
||||||
|
shouldCacheIgnoreZoom: appState.shouldCacheIgnoreZoom,
|
||||||
|
exportScale: appState.exportScale,
|
||||||
});
|
});
|
||||||
|
|
||||||
const areEqual = (
|
const areEqual = (
|
||||||
|
|||||||
@ -99,6 +99,7 @@ const getRelevantAppStateProps = (appState: AppState): StaticCanvasAppState => {
|
|||||||
editingGroupId: appState.editingGroupId,
|
editingGroupId: appState.editingGroupId,
|
||||||
currentHoveredFontFamily: appState.currentHoveredFontFamily,
|
currentHoveredFontFamily: appState.currentHoveredFontFamily,
|
||||||
croppingElementId: appState.croppingElementId,
|
croppingElementId: appState.croppingElementId,
|
||||||
|
suggestedBinding: appState.suggestedBinding,
|
||||||
};
|
};
|
||||||
|
|
||||||
return relevantAppStateProps;
|
return relevantAppStateProps;
|
||||||
|
|||||||
@ -88,8 +88,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "ellipse-1",
|
"elementId": "ellipse-1",
|
||||||
"focus": -0.007519379844961235,
|
"fixedPoint": [
|
||||||
"gap": 11.562288374879595,
|
0.04,
|
||||||
|
0.4633333333333333,
|
||||||
|
],
|
||||||
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
@ -98,7 +101,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
@ -118,8 +120,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id49",
|
"elementId": "id49",
|
||||||
"focus": -0.0813953488372095,
|
"fixedPoint": [
|
||||||
"gap": 1,
|
1,
|
||||||
|
0.5001,
|
||||||
|
],
|
||||||
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"strokeColor": "#1864ab",
|
"strokeColor": "#1864ab",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -144,8 +149,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "ellipse-1",
|
"elementId": "ellipse-1",
|
||||||
"focus": 0.10666666666666667,
|
"fixedPoint": [
|
||||||
"gap": 3.8343264684446097,
|
-0.01,
|
||||||
|
0.44666666666666666,
|
||||||
|
],
|
||||||
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
@ -154,7 +162,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a3",
|
"index": "a3",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
@ -174,8 +181,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "diamond-1",
|
"elementId": "diamond-1",
|
||||||
"focus": 0,
|
"fixedPoint": [
|
||||||
"gap": 4.535423522449215,
|
0.9357142857142857,
|
||||||
|
0.5001,
|
||||||
|
],
|
||||||
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"strokeColor": "#e67700",
|
"strokeColor": "#e67700",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -334,8 +344,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "text-2",
|
"elementId": "text-2",
|
||||||
"focus": 0,
|
"fixedPoint": [
|
||||||
"gap": 16,
|
-2.05,
|
||||||
|
0.5001,
|
||||||
|
],
|
||||||
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
@ -344,7 +357,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
@ -364,8 +376,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "text-1",
|
"elementId": "text-1",
|
||||||
"focus": 0,
|
"fixedPoint": [
|
||||||
"gap": 1,
|
1,
|
||||||
|
0.5001,
|
||||||
|
],
|
||||||
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -436,8 +451,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id42",
|
"elementId": "id42",
|
||||||
"focus": -0,
|
"fixedPoint": [
|
||||||
"gap": 1,
|
0,
|
||||||
|
0.5001,
|
||||||
|
],
|
||||||
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
@ -446,7 +464,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
@ -466,8 +483,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id41",
|
"elementId": "id41",
|
||||||
"focus": 0,
|
"fixedPoint": [
|
||||||
"gap": 1,
|
1,
|
||||||
|
0.5001,
|
||||||
|
],
|
||||||
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -612,8 +632,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id46",
|
"elementId": "id46",
|
||||||
"focus": -0,
|
"fixedPoint": [
|
||||||
"gap": 1,
|
0,
|
||||||
|
0.5001,
|
||||||
|
],
|
||||||
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
@ -622,7 +645,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
@ -642,8 +664,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id45",
|
"elementId": "id45",
|
||||||
"focus": 0,
|
"fixedPoint": [
|
||||||
"gap": 1,
|
1,
|
||||||
|
0.5001,
|
||||||
|
],
|
||||||
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -839,7 +864,6 @@ exports[`Test Transform > should transform linear elements 1`] = `
|
|||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
@ -887,7 +911,6 @@ exports[`Test Transform > should transform linear elements 2`] = `
|
|||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a1",
|
"index": "a1",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
@ -934,7 +957,6 @@ exports[`Test Transform > should transform linear elements 3`] = `
|
|||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
@ -982,7 +1004,6 @@ exports[`Test Transform > should transform linear elements 4`] = `
|
|||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a3",
|
"index": "a3",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
@ -1476,8 +1497,11 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "Alice",
|
"elementId": "Alice",
|
||||||
"focus": -0,
|
"fixedPoint": [
|
||||||
"gap": 5.299874999999986,
|
-0.07542628418945944,
|
||||||
|
0.5001,
|
||||||
|
],
|
||||||
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
@ -1486,7 +1510,6 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a4",
|
"index": "a4",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
@ -1508,8 +1531,11 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "Bob",
|
"elementId": "Bob",
|
||||||
"focus": 0,
|
"fixedPoint": [
|
||||||
"gap": 1,
|
1.000004978564514,
|
||||||
|
0.5001,
|
||||||
|
],
|
||||||
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -1539,8 +1565,11 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "B",
|
"elementId": "B",
|
||||||
"focus": 0,
|
"fixedPoint": [
|
||||||
"gap": 32,
|
0.46387050630528887,
|
||||||
|
0.48466257668711654,
|
||||||
|
],
|
||||||
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
@ -1549,7 +1578,6 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a5",
|
"index": "a5",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
@ -1567,8 +1595,11 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "Bob",
|
"elementId": "Bob",
|
||||||
"focus": 0,
|
"fixedPoint": [
|
||||||
"gap": 1,
|
0.39381496335223337,
|
||||||
|
1,
|
||||||
|
],
|
||||||
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
@ -1858,7 +1889,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
@ -1911,7 +1941,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a1",
|
"index": "a1",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
@ -1964,7 +1993,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
@ -2017,7 +2045,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
"id": Any<String>,
|
"id": Any<String>,
|
||||||
"index": "a3",
|
"index": "a3",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
|
|||||||
@ -7,8 +7,6 @@ import {
|
|||||||
isPromiseLike,
|
isPromiseLike,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import { clearElementsForExport } from "@excalidraw/element";
|
|
||||||
|
|
||||||
import type { ValueOf } from "@excalidraw/common/utility-types";
|
import type { ValueOf } from "@excalidraw/common/utility-types";
|
||||||
import type { ExcalidrawElement, FileId } from "@excalidraw/element/types";
|
import type { ExcalidrawElement, FileId } from "@excalidraw/element/types";
|
||||||
|
|
||||||
@ -159,7 +157,7 @@ export const loadSceneOrLibraryFromBlob = async (
|
|||||||
type: MIME_TYPES.excalidraw,
|
type: MIME_TYPES.excalidraw,
|
||||||
data: restore(
|
data: restore(
|
||||||
{
|
{
|
||||||
elements: clearElementsForExport(data.elements || []),
|
elements: data.elements || [],
|
||||||
appState: {
|
appState: {
|
||||||
theme: localAppState?.theme,
|
theme: localAppState?.theme,
|
||||||
fileHandle: fileHandle || blob.handle || null,
|
fileHandle: fileHandle || blob.handle || null,
|
||||||
|
|||||||
@ -6,11 +6,6 @@ import {
|
|||||||
VERSIONS,
|
VERSIONS,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
|
||||||
clearElementsForDatabase,
|
|
||||||
clearElementsForExport,
|
|
||||||
} from "@excalidraw/element";
|
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { cleanAppStateForExport, clearAppStateForDatabase } from "../appState";
|
import { cleanAppStateForExport, clearAppStateForDatabase } from "../appState";
|
||||||
@ -57,10 +52,7 @@ export const serializeAsJSON = (
|
|||||||
type: EXPORT_DATA_TYPES.excalidraw,
|
type: EXPORT_DATA_TYPES.excalidraw,
|
||||||
version: VERSIONS.excalidraw,
|
version: VERSIONS.excalidraw,
|
||||||
source: getExportSource(),
|
source: getExportSource(),
|
||||||
elements:
|
elements,
|
||||||
type === "local"
|
|
||||||
? clearElementsForExport(elements)
|
|
||||||
: clearElementsForDatabase(elements),
|
|
||||||
appState:
|
appState:
|
||||||
type === "local"
|
type === "local"
|
||||||
? cleanAppStateForExport(appState)
|
? cleanAppStateForExport(appState)
|
||||||
|
|||||||
@ -32,7 +32,6 @@ import {
|
|||||||
isArrowBoundToElement,
|
isArrowBoundToElement,
|
||||||
isArrowElement,
|
isArrowElement,
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
isFixedPointBinding,
|
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
isLineElement,
|
isLineElement,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
@ -61,7 +60,6 @@ import type {
|
|||||||
FontFamilyValues,
|
FontFamilyValues,
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
PointBinding,
|
|
||||||
StrokeRoundness,
|
StrokeRoundness,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
@ -123,36 +121,29 @@ const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => {
|
|||||||
|
|
||||||
const repairBinding = <T extends ExcalidrawLinearElement>(
|
const repairBinding = <T extends ExcalidrawLinearElement>(
|
||||||
element: T,
|
element: T,
|
||||||
binding: PointBinding | FixedPointBinding | null,
|
binding: FixedPointBinding | null,
|
||||||
): T extends ExcalidrawElbowArrowElement
|
): FixedPointBinding | null => {
|
||||||
? FixedPointBinding | null
|
|
||||||
: PointBinding | FixedPointBinding | null => {
|
|
||||||
if (!binding) {
|
if (!binding) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const focus = binding.focus || 0;
|
|
||||||
|
|
||||||
if (isElbowArrow(element)) {
|
if (isElbowArrow(element)) {
|
||||||
const fixedPointBinding:
|
const fixedPointBinding:
|
||||||
| ExcalidrawElbowArrowElement["startBinding"]
|
| ExcalidrawElbowArrowElement["startBinding"]
|
||||||
| ExcalidrawElbowArrowElement["endBinding"] = isFixedPointBinding(binding)
|
| ExcalidrawElbowArrowElement["endBinding"] = {
|
||||||
? {
|
|
||||||
...binding,
|
...binding,
|
||||||
focus,
|
|
||||||
fixedPoint: normalizeFixedPoint(binding.fixedPoint ?? [0, 0]),
|
fixedPoint: normalizeFixedPoint(binding.fixedPoint ?? [0, 0]),
|
||||||
}
|
mode: binding.mode || "orbit",
|
||||||
: null;
|
};
|
||||||
|
|
||||||
return fixedPointBinding;
|
return fixedPointBinding;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...binding,
|
elementId: binding.elementId,
|
||||||
focus,
|
mode: binding.mode || "orbit",
|
||||||
} as T extends ExcalidrawElbowArrowElement
|
fixedPoint: normalizeFixedPoint(binding.fixedPoint || [0.51, 0.51]),
|
||||||
? FixedPointBinding | null
|
} as FixedPointBinding | null;
|
||||||
: PointBinding | FixedPointBinding | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const restoreElementWithProperties = <
|
const restoreElementWithProperties = <
|
||||||
@ -301,7 +292,6 @@ export const restoreElement = (
|
|||||||
case "freedraw": {
|
case "freedraw": {
|
||||||
return restoreElementWithProperties(element, {
|
return restoreElementWithProperties(element, {
|
||||||
points: element.points,
|
points: element.points,
|
||||||
lastCommittedPoint: null,
|
|
||||||
simulatePressure: element.simulatePressure,
|
simulatePressure: element.simulatePressure,
|
||||||
pressures: element.pressures,
|
pressures: element.pressures,
|
||||||
});
|
});
|
||||||
@ -337,7 +327,6 @@ export const restoreElement = (
|
|||||||
: element.type,
|
: element.type,
|
||||||
startBinding: repairBinding(element, element.startBinding),
|
startBinding: repairBinding(element, element.startBinding),
|
||||||
endBinding: repairBinding(element, element.endBinding),
|
endBinding: repairBinding(element, element.endBinding),
|
||||||
lastCommittedPoint: null,
|
|
||||||
startArrowhead,
|
startArrowhead,
|
||||||
endArrowhead,
|
endArrowhead,
|
||||||
points,
|
points,
|
||||||
@ -370,7 +359,6 @@ export const restoreElement = (
|
|||||||
type: element.type,
|
type: element.type,
|
||||||
startBinding: repairBinding(element, element.startBinding),
|
startBinding: repairBinding(element, element.startBinding),
|
||||||
endBinding: repairBinding(element, element.endBinding),
|
endBinding: repairBinding(element, element.endBinding),
|
||||||
lastCommittedPoint: null,
|
|
||||||
startArrowhead,
|
startArrowhead,
|
||||||
endArrowhead,
|
endArrowhead,
|
||||||
points,
|
points,
|
||||||
|
|||||||
@ -432,12 +432,9 @@ describe("Test Transform", () => {
|
|||||||
boundElements: [{ id: text.id, type: "text" }],
|
boundElements: [{ id: text.id, type: "text" }],
|
||||||
startBinding: {
|
startBinding: {
|
||||||
elementId: rectangle.id,
|
elementId: rectangle.id,
|
||||||
focus: 0,
|
|
||||||
gap: 1,
|
|
||||||
},
|
},
|
||||||
endBinding: {
|
endBinding: {
|
||||||
elementId: ellipse.id,
|
elementId: ellipse.id,
|
||||||
focus: -0,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -517,12 +514,9 @@ describe("Test Transform", () => {
|
|||||||
boundElements: [{ id: text1.id, type: "text" }],
|
boundElements: [{ id: text1.id, type: "text" }],
|
||||||
startBinding: {
|
startBinding: {
|
||||||
elementId: text2.id,
|
elementId: text2.id,
|
||||||
focus: 0,
|
|
||||||
gap: 1,
|
|
||||||
},
|
},
|
||||||
endBinding: {
|
endBinding: {
|
||||||
elementId: text3.id,
|
elementId: text3.id,
|
||||||
focus: -0,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -780,8 +774,8 @@ describe("Test Transform", () => {
|
|||||||
const [arrow, rect] = excalidrawElements;
|
const [arrow, rect] = excalidrawElements;
|
||||||
expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
|
expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
|
||||||
elementId: "rect-1",
|
elementId: "rect-1",
|
||||||
focus: -0,
|
fixedPoint: [-2.05, 0.5001],
|
||||||
gap: 25,
|
mode: "orbit",
|
||||||
});
|
});
|
||||||
expect(rect.boundElements).toStrictEqual([
|
expect(rect.boundElements).toStrictEqual([
|
||||||
{
|
{
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import {
|
|||||||
getLineHeight,
|
getLineHeight,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import { bindLinearElement } from "@excalidraw/element";
|
import { bindBindingElement } from "@excalidraw/element";
|
||||||
import {
|
import {
|
||||||
newArrowElement,
|
newArrowElement,
|
||||||
newElement,
|
newElement,
|
||||||
@ -330,9 +330,10 @@ const bindLinearElementToElement = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bindLinearElement(
|
bindBindingElement(
|
||||||
linearElement,
|
linearElement,
|
||||||
startBoundElement as ExcalidrawBindableElement,
|
startBoundElement as ExcalidrawBindableElement,
|
||||||
|
"orbit",
|
||||||
"start",
|
"start",
|
||||||
scene,
|
scene,
|
||||||
);
|
);
|
||||||
@ -405,9 +406,10 @@ const bindLinearElementToElement = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bindLinearElement(
|
bindBindingElement(
|
||||||
linearElement,
|
linearElement,
|
||||||
endBoundElement as ExcalidrawBindableElement,
|
endBoundElement as ExcalidrawBindableElement,
|
||||||
|
"orbit",
|
||||||
"end",
|
"end",
|
||||||
scene,
|
scene,
|
||||||
);
|
);
|
||||||
|
|||||||
5
packages/excalidraw/global.d.ts
vendored
5
packages/excalidraw/global.d.ts
vendored
@ -101,7 +101,10 @@ declare module "image-blob-reduce" {
|
|||||||
|
|
||||||
interface CustomMatchers {
|
interface CustomMatchers {
|
||||||
toBeNonNaNNumber(): void;
|
toBeNonNaNNumber(): void;
|
||||||
toCloselyEqualPoints(points: readonly [number, number][]): void;
|
toCloselyEqualPoints(
|
||||||
|
points: readonly [number, number][],
|
||||||
|
precision?: number,
|
||||||
|
): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare namespace jest {
|
declare namespace jest {
|
||||||
|
|||||||
@ -332,6 +332,7 @@
|
|||||||
"dismissSearch": "Escape to dismiss search",
|
"dismissSearch": "Escape to dismiss search",
|
||||||
"canvasPanning": "To move canvas, hold mouse wheel or spacebar while dragging, or use the hand tool",
|
"canvasPanning": "To move canvas, hold mouse wheel or spacebar while dragging, or use the hand tool",
|
||||||
"linearElement": "Click to start multiple points, drag for single line",
|
"linearElement": "Click to start multiple points, drag for single line",
|
||||||
|
"arrowBindModifiers": "Hold Alt to bind inside, or CtrlOrCmd to disable binding",
|
||||||
"arrowTool": "Click to start multiple points, drag for single line. Press {{arrowShortcut}} again to change arrow type.",
|
"arrowTool": "Click to start multiple points, drag for single line. Press {{arrowShortcut}} again to change arrow type.",
|
||||||
"freeDraw": "Click and drag, release when you're finished",
|
"freeDraw": "Click and drag, release when you're finished",
|
||||||
"text": "Tip: you can also add text by double-clicking anywhere with the selection tool",
|
"text": "Tip: you can also add text by double-clicking anywhere with the selection tool",
|
||||||
|
|||||||
@ -81,8 +81,8 @@
|
|||||||
"@braintree/sanitize-url": "6.0.2",
|
"@braintree/sanitize-url": "6.0.2",
|
||||||
"@excalidraw/common": "0.18.0",
|
"@excalidraw/common": "0.18.0",
|
||||||
"@excalidraw/element": "0.18.0",
|
"@excalidraw/element": "0.18.0",
|
||||||
"@excalidraw/math": "0.18.0",
|
|
||||||
"@excalidraw/laser-pointer": "1.3.1",
|
"@excalidraw/laser-pointer": "1.3.1",
|
||||||
|
"@excalidraw/math": "0.18.0",
|
||||||
"@excalidraw/mermaid-to-excalidraw": "1.1.3",
|
"@excalidraw/mermaid-to-excalidraw": "1.1.3",
|
||||||
"@excalidraw/random-username": "1.1.0",
|
"@excalidraw/random-username": "1.1.0",
|
||||||
"@radix-ui/react-popover": "1.1.6",
|
"@radix-ui/react-popover": "1.1.6",
|
||||||
@ -97,8 +97,8 @@
|
|||||||
"image-blob-reduce": "3.0.1",
|
"image-blob-reduce": "3.0.1",
|
||||||
"jotai": "2.11.0",
|
"jotai": "2.11.0",
|
||||||
"jotai-scope": "0.7.2",
|
"jotai-scope": "0.7.2",
|
||||||
"lodash.throttle": "4.1.1",
|
|
||||||
"lodash.debounce": "4.0.8",
|
"lodash.debounce": "4.0.8",
|
||||||
|
"lodash.throttle": "4.1.1",
|
||||||
"nanoid": "3.3.3",
|
"nanoid": "3.3.3",
|
||||||
"open-color": "1.9.1",
|
"open-color": "1.9.1",
|
||||||
"pako": "2.0.3",
|
"pako": "2.0.3",
|
||||||
|
|||||||
84
packages/excalidraw/renderer/animation.ts
Normal file
84
packages/excalidraw/renderer/animation.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { isRenderThrottlingEnabled } from "../reactUtils";
|
||||||
|
|
||||||
|
export type Animation<R extends object> = (params: {
|
||||||
|
deltaTime: number;
|
||||||
|
state?: R;
|
||||||
|
}) => R | null | undefined;
|
||||||
|
|
||||||
|
export class AnimationController {
|
||||||
|
private static isRunning = false;
|
||||||
|
private static animations = new Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
animation: Animation<any>;
|
||||||
|
lastTime: number;
|
||||||
|
state: any;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
|
||||||
|
static start<R extends object>(key: string, animation: Animation<R>) {
|
||||||
|
const initialState = animation({
|
||||||
|
deltaTime: 0,
|
||||||
|
state: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (initialState) {
|
||||||
|
AnimationController.animations.set(key, {
|
||||||
|
animation,
|
||||||
|
lastTime: 0,
|
||||||
|
state: initialState,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!AnimationController.isRunning) {
|
||||||
|
AnimationController.isRunning = true;
|
||||||
|
|
||||||
|
if (isRenderThrottlingEnabled()) {
|
||||||
|
requestAnimationFrame(AnimationController.tick);
|
||||||
|
} else {
|
||||||
|
setTimeout(AnimationController.tick, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static tick() {
|
||||||
|
if (AnimationController.animations.size > 0) {
|
||||||
|
for (const [key, animation] of AnimationController.animations) {
|
||||||
|
const now = performance.now();
|
||||||
|
const deltaTime =
|
||||||
|
animation.lastTime === 0 ? 0 : now - animation.lastTime;
|
||||||
|
|
||||||
|
const state = animation.animation({
|
||||||
|
deltaTime,
|
||||||
|
state: animation.state,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!state) {
|
||||||
|
AnimationController.animations.delete(key);
|
||||||
|
|
||||||
|
if (AnimationController.animations.size === 0) {
|
||||||
|
AnimationController.isRunning = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
animation.lastTime = now;
|
||||||
|
animation.state = state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRenderThrottlingEnabled()) {
|
||||||
|
requestAnimationFrame(AnimationController.tick);
|
||||||
|
} else {
|
||||||
|
setTimeout(AnimationController.tick, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static running(key: string) {
|
||||||
|
return AnimationController.animations.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
static cancel(key: string) {
|
||||||
|
AnimationController.animations.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,26 +1,5 @@
|
|||||||
import { THEME, THEME_FILTER } from "@excalidraw/common";
|
import { THEME, THEME_FILTER } from "@excalidraw/common";
|
||||||
|
|
||||||
import { FIXED_BINDING_DISTANCE } from "@excalidraw/element";
|
|
||||||
import { getDiamondPoints } from "@excalidraw/element";
|
|
||||||
import { elementCenterPoint, getCornerRadius } from "@excalidraw/element";
|
|
||||||
|
|
||||||
import {
|
|
||||||
curve,
|
|
||||||
curveCatmullRomCubicApproxPoints,
|
|
||||||
curveCatmullRomQuadraticApproxPoints,
|
|
||||||
curveOffsetPoints,
|
|
||||||
type GlobalPoint,
|
|
||||||
offsetPointsForQuadraticBezier,
|
|
||||||
pointFrom,
|
|
||||||
pointRotateRads,
|
|
||||||
} from "@excalidraw/math";
|
|
||||||
|
|
||||||
import type {
|
|
||||||
ElementsMap,
|
|
||||||
ExcalidrawDiamondElement,
|
|
||||||
ExcalidrawRectanguloidElement,
|
|
||||||
} from "@excalidraw/element/types";
|
|
||||||
|
|
||||||
import type { StaticCanvasRenderConfig } from "../scene/types";
|
import type { StaticCanvasRenderConfig } from "../scene/types";
|
||||||
import type { AppState, StaticCanvasAppState } from "../types";
|
import type { AppState, StaticCanvasAppState } from "../types";
|
||||||
|
|
||||||
@ -97,163 +76,6 @@ export const bootstrapCanvas = ({
|
|||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
function drawCatmullRomQuadraticApprox(
|
|
||||||
ctx: CanvasRenderingContext2D,
|
|
||||||
points: GlobalPoint[],
|
|
||||||
tension = 0.5,
|
|
||||||
) {
|
|
||||||
const pointSets = curveCatmullRomQuadraticApproxPoints(points, tension);
|
|
||||||
if (pointSets) {
|
|
||||||
for (let i = 0; i < pointSets.length - 1; i++) {
|
|
||||||
const [[cpX, cpY], [p2X, p2Y]] = pointSets[i];
|
|
||||||
|
|
||||||
ctx.quadraticCurveTo(cpX, cpY, p2X, p2Y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawCatmullRomCubicApprox(
|
|
||||||
ctx: CanvasRenderingContext2D,
|
|
||||||
points: GlobalPoint[],
|
|
||||||
tension = 0.5,
|
|
||||||
) {
|
|
||||||
const pointSets = curveCatmullRomCubicApproxPoints(points, tension);
|
|
||||||
if (pointSets) {
|
|
||||||
for (let i = 0; i < pointSets.length; i++) {
|
|
||||||
const [[cp1x, cp1y], [cp2x, cp2y], [x, y]] = pointSets[i];
|
|
||||||
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const drawHighlightForRectWithRotation = (
|
|
||||||
context: CanvasRenderingContext2D,
|
|
||||||
element: ExcalidrawRectanguloidElement,
|
|
||||||
elementsMap: ElementsMap,
|
|
||||||
padding: number,
|
|
||||||
) => {
|
|
||||||
const [x, y] = pointRotateRads(
|
|
||||||
pointFrom<GlobalPoint>(element.x, element.y),
|
|
||||||
elementCenterPoint(element, elementsMap),
|
|
||||||
element.angle,
|
|
||||||
);
|
|
||||||
|
|
||||||
context.save();
|
|
||||||
context.translate(x, y);
|
|
||||||
context.rotate(element.angle);
|
|
||||||
|
|
||||||
let radius = getCornerRadius(
|
|
||||||
Math.min(element.width, element.height),
|
|
||||||
element,
|
|
||||||
);
|
|
||||||
if (radius === 0) {
|
|
||||||
radius = 0.01;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.beginPath();
|
|
||||||
|
|
||||||
{
|
|
||||||
const topLeftApprox = offsetPointsForQuadraticBezier(
|
|
||||||
pointFrom(0, 0 + radius),
|
|
||||||
pointFrom(0, 0),
|
|
||||||
pointFrom(0 + radius, 0),
|
|
||||||
padding,
|
|
||||||
);
|
|
||||||
const topRightApprox = offsetPointsForQuadraticBezier(
|
|
||||||
pointFrom(element.width - radius, 0),
|
|
||||||
pointFrom(element.width, 0),
|
|
||||||
pointFrom(element.width, radius),
|
|
||||||
padding,
|
|
||||||
);
|
|
||||||
const bottomRightApprox = offsetPointsForQuadraticBezier(
|
|
||||||
pointFrom(element.width, element.height - radius),
|
|
||||||
pointFrom(element.width, element.height),
|
|
||||||
pointFrom(element.width - radius, element.height),
|
|
||||||
padding,
|
|
||||||
);
|
|
||||||
const bottomLeftApprox = offsetPointsForQuadraticBezier(
|
|
||||||
pointFrom(radius, element.height),
|
|
||||||
pointFrom(0, element.height),
|
|
||||||
pointFrom(0, element.height - radius),
|
|
||||||
padding,
|
|
||||||
);
|
|
||||||
|
|
||||||
context.moveTo(
|
|
||||||
topLeftApprox[topLeftApprox.length - 1][0],
|
|
||||||
topLeftApprox[topLeftApprox.length - 1][1],
|
|
||||||
);
|
|
||||||
context.lineTo(topRightApprox[0][0], topRightApprox[0][1]);
|
|
||||||
drawCatmullRomQuadraticApprox(context, topRightApprox);
|
|
||||||
context.lineTo(bottomRightApprox[0][0], bottomRightApprox[0][1]);
|
|
||||||
drawCatmullRomQuadraticApprox(context, bottomRightApprox);
|
|
||||||
context.lineTo(bottomLeftApprox[0][0], bottomLeftApprox[0][1]);
|
|
||||||
drawCatmullRomQuadraticApprox(context, bottomLeftApprox);
|
|
||||||
context.lineTo(topLeftApprox[0][0], topLeftApprox[0][1]);
|
|
||||||
drawCatmullRomQuadraticApprox(context, topLeftApprox);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Counter-clockwise for the cutout in the middle. We need to have an "inverse
|
|
||||||
// mask" on a filled shape for the diamond highlight, because stroking creates
|
|
||||||
// sharp inset edges on line joins < 90 degrees.
|
|
||||||
{
|
|
||||||
const topLeftApprox = offsetPointsForQuadraticBezier(
|
|
||||||
pointFrom(0 + radius, 0),
|
|
||||||
pointFrom(0, 0),
|
|
||||||
pointFrom(0, 0 + radius),
|
|
||||||
-FIXED_BINDING_DISTANCE,
|
|
||||||
);
|
|
||||||
const topRightApprox = offsetPointsForQuadraticBezier(
|
|
||||||
pointFrom(element.width, radius),
|
|
||||||
pointFrom(element.width, 0),
|
|
||||||
pointFrom(element.width - radius, 0),
|
|
||||||
-FIXED_BINDING_DISTANCE,
|
|
||||||
);
|
|
||||||
const bottomRightApprox = offsetPointsForQuadraticBezier(
|
|
||||||
pointFrom(element.width - radius, element.height),
|
|
||||||
pointFrom(element.width, element.height),
|
|
||||||
pointFrom(element.width, element.height - radius),
|
|
||||||
-FIXED_BINDING_DISTANCE,
|
|
||||||
);
|
|
||||||
const bottomLeftApprox = offsetPointsForQuadraticBezier(
|
|
||||||
pointFrom(0, element.height - radius),
|
|
||||||
pointFrom(0, element.height),
|
|
||||||
pointFrom(radius, element.height),
|
|
||||||
-FIXED_BINDING_DISTANCE,
|
|
||||||
);
|
|
||||||
|
|
||||||
context.moveTo(
|
|
||||||
topLeftApprox[topLeftApprox.length - 1][0],
|
|
||||||
topLeftApprox[topLeftApprox.length - 1][1],
|
|
||||||
);
|
|
||||||
context.lineTo(bottomLeftApprox[0][0], bottomLeftApprox[0][1]);
|
|
||||||
drawCatmullRomQuadraticApprox(context, bottomLeftApprox);
|
|
||||||
context.lineTo(bottomRightApprox[0][0], bottomRightApprox[0][1]);
|
|
||||||
drawCatmullRomQuadraticApprox(context, bottomRightApprox);
|
|
||||||
context.lineTo(topRightApprox[0][0], topRightApprox[0][1]);
|
|
||||||
drawCatmullRomQuadraticApprox(context, topRightApprox);
|
|
||||||
context.lineTo(topLeftApprox[0][0], topLeftApprox[0][1]);
|
|
||||||
drawCatmullRomQuadraticApprox(context, topLeftApprox);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.closePath();
|
|
||||||
context.fill();
|
|
||||||
|
|
||||||
context.restore();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const strokeEllipseWithRotation = (
|
|
||||||
context: CanvasRenderingContext2D,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
cx: number,
|
|
||||||
cy: number,
|
|
||||||
angle: number,
|
|
||||||
) => {
|
|
||||||
context.beginPath();
|
|
||||||
context.ellipse(cx, cy, width / 2, height / 2, angle, 0, Math.PI * 2);
|
|
||||||
context.stroke();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const strokeRectWithRotation = (
|
export const strokeRectWithRotation = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
x: number,
|
x: number,
|
||||||
@ -283,147 +105,3 @@ export const strokeRectWithRotation = (
|
|||||||
}
|
}
|
||||||
context.restore();
|
context.restore();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const drawHighlightForDiamondWithRotation = (
|
|
||||||
context: CanvasRenderingContext2D,
|
|
||||||
padding: number,
|
|
||||||
element: ExcalidrawDiamondElement,
|
|
||||||
elementsMap: ElementsMap,
|
|
||||||
) => {
|
|
||||||
const [x, y] = pointRotateRads(
|
|
||||||
pointFrom<GlobalPoint>(element.x, element.y),
|
|
||||||
elementCenterPoint(element, elementsMap),
|
|
||||||
element.angle,
|
|
||||||
);
|
|
||||||
context.save();
|
|
||||||
context.translate(x, y);
|
|
||||||
context.rotate(element.angle);
|
|
||||||
|
|
||||||
{
|
|
||||||
context.beginPath();
|
|
||||||
|
|
||||||
const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
|
|
||||||
getDiamondPoints(element);
|
|
||||||
const verticalRadius = element.roundness
|
|
||||||
? getCornerRadius(Math.abs(topX - leftX), element)
|
|
||||||
: (topX - leftX) * 0.01;
|
|
||||||
const horizontalRadius = element.roundness
|
|
||||||
? getCornerRadius(Math.abs(rightY - topY), element)
|
|
||||||
: (rightY - topY) * 0.01;
|
|
||||||
const topApprox = curveOffsetPoints(
|
|
||||||
curve(
|
|
||||||
pointFrom(topX - verticalRadius, topY + horizontalRadius),
|
|
||||||
pointFrom(topX, topY),
|
|
||||||
pointFrom(topX, topY),
|
|
||||||
pointFrom(topX + verticalRadius, topY + horizontalRadius),
|
|
||||||
),
|
|
||||||
padding,
|
|
||||||
);
|
|
||||||
const rightApprox = curveOffsetPoints(
|
|
||||||
curve(
|
|
||||||
pointFrom(rightX - verticalRadius, rightY - horizontalRadius),
|
|
||||||
pointFrom(rightX, rightY),
|
|
||||||
pointFrom(rightX, rightY),
|
|
||||||
pointFrom(rightX - verticalRadius, rightY + horizontalRadius),
|
|
||||||
),
|
|
||||||
padding,
|
|
||||||
);
|
|
||||||
const bottomApprox = curveOffsetPoints(
|
|
||||||
curve(
|
|
||||||
pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius),
|
|
||||||
pointFrom(bottomX, bottomY),
|
|
||||||
pointFrom(bottomX, bottomY),
|
|
||||||
pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius),
|
|
||||||
),
|
|
||||||
padding,
|
|
||||||
);
|
|
||||||
const leftApprox = curveOffsetPoints(
|
|
||||||
curve(
|
|
||||||
pointFrom(leftX + verticalRadius, leftY + horizontalRadius),
|
|
||||||
pointFrom(leftX, leftY),
|
|
||||||
pointFrom(leftX, leftY),
|
|
||||||
pointFrom(leftX + verticalRadius, leftY - horizontalRadius),
|
|
||||||
),
|
|
||||||
padding,
|
|
||||||
);
|
|
||||||
|
|
||||||
context.moveTo(
|
|
||||||
topApprox[topApprox.length - 1][0],
|
|
||||||
topApprox[topApprox.length - 1][1],
|
|
||||||
);
|
|
||||||
context.lineTo(rightApprox[1][0], rightApprox[1][1]);
|
|
||||||
drawCatmullRomCubicApprox(context, rightApprox);
|
|
||||||
context.lineTo(bottomApprox[1][0], bottomApprox[1][1]);
|
|
||||||
drawCatmullRomCubicApprox(context, bottomApprox);
|
|
||||||
context.lineTo(leftApprox[1][0], leftApprox[1][1]);
|
|
||||||
drawCatmullRomCubicApprox(context, leftApprox);
|
|
||||||
context.lineTo(topApprox[1][0], topApprox[1][1]);
|
|
||||||
drawCatmullRomCubicApprox(context, topApprox);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Counter-clockwise for the cutout in the middle. We need to have an "inverse
|
|
||||||
// mask" on a filled shape for the diamond highlight, because stroking creates
|
|
||||||
// sharp inset edges on line joins < 90 degrees.
|
|
||||||
{
|
|
||||||
const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
|
|
||||||
getDiamondPoints(element);
|
|
||||||
const verticalRadius = element.roundness
|
|
||||||
? getCornerRadius(Math.abs(topX - leftX), element)
|
|
||||||
: (topX - leftX) * 0.01;
|
|
||||||
const horizontalRadius = element.roundness
|
|
||||||
? getCornerRadius(Math.abs(rightY - topY), element)
|
|
||||||
: (rightY - topY) * 0.01;
|
|
||||||
const topApprox = curveOffsetPoints(
|
|
||||||
curve(
|
|
||||||
pointFrom(topX + verticalRadius, topY + horizontalRadius),
|
|
||||||
pointFrom(topX, topY),
|
|
||||||
pointFrom(topX, topY),
|
|
||||||
pointFrom(topX - verticalRadius, topY + horizontalRadius),
|
|
||||||
),
|
|
||||||
-FIXED_BINDING_DISTANCE,
|
|
||||||
);
|
|
||||||
const rightApprox = curveOffsetPoints(
|
|
||||||
curve(
|
|
||||||
pointFrom(rightX - verticalRadius, rightY + horizontalRadius),
|
|
||||||
pointFrom(rightX, rightY),
|
|
||||||
pointFrom(rightX, rightY),
|
|
||||||
pointFrom(rightX - verticalRadius, rightY - horizontalRadius),
|
|
||||||
),
|
|
||||||
-FIXED_BINDING_DISTANCE,
|
|
||||||
);
|
|
||||||
const bottomApprox = curveOffsetPoints(
|
|
||||||
curve(
|
|
||||||
pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius),
|
|
||||||
pointFrom(bottomX, bottomY),
|
|
||||||
pointFrom(bottomX, bottomY),
|
|
||||||
pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius),
|
|
||||||
),
|
|
||||||
-FIXED_BINDING_DISTANCE,
|
|
||||||
);
|
|
||||||
const leftApprox = curveOffsetPoints(
|
|
||||||
curve(
|
|
||||||
pointFrom(leftX + verticalRadius, leftY - horizontalRadius),
|
|
||||||
pointFrom(leftX, leftY),
|
|
||||||
pointFrom(leftX, leftY),
|
|
||||||
pointFrom(leftX + verticalRadius, leftY + horizontalRadius),
|
|
||||||
),
|
|
||||||
-FIXED_BINDING_DISTANCE,
|
|
||||||
);
|
|
||||||
|
|
||||||
context.moveTo(
|
|
||||||
topApprox[topApprox.length - 1][0],
|
|
||||||
topApprox[topApprox.length - 1][1],
|
|
||||||
);
|
|
||||||
context.lineTo(leftApprox[1][0], leftApprox[1][1]);
|
|
||||||
drawCatmullRomCubicApprox(context, leftApprox);
|
|
||||||
context.lineTo(bottomApprox[1][0], bottomApprox[1][1]);
|
|
||||||
drawCatmullRomCubicApprox(context, bottomApprox);
|
|
||||||
context.lineTo(rightApprox[1][0], rightApprox[1][1]);
|
|
||||||
drawCatmullRomCubicApprox(context, rightApprox);
|
|
||||||
context.lineTo(topApprox[1][0], topApprox[1][1]);
|
|
||||||
drawCatmullRomCubicApprox(context, topApprox);
|
|
||||||
}
|
|
||||||
context.closePath();
|
|
||||||
context.fill();
|
|
||||||
context.restore();
|
|
||||||
};
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
clamp,
|
||||||
pointFrom,
|
pointFrom,
|
||||||
pointsEqual,
|
pointsEqual,
|
||||||
type GlobalPoint,
|
type GlobalPoint,
|
||||||
@ -9,6 +10,7 @@ import oc from "open-color";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
arrayToMap,
|
arrayToMap,
|
||||||
|
BIND_MODE_TIMEOUT,
|
||||||
DEFAULT_TRANSFORM_HANDLE_SPACING,
|
DEFAULT_TRANSFORM_HANDLE_SPACING,
|
||||||
FRAME_STYLE,
|
FRAME_STYLE,
|
||||||
invariant,
|
invariant,
|
||||||
@ -16,8 +18,12 @@ import {
|
|||||||
throttleRAF,
|
throttleRAF,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import { FIXED_BINDING_DISTANCE, maxBindingGap } from "@excalidraw/element";
|
import {
|
||||||
import { LinearElementEditor } from "@excalidraw/element";
|
deconstructDiamondElement,
|
||||||
|
deconstructRectanguloidElement,
|
||||||
|
elementCenterPoint,
|
||||||
|
LinearElementEditor,
|
||||||
|
} from "@excalidraw/element";
|
||||||
import {
|
import {
|
||||||
getOmitSidesForDevice,
|
getOmitSidesForDevice,
|
||||||
getTransformHandles,
|
getTransformHandles,
|
||||||
@ -44,11 +50,6 @@ import {
|
|||||||
|
|
||||||
import { getCommonBounds, getElementAbsoluteCoords } from "@excalidraw/element";
|
import { getCommonBounds, getElementAbsoluteCoords } from "@excalidraw/element";
|
||||||
|
|
||||||
import type {
|
|
||||||
SuggestedBinding,
|
|
||||||
SuggestedPointBinding,
|
|
||||||
} from "@excalidraw/element";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
TransformHandles,
|
TransformHandles,
|
||||||
TransformHandleType,
|
TransformHandleType,
|
||||||
@ -64,6 +65,7 @@ import type {
|
|||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
GroupId,
|
GroupId,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
|
NonDeletedSceneElementsMap,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { renderSnaps } from "../renderer/renderSnaps";
|
import { renderSnaps } from "../renderer/renderSnaps";
|
||||||
@ -73,17 +75,18 @@ import {
|
|||||||
SCROLLBAR_COLOR,
|
SCROLLBAR_COLOR,
|
||||||
SCROLLBAR_WIDTH,
|
SCROLLBAR_WIDTH,
|
||||||
} from "../scene/scrollbars";
|
} from "../scene/scrollbars";
|
||||||
import { type InteractiveCanvasAppState } from "../types";
|
|
||||||
|
import {
|
||||||
|
type AppClassProperties,
|
||||||
|
type InteractiveCanvasAppState,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
import { getClientColor, renderRemoteCursors } from "../clients";
|
import { getClientColor, renderRemoteCursors } from "../clients";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
bootstrapCanvas,
|
bootstrapCanvas,
|
||||||
drawHighlightForDiamondWithRotation,
|
|
||||||
drawHighlightForRectWithRotation,
|
|
||||||
fillCircle,
|
fillCircle,
|
||||||
getNormalizedCanvasDimensions,
|
getNormalizedCanvasDimensions,
|
||||||
strokeEllipseWithRotation,
|
|
||||||
strokeRectWithRotation,
|
strokeRectWithRotation,
|
||||||
} from "./helpers";
|
} from "./helpers";
|
||||||
|
|
||||||
@ -189,82 +192,236 @@ const renderSingleLinearPoint = <Point extends GlobalPoint | LocalPoint>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderBindingHighlightForBindableElement = (
|
const renderBindingHighlightForBindableElement = (
|
||||||
|
app: AppClassProperties,
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
element: ExcalidrawBindableElement,
|
element: ExcalidrawBindableElement,
|
||||||
elementsMap: ElementsMap,
|
allElementsMap: NonDeletedSceneElementsMap,
|
||||||
zoom: InteractiveCanvasAppState["zoom"],
|
appState: InteractiveCanvasAppState,
|
||||||
|
deltaTime: number,
|
||||||
|
state?: { runtime: number },
|
||||||
) => {
|
) => {
|
||||||
const padding = maxBindingGap(element, element.width, element.height, zoom);
|
const countdownInProgress =
|
||||||
|
app.state.bindMode === "orbit" && app.bindModeHandler !== null;
|
||||||
|
|
||||||
context.fillStyle = "rgba(0,0,0,.05)";
|
const remainingTime =
|
||||||
|
BIND_MODE_TIMEOUT -
|
||||||
|
(state?.runtime ?? (countdownInProgress ? 0 : BIND_MODE_TIMEOUT));
|
||||||
|
const opacity = clamp((1 / BIND_MODE_TIMEOUT) * remainingTime, 0.0001, 1);
|
||||||
|
const offset = element.strokeWidth / 2;
|
||||||
|
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "rectangle":
|
|
||||||
case "text":
|
|
||||||
case "image":
|
|
||||||
case "iframe":
|
|
||||||
case "embeddable":
|
|
||||||
case "frame":
|
|
||||||
case "magicframe":
|
case "magicframe":
|
||||||
drawHighlightForRectWithRotation(context, element, elementsMap, padding);
|
case "frame":
|
||||||
|
context.save();
|
||||||
|
|
||||||
|
context.translate(
|
||||||
|
element.x + appState.scrollX,
|
||||||
|
element.y + appState.scrollY,
|
||||||
|
);
|
||||||
|
|
||||||
|
context.lineWidth = FRAME_STYLE.strokeWidth / appState.zoom.value;
|
||||||
|
context.strokeStyle =
|
||||||
|
appState.theme === THEME.DARK
|
||||||
|
? `rgba(3, 93, 161, ${opacity})`
|
||||||
|
: `rgba(106, 189, 252, ${opacity})`;
|
||||||
|
|
||||||
|
if (FRAME_STYLE.radius && context.roundRect) {
|
||||||
|
context.beginPath();
|
||||||
|
context.roundRect(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
element.width,
|
||||||
|
element.height,
|
||||||
|
FRAME_STYLE.radius / appState.zoom.value,
|
||||||
|
);
|
||||||
|
context.stroke();
|
||||||
|
context.closePath();
|
||||||
|
} else {
|
||||||
|
context.strokeRect(0, 0, element.width, element.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.restore();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
context.save();
|
||||||
|
|
||||||
|
const center = elementCenterPoint(element, allElementsMap);
|
||||||
|
const cx = center[0] + appState.scrollX;
|
||||||
|
const cy = center[1] + appState.scrollY;
|
||||||
|
|
||||||
|
context.translate(cx, cy);
|
||||||
|
context.rotate(element.angle as Radians);
|
||||||
|
context.translate(-cx, -cy);
|
||||||
|
|
||||||
|
context.translate(
|
||||||
|
element.x + appState.scrollX - offset,
|
||||||
|
element.y + appState.scrollY - offset,
|
||||||
|
);
|
||||||
|
|
||||||
|
context.lineWidth =
|
||||||
|
clamp(2.5, element.strokeWidth * 1.75, 4) /
|
||||||
|
Math.max(0.25, appState.zoom.value);
|
||||||
|
context.strokeStyle =
|
||||||
|
appState.theme === THEME.DARK
|
||||||
|
? `rgba(3, 93, 161, ${opacity / 2})`
|
||||||
|
: `rgba(106, 189, 252, ${opacity / 2})`;
|
||||||
|
|
||||||
|
switch (element.type) {
|
||||||
|
case "ellipse":
|
||||||
|
context.beginPath();
|
||||||
|
context.ellipse(
|
||||||
|
(element.width + offset * 2) / 2,
|
||||||
|
(element.height + offset * 2) / 2,
|
||||||
|
(element.width + offset * 2) / 2,
|
||||||
|
(element.height + offset * 2) / 2,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
2 * Math.PI,
|
||||||
|
);
|
||||||
|
context.closePath();
|
||||||
|
context.stroke();
|
||||||
break;
|
break;
|
||||||
case "diamond":
|
case "diamond":
|
||||||
drawHighlightForDiamondWithRotation(
|
{
|
||||||
context,
|
const [segments, curves] = deconstructDiamondElement(
|
||||||
padding,
|
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
offset,
|
||||||
);
|
|
||||||
break;
|
|
||||||
case "ellipse": {
|
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
|
||||||
const width = x2 - x1;
|
|
||||||
const height = y2 - y1;
|
|
||||||
|
|
||||||
context.strokeStyle = "rgba(0,0,0,.05)";
|
|
||||||
context.lineWidth = padding - FIXED_BINDING_DISTANCE;
|
|
||||||
|
|
||||||
strokeEllipseWithRotation(
|
|
||||||
context,
|
|
||||||
width + padding + FIXED_BINDING_DISTANCE,
|
|
||||||
height + padding + FIXED_BINDING_DISTANCE,
|
|
||||||
x1 + width / 2,
|
|
||||||
y1 + height / 2,
|
|
||||||
element.angle,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderBindingHighlightForSuggestedPointBinding = (
|
|
||||||
context: CanvasRenderingContext2D,
|
|
||||||
suggestedBinding: SuggestedPointBinding,
|
|
||||||
elementsMap: ElementsMap,
|
|
||||||
zoom: InteractiveCanvasAppState["zoom"],
|
|
||||||
) => {
|
|
||||||
const [element, startOrEnd, bindableElement] = suggestedBinding;
|
|
||||||
|
|
||||||
const threshold = maxBindingGap(
|
|
||||||
bindableElement,
|
|
||||||
bindableElement.width,
|
|
||||||
bindableElement.height,
|
|
||||||
zoom,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
context.strokeStyle = "rgba(0,0,0,0)";
|
// Draw each line segment individually
|
||||||
context.fillStyle = "rgba(0,0,0,.05)";
|
segments.forEach((segment) => {
|
||||||
|
context.beginPath();
|
||||||
const pointIndices =
|
context.moveTo(
|
||||||
startOrEnd === "both" ? [0, -1] : startOrEnd === "start" ? [0] : [-1];
|
segment[0][0] - element.x + offset,
|
||||||
pointIndices.forEach((index) => {
|
segment[0][1] - element.y + offset,
|
||||||
const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
|
||||||
element,
|
|
||||||
index,
|
|
||||||
elementsMap,
|
|
||||||
);
|
);
|
||||||
fillCircle(context, x, y, threshold, true);
|
context.lineTo(
|
||||||
|
segment[1][0] - element.x + offset,
|
||||||
|
segment[1][1] - element.y + offset,
|
||||||
|
);
|
||||||
|
context.stroke();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Draw each curve individually (for rounded corners)
|
||||||
|
curves.forEach((curve) => {
|
||||||
|
const [start, control1, control2, end] = curve;
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(
|
||||||
|
start[0] - element.x + offset,
|
||||||
|
start[1] - element.y + offset,
|
||||||
|
);
|
||||||
|
context.bezierCurveTo(
|
||||||
|
control1[0] - element.x + offset,
|
||||||
|
control1[1] - element.y + offset,
|
||||||
|
control2[0] - element.x + offset,
|
||||||
|
control2[1] - element.y + offset,
|
||||||
|
end[0] - element.x + offset,
|
||||||
|
end[1] - element.y + offset,
|
||||||
|
);
|
||||||
|
context.stroke();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
const [segments, curves] = deconstructRectanguloidElement(
|
||||||
|
element,
|
||||||
|
offset,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw each line segment individually
|
||||||
|
segments.forEach((segment) => {
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(
|
||||||
|
segment[0][0] - element.x + offset,
|
||||||
|
segment[0][1] - element.y + offset,
|
||||||
|
);
|
||||||
|
context.lineTo(
|
||||||
|
segment[1][0] - element.x + offset,
|
||||||
|
segment[1][1] - element.y + offset,
|
||||||
|
);
|
||||||
|
context.stroke();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw each curve individually (for rounded corners)
|
||||||
|
curves.forEach((curve) => {
|
||||||
|
const [start, control1, control2, end] = curve;
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(
|
||||||
|
start[0] - element.x + offset,
|
||||||
|
start[1] - element.y + offset,
|
||||||
|
);
|
||||||
|
context.bezierCurveTo(
|
||||||
|
control1[0] - element.x + offset,
|
||||||
|
control1[1] - element.y + offset,
|
||||||
|
control2[0] - element.x + offset,
|
||||||
|
control2[1] - element.y + offset,
|
||||||
|
end[0] - element.x + offset,
|
||||||
|
end[1] - element.y + offset,
|
||||||
|
);
|
||||||
|
context.stroke();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.restore();
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middle indicator is not rendered after it expired
|
||||||
|
if (!countdownInProgress || (state?.runtime ?? 0) > BIND_MODE_TIMEOUT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const radius = 0.5 * (Math.min(element.width, element.height) / 2);
|
||||||
|
|
||||||
|
// Draw center snap area
|
||||||
|
context.save();
|
||||||
|
context.translate(element.x + appState.scrollX, element.y + appState.scrollY);
|
||||||
|
|
||||||
|
const PROGRESS_RATIO = (1 / BIND_MODE_TIMEOUT) * remainingTime;
|
||||||
|
|
||||||
|
context.strokeStyle = "rgba(0, 0, 0, 0.2)";
|
||||||
|
context.lineWidth = 1 / appState.zoom.value;
|
||||||
|
context.setLineDash([4 / appState.zoom.value, 4 / appState.zoom.value]);
|
||||||
|
context.lineDashOffset = (-PROGRESS_RATIO * 10) / appState.zoom.value;
|
||||||
|
|
||||||
|
context.beginPath();
|
||||||
|
context.ellipse(
|
||||||
|
element.width / 2,
|
||||||
|
element.height / 2,
|
||||||
|
radius,
|
||||||
|
radius,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
2 * Math.PI,
|
||||||
|
);
|
||||||
|
context.stroke();
|
||||||
|
|
||||||
|
// context.strokeStyle = "transparent";
|
||||||
|
context.fillStyle = "rgba(0, 0, 0, 0.04)";
|
||||||
|
context.beginPath();
|
||||||
|
context.ellipse(
|
||||||
|
element.width / 2,
|
||||||
|
element.height / 2,
|
||||||
|
radius * (1 - opacity),
|
||||||
|
radius * (1 - opacity),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
2 * Math.PI,
|
||||||
|
);
|
||||||
|
|
||||||
|
context.fill();
|
||||||
|
|
||||||
|
context.restore();
|
||||||
|
|
||||||
|
return {
|
||||||
|
runtime: (state?.runtime ?? 0) + deltaTime,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type ElementSelectionBorder = {
|
type ElementSelectionBorder = {
|
||||||
@ -336,23 +493,6 @@ const renderSelectionBorder = (
|
|||||||
context.restore();
|
context.restore();
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderBindingHighlight = (
|
|
||||||
context: CanvasRenderingContext2D,
|
|
||||||
appState: InteractiveCanvasAppState,
|
|
||||||
suggestedBinding: SuggestedBinding,
|
|
||||||
elementsMap: ElementsMap,
|
|
||||||
) => {
|
|
||||||
const renderHighlight = Array.isArray(suggestedBinding)
|
|
||||||
? renderBindingHighlightForSuggestedPointBinding
|
|
||||||
: renderBindingHighlightForBindableElement;
|
|
||||||
|
|
||||||
context.save();
|
|
||||||
context.translate(appState.scrollX, appState.scrollY);
|
|
||||||
renderHighlight(context, suggestedBinding as any, elementsMap, appState.zoom);
|
|
||||||
|
|
||||||
context.restore();
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderFrameHighlight = (
|
const renderFrameHighlight = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
appState: InteractiveCanvasAppState,
|
appState: InteractiveCanvasAppState,
|
||||||
@ -726,6 +866,7 @@ const renderTextBox = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const _renderInteractiveScene = ({
|
const _renderInteractiveScene = ({
|
||||||
|
app,
|
||||||
canvas,
|
canvas,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
visibleElements,
|
visibleElements,
|
||||||
@ -735,7 +876,14 @@ const _renderInteractiveScene = ({
|
|||||||
appState,
|
appState,
|
||||||
renderConfig,
|
renderConfig,
|
||||||
device,
|
device,
|
||||||
}: InteractiveSceneRenderConfig) => {
|
animationState,
|
||||||
|
deltaTime,
|
||||||
|
}: InteractiveSceneRenderConfig): {
|
||||||
|
scrollBars?: ReturnType<typeof getScrollBars>;
|
||||||
|
atLeastOneVisibleElement: boolean;
|
||||||
|
elementsMap: RenderableElementsMap;
|
||||||
|
animationState?: typeof animationState;
|
||||||
|
} => {
|
||||||
if (canvas === null) {
|
if (canvas === null) {
|
||||||
return { atLeastOneVisibleElement: false, elementsMap };
|
return { atLeastOneVisibleElement: false, elementsMap };
|
||||||
}
|
}
|
||||||
@ -744,6 +892,7 @@ const _renderInteractiveScene = ({
|
|||||||
canvas,
|
canvas,
|
||||||
scale,
|
scale,
|
||||||
);
|
);
|
||||||
|
let nextAnimationState = animationState;
|
||||||
|
|
||||||
const context = bootstrapCanvas({
|
const context = bootstrapCanvas({
|
||||||
canvas,
|
canvas,
|
||||||
@ -813,17 +962,24 @@ const _renderInteractiveScene = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appState.isBindingEnabled) {
|
if (appState.isBindingEnabled && appState.suggestedBinding) {
|
||||||
appState.suggestedBindings
|
nextAnimationState = {
|
||||||
.filter((binding) => binding != null)
|
...animationState,
|
||||||
.forEach((suggestedBinding) => {
|
bindingHighlight: renderBindingHighlightForBindableElement(
|
||||||
renderBindingHighlight(
|
app,
|
||||||
context,
|
context,
|
||||||
|
appState.suggestedBinding,
|
||||||
|
allElementsMap,
|
||||||
appState,
|
appState,
|
||||||
suggestedBinding!,
|
deltaTime,
|
||||||
elementsMap,
|
animationState?.bindingHighlight,
|
||||||
);
|
),
|
||||||
});
|
};
|
||||||
|
} else {
|
||||||
|
nextAnimationState = {
|
||||||
|
...animationState,
|
||||||
|
bindingHighlight: undefined,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appState.frameToHighlight) {
|
if (appState.frameToHighlight) {
|
||||||
@ -891,7 +1047,11 @@ const _renderInteractiveScene = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Paint selected elements
|
// Paint selected elements
|
||||||
if (!appState.multiElement && !appState.selectedLinearElement?.isEditing) {
|
if (
|
||||||
|
!appState.multiElement &&
|
||||||
|
!appState.newElement &&
|
||||||
|
!appState.selectedLinearElement?.isEditing
|
||||||
|
) {
|
||||||
const showBoundingBox = shouldShowBoundingBox(selectedElements, appState);
|
const showBoundingBox = shouldShowBoundingBox(selectedElements, appState);
|
||||||
|
|
||||||
const isSingleLinearElementSelected =
|
const isSingleLinearElementSelected =
|
||||||
@ -1191,6 +1351,7 @@ const _renderInteractiveScene = ({
|
|||||||
scrollBars,
|
scrollBars,
|
||||||
atLeastOneVisibleElement: visibleElements.length > 0,
|
atLeastOneVisibleElement: visibleElements.length > 0,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
animationState: nextAnimationState,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -66,6 +66,7 @@ export type InteractiveCanvasRenderConfig = {
|
|||||||
remotePointerUsernames: Map<SocketId, string>;
|
remotePointerUsernames: Map<SocketId, string>;
|
||||||
remotePointerButton: Map<SocketId, string | undefined>;
|
remotePointerButton: Map<SocketId, string | undefined>;
|
||||||
selectionColor: string;
|
selectionColor: string;
|
||||||
|
lastViewportPosition: { x: number; y: number };
|
||||||
// extra options passed to the renderer
|
// extra options passed to the renderer
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
renderScrollbars?: boolean;
|
renderScrollbars?: boolean;
|
||||||
@ -88,7 +89,12 @@ export type StaticSceneRenderConfig = {
|
|||||||
renderConfig: StaticCanvasRenderConfig;
|
renderConfig: StaticCanvasRenderConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type InteractiveSceneRenderAnimationState = {
|
||||||
|
bindingHighlight: { runtime: number } | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
export type InteractiveSceneRenderConfig = {
|
export type InteractiveSceneRenderConfig = {
|
||||||
|
app: AppClassProperties;
|
||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement | null;
|
||||||
elementsMap: RenderableElementsMap;
|
elementsMap: RenderableElementsMap;
|
||||||
visibleElements: readonly NonDeletedExcalidrawElement[];
|
visibleElements: readonly NonDeletedExcalidrawElement[];
|
||||||
@ -99,6 +105,8 @@ export type InteractiveSceneRenderConfig = {
|
|||||||
renderConfig: InteractiveCanvasRenderConfig;
|
renderConfig: InteractiveCanvasRenderConfig;
|
||||||
device: Device;
|
device: Device;
|
||||||
callback: (data: RenderInteractiveSceneCallback) => void;
|
callback: (data: RenderInteractiveSceneCallback) => void;
|
||||||
|
animationState?: InteractiveSceneRenderAnimationState;
|
||||||
|
deltaTime: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NewElementSceneRenderConfig = {
|
export type NewElementSceneRenderConfig = {
|
||||||
|
|||||||
@ -11,6 +11,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "orbit",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"items": [
|
"items": [
|
||||||
@ -982,7 +983,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"panels": 3,
|
"panels": 3,
|
||||||
},
|
},
|
||||||
"stylesPanelMode": "full",
|
"stylesPanelMode": "full",
|
||||||
"suggestedBindings": [],
|
"suggestedBinding": null,
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
"userToFollow": null,
|
"userToFollow": null,
|
||||||
@ -1083,6 +1084,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "orbit",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -1174,7 +1176,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||||||
"panels": 3,
|
"panels": 3,
|
||||||
},
|
},
|
||||||
"stylesPanelMode": "full",
|
"stylesPanelMode": "full",
|
||||||
"suggestedBindings": [],
|
"suggestedBinding": null,
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": {
|
"toast": {
|
||||||
"message": "Added to library",
|
"message": "Added to library",
|
||||||
@ -1296,6 +1298,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "orbit",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -1387,7 +1390,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"panels": 3,
|
"panels": 3,
|
||||||
},
|
},
|
||||||
"stylesPanelMode": "full",
|
"stylesPanelMode": "full",
|
||||||
"suggestedBindings": [],
|
"suggestedBinding": null,
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
"userToFollow": null,
|
"userToFollow": null,
|
||||||
@ -1626,6 +1629,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "orbit",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -1717,7 +1721,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"panels": 3,
|
"panels": 3,
|
||||||
},
|
},
|
||||||
"stylesPanelMode": "full",
|
"stylesPanelMode": "full",
|
||||||
"suggestedBindings": [],
|
"suggestedBinding": null,
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
"userToFollow": null,
|
"userToFollow": null,
|
||||||
@ -1956,6 +1960,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "orbit",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -2047,7 +2052,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
|||||||
"panels": 3,
|
"panels": 3,
|
||||||
},
|
},
|
||||||
"stylesPanelMode": "full",
|
"stylesPanelMode": "full",
|
||||||
"suggestedBindings": [],
|
"suggestedBinding": null,
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": {
|
"toast": {
|
||||||
"message": "Copied styles.",
|
"message": "Copied styles.",
|
||||||
@ -2169,6 +2174,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "orbit",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -2258,7 +2264,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||||||
"panels": 3,
|
"panels": 3,
|
||||||
},
|
},
|
||||||
"stylesPanelMode": "full",
|
"stylesPanelMode": "full",
|
||||||
"suggestedBindings": [],
|
"suggestedBinding": null,
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
"userToFollow": null,
|
"userToFollow": null,
|
||||||
@ -2409,6 +2415,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "orbit",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -2500,7 +2507,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"panels": 3,
|
"panels": 3,
|
||||||
},
|
},
|
||||||
"stylesPanelMode": "full",
|
"stylesPanelMode": "full",
|
||||||
"suggestedBindings": [],
|
"suggestedBinding": null,
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
"userToFollow": null,
|
"userToFollow": null,
|
||||||
@ -2706,6 +2713,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "orbit",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -2802,7 +2810,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"panels": 3,
|
"panels": 3,
|
||||||
},
|
},
|
||||||
"stylesPanelMode": "full",
|
"stylesPanelMode": "full",
|
||||||
"suggestedBindings": [],
|
"suggestedBinding": null,
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
"userToFollow": null,
|
"userToFollow": null,
|
||||||
@ -3077,6 +3085,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "orbit",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -3168,7 +3177,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"panels": 3,
|
"panels": 3,
|
||||||
},
|
},
|
||||||
"stylesPanelMode": "full",
|
"stylesPanelMode": "full",
|
||||||
"suggestedBindings": [],
|
"suggestedBinding": null,
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": {
|
"toast": {
|
||||||
"message": "Copied styles.",
|
"message": "Copied styles.",
|
||||||
@ -3569,6 +3578,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "orbit",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -3660,7 +3670,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"panels": 3,
|
"panels": 3,
|
||||||
},
|
},
|
||||||
"stylesPanelMode": "full",
|
"stylesPanelMode": "full",
|
||||||
"suggestedBindings": [],
|
"suggestedBinding": null,
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
"userToFollow": null,
|
"userToFollow": null,
|
||||||
@ -3891,6 +3901,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "orbit",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -3982,7 +3993,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"panels": 3,
|
"panels": 3,
|
||||||
},
|
},
|
||||||
"stylesPanelMode": "full",
|
"stylesPanelMode": "full",
|
||||||
"suggestedBindings": [],
|
"suggestedBinding": null,
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
"userToFollow": null,
|
"userToFollow": null,
|
||||||
@ -4213,6 +4224,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "orbit",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -4307,7 +4319,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"panels": 3,
|
"panels": 3,
|
||||||
},
|
},
|
||||||
"stylesPanelMode": "full",
|
"stylesPanelMode": "full",
|
||||||
"suggestedBindings": [],
|
"suggestedBinding": null,
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
"userToFollow": null,
|
"userToFollow": null,
|
||||||
@ -4623,6 +4635,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "orbit",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"items": [
|
"items": [
|
||||||
@ -5591,7 +5604,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"panels": 3,
|
"panels": 3,
|
||||||
},
|
},
|
||||||
"stylesPanelMode": "full",
|
"stylesPanelMode": "full",
|
||||||
"suggestedBindings": [],
|
"suggestedBinding": null,
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
"userToFollow": null,
|
"userToFollow": null,
|
||||||
@ -5839,6 +5852,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "orbit",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"items": [
|
"items": [
|
||||||
@ -6809,7 +6823,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"panels": 3,
|
"panels": 3,
|
||||||
},
|
},
|
||||||
"stylesPanelMode": "full",
|
"stylesPanelMode": "full",
|
||||||
"suggestedBindings": [],
|
"suggestedBinding": null,
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
"userToFollow": null,
|
"userToFollow": null,
|
||||||
@ -7106,6 +7120,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "orbit",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"items": [
|
"items": [
|
||||||
@ -7739,7 +7754,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
"panels": 3,
|
"panels": 3,
|
||||||
},
|
},
|
||||||
"stylesPanelMode": "full",
|
"stylesPanelMode": "full",
|
||||||
"suggestedBindings": [],
|
"suggestedBinding": null,
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
"userToFollow": null,
|
"userToFollow": null,
|
||||||
@ -7772,6 +7787,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "orbit",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"items": [
|
"items": [
|
||||||
@ -8737,7 +8753,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"panels": 3,
|
"panels": 3,
|
||||||
},
|
},
|
||||||
"stylesPanelMode": "full",
|
"stylesPanelMode": "full",
|
||||||
"suggestedBindings": [],
|
"suggestedBinding": null,
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
"userToFollow": null,
|
"userToFollow": null,
|
||||||
@ -8762,6 +8778,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "orbit",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"items": [
|
"items": [
|
||||||
@ -9730,7 +9747,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"panels": 3,
|
"panels": 3,
|
||||||
},
|
},
|
||||||
"stylesPanelMode": "full",
|
"stylesPanelMode": "full",
|
||||||
"suggestedBindings": [],
|
"suggestedBinding": null,
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
"userToFollow": null,
|
"userToFollow": null,
|
||||||
|
|||||||
@ -18,7 +18,6 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
|
|||||||
"id": "id0",
|
"id": "id0",
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
@ -135,7 +134,6 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
|
|||||||
"id": "id0",
|
"id": "id0",
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -126,7 +126,7 @@ exports[`move element > rectangles with binding arrow 5`] = `
|
|||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 1006504105,
|
"versionNonce": 640725609,
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@ -163,7 +163,7 @@ exports[`move element > rectangles with binding arrow 6`] = `
|
|||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 7,
|
"version": 7,
|
||||||
"versionNonce": 1984422985,
|
"versionNonce": 1051383431,
|
||||||
"width": 300,
|
"width": 300,
|
||||||
"x": 201,
|
"x": 201,
|
||||||
"y": 2,
|
"y": 2,
|
||||||
@ -180,19 +180,22 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id3",
|
"elementId": "id3",
|
||||||
"focus": "-0.46667",
|
"fixedPoint": [
|
||||||
"gap": 10,
|
"-0.03333",
|
||||||
|
"0.43333",
|
||||||
|
],
|
||||||
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": "81.40630",
|
"height": "90.03375",
|
||||||
"id": "id6",
|
"id": "id6",
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
|
"moveMidPointsWithElement": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
@ -200,8 +203,8 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"81.00000",
|
89,
|
||||||
"81.40630",
|
"90.03375",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@ -212,18 +215,21 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id0",
|
"elementId": "id0",
|
||||||
"focus": "-0.60000",
|
"fixedPoint": [
|
||||||
"gap": 10,
|
"1.10000",
|
||||||
|
"0.50010",
|
||||||
|
],
|
||||||
|
"mode": "orbit",
|
||||||
},
|
},
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 11,
|
"version": 9,
|
||||||
"versionNonce": 1573789895,
|
"versionNonce": 1996028265,
|
||||||
"width": "81.00000",
|
"width": 89,
|
||||||
"x": "110.00000",
|
"x": 106,
|
||||||
"y": 50,
|
"y": "46.01049",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -16,10 +16,6 @@ exports[`multi point mode in linear elements > arrow 3`] = `
|
|||||||
"id": "id0",
|
"id": "id0",
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": [
|
|
||||||
70,
|
|
||||||
110,
|
|
||||||
],
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
@ -49,8 +45,8 @@ exports[`multi point mode in linear elements > arrow 3`] = `
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 8,
|
"version": 5,
|
||||||
"versionNonce": 1604849351,
|
"versionNonce": 1014066025,
|
||||||
"width": 70,
|
"width": 70,
|
||||||
"x": 30,
|
"x": 30,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
@ -72,10 +68,6 @@ exports[`multi point mode in linear elements > line 3`] = `
|
|||||||
"id": "id0",
|
"id": "id0",
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": [
|
|
||||||
70,
|
|
||||||
110,
|
|
||||||
],
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
@ -104,8 +96,8 @@ exports[`multi point mode in linear elements > line 3`] = `
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "line",
|
"type": "line",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 8,
|
"version": 5,
|
||||||
"versionNonce": 1604849351,
|
"versionNonce": 1014066025,
|
||||||
"width": 70,
|
"width": 70,
|
||||||
"x": 30,
|
"x": 30,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -16,7 +16,6 @@ exports[`select single element on the scene > arrow 1`] = `
|
|||||||
"id": "id0",
|
"id": "id0",
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
@ -65,7 +64,6 @@ exports[`select single element on the scene > arrow escape 1`] = `
|
|||||||
"id": "id0",
|
"id": "id0",
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
|
|||||||
@ -16,7 +16,6 @@ exports[`restoreElements > should restore arrow element correctly 1`] = `
|
|||||||
"id": "id-arrow01",
|
"id": "id-arrow01",
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
@ -175,7 +174,6 @@ exports[`restoreElements > should restore freedraw element correctly 1`] = `
|
|||||||
"id": "id-freedraw01",
|
"id": "id-freedraw01",
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
@ -222,7 +220,6 @@ exports[`restoreElements > should restore line and draw elements correctly 1`] =
|
|||||||
"id": "id-line01",
|
"id": "id-line01",
|
||||||
"index": "a0",
|
"index": "a0",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
@ -270,7 +267,6 @@ exports[`restoreElements > should restore line and draw elements correctly 2`] =
|
|||||||
"id": "id-draw01",
|
"id": "id-draw01",
|
||||||
"index": "a1",
|
"index": "a1",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
|
|||||||
@ -157,9 +157,9 @@ describe("Test dragCreate", () => {
|
|||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`5`,
|
`6`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
|
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
@ -195,9 +195,9 @@ describe("Test dragCreate", () => {
|
|||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`5`,
|
`6`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
|
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
|
|||||||
@ -1021,7 +1021,7 @@ describe("history", () => {
|
|||||||
// leave editor
|
// leave editor
|
||||||
Keyboard.keyPress(KEYS.ESCAPE);
|
Keyboard.keyPress(KEYS.ESCAPE);
|
||||||
|
|
||||||
expect(API.getUndoStack().length).toBe(6);
|
expect(API.getUndoStack().length).toBe(5);
|
||||||
expect(API.getRedoStack().length).toBe(0);
|
expect(API.getRedoStack().length).toBe(0);
|
||||||
expect(assertSelectedElements(h.elements[0]));
|
expect(assertSelectedElements(h.elements[0]));
|
||||||
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
|
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
|
||||||
@ -1038,7 +1038,7 @@ describe("history", () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
expect(API.getUndoStack().length).toBe(5);
|
expect(API.getUndoStack().length).toBe(4);
|
||||||
expect(API.getRedoStack().length).toBe(1);
|
expect(API.getRedoStack().length).toBe(1);
|
||||||
expect(assertSelectedElements(h.elements[0]));
|
expect(assertSelectedElements(h.elements[0]));
|
||||||
expect(h.state.selectedLinearElement?.isEditing).toBe(true);
|
expect(h.state.selectedLinearElement?.isEditing).toBe(true);
|
||||||
@ -1058,11 +1058,11 @@ describe("history", () => {
|
|||||||
mouse.clickAt(0, 0);
|
mouse.clickAt(0, 0);
|
||||||
mouse.clickAt(10, 10);
|
mouse.clickAt(10, 10);
|
||||||
mouse.clickAt(20, 20);
|
mouse.clickAt(20, 20);
|
||||||
expect(API.getUndoStack().length).toBe(5);
|
expect(API.getUndoStack().length).toBe(4);
|
||||||
expect(API.getRedoStack().length).toBe(1);
|
expect(API.getRedoStack().length).toBe(1);
|
||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
expect(API.getUndoStack().length).toBe(4);
|
expect(API.getUndoStack().length).toBe(3);
|
||||||
expect(API.getRedoStack().length).toBe(2);
|
expect(API.getRedoStack().length).toBe(2);
|
||||||
expect(assertSelectedElements(h.elements[0]));
|
expect(assertSelectedElements(h.elements[0]));
|
||||||
expect(h.state.selectedLinearElement?.isEditing).toBe(true);
|
expect(h.state.selectedLinearElement?.isEditing).toBe(true);
|
||||||
@ -1079,10 +1079,10 @@ describe("history", () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
expect(API.getUndoStack().length).toBe(3);
|
expect(API.getUndoStack().length).toBe(2);
|
||||||
expect(API.getRedoStack().length).toBe(3);
|
expect(API.getRedoStack().length).toBe(3);
|
||||||
expect(assertSelectedElements(h.elements[0]));
|
expect(assertSelectedElements(h.elements[0]));
|
||||||
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false); // undo `open editor`
|
expect(h.state.selectedLinearElement?.isEditing).toBe(false); // undo `open editor`
|
||||||
expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id);
|
expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id);
|
||||||
expect(h.elements).toEqual([
|
expect(h.elements).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -1095,29 +1095,29 @@ describe("history", () => {
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Keyboard.undo();
|
// Keyboard.undo();
|
||||||
expect(API.getUndoStack().length).toBe(2);
|
// expect(API.getUndoStack().length).toBe(2);
|
||||||
expect(API.getRedoStack().length).toBe(4);
|
// expect(API.getRedoStack().length).toBe(4);
|
||||||
expect(assertSelectedElements(h.elements[0]));
|
// expect(assertSelectedElements(h.elements[0]));
|
||||||
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
|
// expect(h.state.selectedLinearElement?.isEditing).toBe(false);
|
||||||
expect(h.state.selectedLinearElement).toBeNull(); // undo `actionFinalize`
|
// expect(h.state.selectedLinearElement).toBeNull(); // undo `actionFinalize`
|
||||||
expect(h.elements).toEqual([
|
// expect(h.elements).toEqual([
|
||||||
expect.objectContaining({
|
// expect.objectContaining({
|
||||||
isDeleted: false,
|
// isDeleted: false,
|
||||||
points: [
|
// points: [
|
||||||
[0, 0],
|
// [0, 0],
|
||||||
[10, 10],
|
// [10, 10],
|
||||||
[20, 0],
|
// [20, 0],
|
||||||
],
|
// ],
|
||||||
}),
|
// }),
|
||||||
]);
|
// ]);
|
||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
expect(API.getUndoStack().length).toBe(1);
|
expect(API.getUndoStack().length).toBe(1);
|
||||||
expect(API.getRedoStack().length).toBe(5);
|
expect(API.getRedoStack().length).toBe(4);
|
||||||
expect(assertSelectedElements(h.elements[0]));
|
expect(assertSelectedElements(h.elements[0]));
|
||||||
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
|
expect(h.state.selectedLinearElement?.isEditing).toBe(false);
|
||||||
expect(h.state.selectedLinearElement).toBeNull();
|
expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id);
|
||||||
expect(h.elements).toEqual([
|
expect(h.elements).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
@ -1130,9 +1130,8 @@ describe("history", () => {
|
|||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
expect(API.getUndoStack().length).toBe(0);
|
expect(API.getUndoStack().length).toBe(0);
|
||||||
expect(API.getRedoStack().length).toBe(6);
|
expect(API.getRedoStack().length).toBe(5);
|
||||||
expect(API.getSelectedElements().length).toBe(0);
|
expect(API.getSelectedElements().length).toBe(0);
|
||||||
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
|
|
||||||
expect(h.state.selectedLinearElement).toBeNull();
|
expect(h.state.selectedLinearElement).toBeNull();
|
||||||
expect(h.elements).toEqual([
|
expect(h.elements).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -1146,10 +1145,10 @@ describe("history", () => {
|
|||||||
|
|
||||||
Keyboard.redo();
|
Keyboard.redo();
|
||||||
expect(API.getUndoStack().length).toBe(1);
|
expect(API.getUndoStack().length).toBe(1);
|
||||||
expect(API.getRedoStack().length).toBe(5);
|
expect(API.getRedoStack().length).toBe(4);
|
||||||
expect(assertSelectedElements(h.elements[0]));
|
expect(assertSelectedElements(h.elements[0]));
|
||||||
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
|
expect(h.state.selectedLinearElement?.isEditing).toBe(false);
|
||||||
expect(h.state.selectedLinearElement).toBeNull();
|
expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id);
|
||||||
expect(h.elements).toEqual([
|
expect(h.elements).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
@ -1160,25 +1159,25 @@ describe("history", () => {
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Keyboard.redo();
|
||||||
|
// expect(API.getUndoStack().length).toBe(2);
|
||||||
|
// expect(API.getRedoStack().length).toBe(3);
|
||||||
|
// expect(assertSelectedElements(h.elements[0]));
|
||||||
|
// expect(h.state.selectedLinearElement?.isEditing).toBe(false);
|
||||||
|
// expect(h.state.selectedLinearElement).toBeNull(); // undo `actionFinalize`
|
||||||
|
// expect(h.elements).toEqual([
|
||||||
|
// expect.objectContaining({
|
||||||
|
// isDeleted: false,
|
||||||
|
// points: [
|
||||||
|
// [0, 0],
|
||||||
|
// [10, 10],
|
||||||
|
// [20, 0],
|
||||||
|
// ],
|
||||||
|
// }),
|
||||||
|
// ]);
|
||||||
|
|
||||||
Keyboard.redo();
|
Keyboard.redo();
|
||||||
expect(API.getUndoStack().length).toBe(2);
|
expect(API.getUndoStack().length).toBe(2);
|
||||||
expect(API.getRedoStack().length).toBe(4);
|
|
||||||
expect(assertSelectedElements(h.elements[0]));
|
|
||||||
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
|
|
||||||
expect(h.state.selectedLinearElement).toBeNull(); // undo `actionFinalize`
|
|
||||||
expect(h.elements).toEqual([
|
|
||||||
expect.objectContaining({
|
|
||||||
isDeleted: false,
|
|
||||||
points: [
|
|
||||||
[0, 0],
|
|
||||||
[10, 10],
|
|
||||||
[20, 0],
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
Keyboard.redo();
|
|
||||||
expect(API.getUndoStack().length).toBe(3);
|
|
||||||
expect(API.getRedoStack().length).toBe(3);
|
expect(API.getRedoStack().length).toBe(3);
|
||||||
expect(assertSelectedElements(h.elements[0]));
|
expect(assertSelectedElements(h.elements[0]));
|
||||||
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false); // undo `open editor`
|
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false); // undo `open editor`
|
||||||
@ -1195,7 +1194,7 @@ describe("history", () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
Keyboard.redo();
|
Keyboard.redo();
|
||||||
expect(API.getUndoStack().length).toBe(4);
|
expect(API.getUndoStack().length).toBe(3);
|
||||||
expect(API.getRedoStack().length).toBe(2);
|
expect(API.getRedoStack().length).toBe(2);
|
||||||
expect(assertSelectedElements(h.elements[0]));
|
expect(assertSelectedElements(h.elements[0]));
|
||||||
expect(h.state.selectedLinearElement?.isEditing).toBe(true);
|
expect(h.state.selectedLinearElement?.isEditing).toBe(true);
|
||||||
@ -1212,7 +1211,7 @@ describe("history", () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
Keyboard.redo();
|
Keyboard.redo();
|
||||||
expect(API.getUndoStack().length).toBe(5);
|
expect(API.getUndoStack().length).toBe(4);
|
||||||
expect(API.getRedoStack().length).toBe(1);
|
expect(API.getRedoStack().length).toBe(1);
|
||||||
expect(assertSelectedElements(h.elements[0]));
|
expect(assertSelectedElements(h.elements[0]));
|
||||||
expect(h.state.selectedLinearElement?.isEditing).toBe(true);
|
expect(h.state.selectedLinearElement?.isEditing).toBe(true);
|
||||||
@ -1229,7 +1228,7 @@ describe("history", () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
Keyboard.redo();
|
Keyboard.redo();
|
||||||
expect(API.getUndoStack().length).toBe(6);
|
expect(API.getUndoStack().length).toBe(5);
|
||||||
expect(API.getRedoStack().length).toBe(0);
|
expect(API.getRedoStack().length).toBe(0);
|
||||||
expect(assertSelectedElements(h.elements[0]));
|
expect(assertSelectedElements(h.elements[0]));
|
||||||
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
|
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
|
||||||
@ -1589,13 +1588,13 @@ describe("history", () => {
|
|||||||
expect(API.getUndoStack().length).toBe(5);
|
expect(API.getUndoStack().length).toBe(5);
|
||||||
expect(arrow.startBinding).toEqual({
|
expect(arrow.startBinding).toEqual({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([1, 0.5001]),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
mode: "orbit",
|
||||||
});
|
});
|
||||||
expect(arrow.endBinding).toEqual({
|
expect(arrow.endBinding).toEqual({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([0, 0.5001]),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
mode: "orbit",
|
||||||
});
|
});
|
||||||
expect(rect1.boundElements).toStrictEqual([
|
expect(rect1.boundElements).toStrictEqual([
|
||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
@ -1612,13 +1611,13 @@ describe("history", () => {
|
|||||||
expect(API.getRedoStack().length).toBe(1);
|
expect(API.getRedoStack().length).toBe(1);
|
||||||
expect(arrow.startBinding).toEqual({
|
expect(arrow.startBinding).toEqual({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([1, 0.5001]),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
mode: "orbit",
|
||||||
});
|
});
|
||||||
expect(arrow.endBinding).toEqual({
|
expect(arrow.endBinding).toEqual({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([0, 0.5001]),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
mode: "orbit",
|
||||||
});
|
});
|
||||||
expect(h.elements).toEqual([
|
expect(h.elements).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -1635,13 +1634,13 @@ describe("history", () => {
|
|||||||
expect(API.getRedoStack().length).toBe(0);
|
expect(API.getRedoStack().length).toBe(0);
|
||||||
expect(arrow.startBinding).toEqual({
|
expect(arrow.startBinding).toEqual({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([1, 0.5001]),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
mode: "orbit",
|
||||||
});
|
});
|
||||||
expect(arrow.endBinding).toEqual({
|
expect(arrow.endBinding).toEqual({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([0, 0.5001]),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
mode: "orbit",
|
||||||
});
|
});
|
||||||
expect(h.elements).toEqual([
|
expect(h.elements).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -1666,13 +1665,13 @@ describe("history", () => {
|
|||||||
expect(API.getRedoStack().length).toBe(0);
|
expect(API.getRedoStack().length).toBe(0);
|
||||||
expect(arrow.startBinding).toEqual({
|
expect(arrow.startBinding).toEqual({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([1, 0.5001]),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
mode: "orbit",
|
||||||
});
|
});
|
||||||
expect(arrow.endBinding).toEqual({
|
expect(arrow.endBinding).toEqual({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([0, 0.5001]),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
mode: "orbit",
|
||||||
});
|
});
|
||||||
expect(h.elements).toEqual([
|
expect(h.elements).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -1689,13 +1688,13 @@ describe("history", () => {
|
|||||||
expect(API.getRedoStack().length).toBe(1);
|
expect(API.getRedoStack().length).toBe(1);
|
||||||
expect(arrow.startBinding).toEqual({
|
expect(arrow.startBinding).toEqual({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([1, 0.5001]),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
mode: "orbit",
|
||||||
});
|
});
|
||||||
expect(arrow.endBinding).toEqual({
|
expect(arrow.endBinding).toEqual({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([0, 0.5001]),
|
||||||
gap: expect.toBeNonNaNNumber(),
|
mode: "orbit",
|
||||||
});
|
});
|
||||||
expect(h.elements).toEqual([
|
expect(h.elements).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -1744,13 +1743,19 @@ describe("history", () => {
|
|||||||
id: arrow.id,
|
id: arrow.id,
|
||||||
startBinding: expect.objectContaining({
|
startBinding: expect.objectContaining({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([
|
||||||
gap: expect.toBeNonNaNNumber(),
|
expect.toBeNonNaNNumber(),
|
||||||
|
expect.toBeNonNaNNumber(),
|
||||||
|
]),
|
||||||
|
mode: "orbit",
|
||||||
}),
|
}),
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([
|
||||||
gap: expect.toBeNonNaNNumber(),
|
expect.toBeNonNaNNumber(),
|
||||||
|
expect.toBeNonNaNNumber(),
|
||||||
|
]),
|
||||||
|
mode: "orbit",
|
||||||
}),
|
}),
|
||||||
isDeleted: true,
|
isDeleted: true,
|
||||||
}),
|
}),
|
||||||
@ -1789,13 +1794,19 @@ describe("history", () => {
|
|||||||
id: arrow.id,
|
id: arrow.id,
|
||||||
startBinding: expect.objectContaining({
|
startBinding: expect.objectContaining({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([
|
||||||
gap: expect.toBeNonNaNNumber(),
|
expect.toBeNonNaNNumber(),
|
||||||
|
expect.toBeNonNaNNumber(),
|
||||||
|
]),
|
||||||
|
mode: "orbit",
|
||||||
}),
|
}),
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([
|
||||||
gap: expect.toBeNonNaNNumber(),
|
expect.toBeNonNaNNumber(),
|
||||||
|
expect.toBeNonNaNNumber(),
|
||||||
|
]),
|
||||||
|
mode: "orbit",
|
||||||
}),
|
}),
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
}),
|
}),
|
||||||
@ -1833,8 +1844,11 @@ describe("history", () => {
|
|||||||
startBinding: null,
|
startBinding: null,
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([
|
||||||
gap: expect.toBeNonNaNNumber(),
|
expect.toBeNonNaNNumber(),
|
||||||
|
expect.toBeNonNaNNumber(),
|
||||||
|
]),
|
||||||
|
mode: "orbit",
|
||||||
}),
|
}),
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
}),
|
}),
|
||||||
@ -1868,13 +1882,19 @@ describe("history", () => {
|
|||||||
id: arrow.id,
|
id: arrow.id,
|
||||||
startBinding: expect.objectContaining({
|
startBinding: expect.objectContaining({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([
|
||||||
gap: expect.toBeNonNaNNumber(),
|
expect.toBeNonNaNNumber(),
|
||||||
|
expect.toBeNonNaNNumber(),
|
||||||
|
]),
|
||||||
|
mode: "orbit",
|
||||||
}),
|
}),
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([
|
||||||
gap: expect.toBeNonNaNNumber(),
|
expect.toBeNonNaNNumber(),
|
||||||
|
expect.toBeNonNaNNumber(),
|
||||||
|
]),
|
||||||
|
mode: "orbit",
|
||||||
}),
|
}),
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
}),
|
}),
|
||||||
@ -1941,13 +1961,19 @@ describe("history", () => {
|
|||||||
id: arrow.id,
|
id: arrow.id,
|
||||||
startBinding: expect.objectContaining({
|
startBinding: expect.objectContaining({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([
|
||||||
gap: expect.toBeNonNaNNumber(),
|
expect.toBeNonNaNNumber(),
|
||||||
|
expect.toBeNonNaNNumber(),
|
||||||
|
]),
|
||||||
|
mode: "orbit",
|
||||||
}),
|
}),
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([
|
||||||
gap: expect.toBeNonNaNNumber(),
|
expect.toBeNonNaNNumber(),
|
||||||
|
expect.toBeNonNaNNumber(),
|
||||||
|
]),
|
||||||
|
mode: "orbit",
|
||||||
}),
|
}),
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
}),
|
}),
|
||||||
@ -2298,15 +2324,13 @@ describe("history", () => {
|
|||||||
],
|
],
|
||||||
startBinding: {
|
startBinding: {
|
||||||
elementId: "KPrBI4g_v9qUB1XxYLgSz",
|
elementId: "KPrBI4g_v9qUB1XxYLgSz",
|
||||||
focus: -0.001587301587301948,
|
|
||||||
gap: 5,
|
|
||||||
fixedPoint: [1.0318471337579618, 0.49920634920634904],
|
fixedPoint: [1.0318471337579618, 0.49920634920634904],
|
||||||
|
mode: "orbit",
|
||||||
} as FixedPointBinding,
|
} as FixedPointBinding,
|
||||||
endBinding: {
|
endBinding: {
|
||||||
elementId: "u2JGnnmoJ0VATV4vCNJE5",
|
elementId: "u2JGnnmoJ0VATV4vCNJE5",
|
||||||
focus: -0.0016129032258049847,
|
|
||||||
gap: 3.537079145500037,
|
|
||||||
fixedPoint: [0.4991935483870975, -0.03875193720914723],
|
fixedPoint: [0.4991935483870975, -0.03875193720914723],
|
||||||
|
mode: "orbit",
|
||||||
} as FixedPointBinding,
|
} as FixedPointBinding,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -2421,10 +2445,9 @@ describe("history", () => {
|
|||||||
captureUpdate: CaptureUpdateAction.NEVER,
|
captureUpdate: CaptureUpdateAction.NEVER,
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.undo(); // undo `actionFinalize`
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
expect(API.getUndoStack().length).toBe(1);
|
expect(API.getUndoStack().length).toBe(1);
|
||||||
expect(API.getRedoStack().length).toBe(2);
|
expect(API.getRedoStack().length).toBe(1);
|
||||||
expect(h.elements).toEqual([
|
expect(h.elements).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
points: [
|
points: [
|
||||||
@ -2438,7 +2461,7 @@ describe("history", () => {
|
|||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
expect(API.getUndoStack().length).toBe(0);
|
expect(API.getUndoStack().length).toBe(0);
|
||||||
expect(API.getRedoStack().length).toBe(3);
|
expect(API.getRedoStack().length).toBe(2);
|
||||||
expect(h.elements).toEqual([
|
expect(h.elements).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
isDeleted: true,
|
isDeleted: true,
|
||||||
@ -2451,7 +2474,7 @@ describe("history", () => {
|
|||||||
|
|
||||||
Keyboard.redo();
|
Keyboard.redo();
|
||||||
expect(API.getUndoStack().length).toBe(1);
|
expect(API.getUndoStack().length).toBe(1);
|
||||||
expect(API.getRedoStack().length).toBe(2);
|
expect(API.getRedoStack().length).toBe(1);
|
||||||
expect(h.elements).toEqual([
|
expect(h.elements).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
@ -2464,21 +2487,6 @@ describe("history", () => {
|
|||||||
|
|
||||||
Keyboard.redo();
|
Keyboard.redo();
|
||||||
expect(API.getUndoStack().length).toBe(2);
|
expect(API.getUndoStack().length).toBe(2);
|
||||||
expect(API.getRedoStack().length).toBe(1);
|
|
||||||
expect(h.elements).toEqual([
|
|
||||||
expect.objectContaining({
|
|
||||||
points: [
|
|
||||||
[0, 0],
|
|
||||||
[5, 5],
|
|
||||||
[10, 10],
|
|
||||||
[15, 15],
|
|
||||||
[20, 20],
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
Keyboard.redo(); // redo `actionFinalize`
|
|
||||||
expect(API.getUndoStack().length).toBe(3);
|
|
||||||
expect(API.getRedoStack().length).toBe(0);
|
expect(API.getRedoStack().length).toBe(0);
|
||||||
expect(h.elements).toEqual([
|
expect(h.elements).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -2978,7 +2986,7 @@ describe("history", () => {
|
|||||||
// leave editor
|
// leave editor
|
||||||
Keyboard.keyPress(KEYS.ESCAPE);
|
Keyboard.keyPress(KEYS.ESCAPE);
|
||||||
|
|
||||||
expect(API.getUndoStack().length).toBe(4);
|
expect(API.getUndoStack().length).toBe(3);
|
||||||
expect(API.getRedoStack().length).toBe(0);
|
expect(API.getRedoStack().length).toBe(0);
|
||||||
expect(h.state.selectedLinearElement).not.toBeNull();
|
expect(h.state.selectedLinearElement).not.toBeNull();
|
||||||
expect(h.state.selectedLinearElement?.isEditing).toBe(false);
|
expect(h.state.selectedLinearElement?.isEditing).toBe(false);
|
||||||
@ -2995,11 +3003,11 @@ describe("history", () => {
|
|||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
expect(API.getUndoStack().length).toBe(0);
|
expect(API.getUndoStack().length).toBe(0);
|
||||||
expect(API.getRedoStack().length).toBe(4);
|
expect(API.getRedoStack().length).toBe(3);
|
||||||
expect(h.state.selectedLinearElement).toBeNull();
|
expect(h.state.selectedLinearElement).toBeNull();
|
||||||
|
|
||||||
Keyboard.redo();
|
Keyboard.redo();
|
||||||
expect(API.getUndoStack().length).toBe(4);
|
expect(API.getUndoStack().length).toBe(3);
|
||||||
expect(API.getRedoStack().length).toBe(0);
|
expect(API.getRedoStack().length).toBe(0);
|
||||||
expect(h.state.selectedLinearElement).toBeNull();
|
expect(h.state.selectedLinearElement).toBeNull();
|
||||||
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
|
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
|
||||||
@ -4500,16 +4508,30 @@ describe("history", () => {
|
|||||||
|
|
||||||
// create start binding
|
// create start binding
|
||||||
mouse.downAt(0, 0);
|
mouse.downAt(0, 0);
|
||||||
mouse.moveTo(0, 1);
|
mouse.moveTo(0, 10);
|
||||||
mouse.moveTo(0, 0);
|
mouse.moveTo(0, 10);
|
||||||
mouse.up();
|
mouse.up();
|
||||||
|
|
||||||
// create end binding
|
// create end binding
|
||||||
mouse.downAt(100, 0);
|
mouse.downAt(100, 0);
|
||||||
mouse.moveTo(100, 1);
|
mouse.moveTo(100, 10);
|
||||||
mouse.moveTo(100, 0);
|
mouse.moveTo(100, 10);
|
||||||
mouse.up();
|
mouse.up();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(h.elements[2] as ExcalidrawElbowArrowElement).startBinding
|
||||||
|
?.fixedPoint,
|
||||||
|
).not.toEqual([1, 0.5001]);
|
||||||
|
expect(
|
||||||
|
(h.elements[2] as ExcalidrawElbowArrowElement).startBinding?.mode,
|
||||||
|
).toBe("orbit");
|
||||||
|
expect(
|
||||||
|
(h.elements[2] as ExcalidrawElbowArrowElement).endBinding,
|
||||||
|
).not.toEqual([1, 0.5001]);
|
||||||
|
expect(
|
||||||
|
(h.elements[2] as ExcalidrawElbowArrowElement).endBinding?.mode,
|
||||||
|
).toBe("orbit");
|
||||||
|
|
||||||
expect(h.elements).toEqual(
|
expect(h.elements).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -4524,13 +4546,19 @@ describe("history", () => {
|
|||||||
id: arrowId,
|
id: arrowId,
|
||||||
startBinding: expect.objectContaining({
|
startBinding: expect.objectContaining({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([
|
||||||
gap: expect.toBeNonNaNNumber(),
|
expect.toBeNonNaNNumber(),
|
||||||
|
expect.toBeNonNaNNumber(),
|
||||||
|
]),
|
||||||
|
mode: "orbit",
|
||||||
}),
|
}),
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([
|
||||||
gap: expect.toBeNonNaNNumber(),
|
expect.toBeNonNaNNumber(),
|
||||||
|
expect.toBeNonNaNNumber(),
|
||||||
|
]),
|
||||||
|
mode: "orbit",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
@ -4543,12 +4571,16 @@ describe("history", () => {
|
|||||||
expect(h.elements).toEqual([
|
expect(h.elements).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: rect1.id,
|
id: rect1.id,
|
||||||
boundElements: [],
|
boundElements: [{ id: arrowId, type: "arrow" }],
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({ id: rect2.id, boundElements: [] }),
|
expect.objectContaining({ id: rect2.id, boundElements: [] }),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: arrowId,
|
id: arrowId,
|
||||||
startBinding: null,
|
startBinding: expect.objectContaining({
|
||||||
|
elementId: rect1.id,
|
||||||
|
fixedPoint: [1, 0.5001],
|
||||||
|
mode: "inside",
|
||||||
|
}),
|
||||||
endBinding: null,
|
endBinding: null,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
@ -4593,13 +4625,13 @@ describe("history", () => {
|
|||||||
id: arrowId,
|
id: arrowId,
|
||||||
startBinding: expect.objectContaining({
|
startBinding: expect.objectContaining({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: [1, 0.6],
|
||||||
gap: expect.toBeNonNaNNumber(),
|
mode: "orbit",
|
||||||
}),
|
}),
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: [0, 0.6],
|
||||||
gap: expect.toBeNonNaNNumber(),
|
mode: "orbit",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
@ -4612,12 +4644,21 @@ describe("history", () => {
|
|||||||
expect(h.elements).toEqual([
|
expect(h.elements).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: rect1.id,
|
id: rect1.id,
|
||||||
boundElements: [],
|
boundElements: [
|
||||||
|
expect.objectContaining({
|
||||||
|
id: arrowId,
|
||||||
|
type: "arrow",
|
||||||
|
}),
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({ id: rect2.id, boundElements: [] }),
|
expect.objectContaining({ id: rect2.id, boundElements: [] }),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: arrowId,
|
id: arrowId,
|
||||||
startBinding: null,
|
startBinding: expect.objectContaining({
|
||||||
|
elementId: rect1.id,
|
||||||
|
fixedPoint: [1, 0.5001],
|
||||||
|
mode: "inside",
|
||||||
|
}),
|
||||||
endBinding: null,
|
endBinding: null,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
@ -4636,13 +4677,13 @@ describe("history", () => {
|
|||||||
|
|
||||||
// create start binding
|
// create start binding
|
||||||
mouse.downAt(0, 0);
|
mouse.downAt(0, 0);
|
||||||
mouse.moveTo(0, 1);
|
mouse.moveTo(0, 10);
|
||||||
mouse.upAt(0, 0);
|
mouse.upAt(0, 10);
|
||||||
|
|
||||||
// create end binding
|
// create end binding
|
||||||
mouse.downAt(100, 0);
|
mouse.downAt(100, 0);
|
||||||
mouse.moveTo(100, 1);
|
mouse.moveTo(100, 10);
|
||||||
mouse.upAt(100, 0);
|
mouse.upAt(100, 10);
|
||||||
|
|
||||||
expect(h.elements).toEqual(
|
expect(h.elements).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
@ -4658,13 +4699,19 @@ describe("history", () => {
|
|||||||
id: arrowId,
|
id: arrowId,
|
||||||
startBinding: expect.objectContaining({
|
startBinding: expect.objectContaining({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([
|
||||||
gap: expect.toBeNonNaNNumber(),
|
expect.toBeNonNaNNumber(),
|
||||||
|
expect.toBeNonNaNNumber(),
|
||||||
|
]),
|
||||||
|
mode: "orbit",
|
||||||
}),
|
}),
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([
|
||||||
gap: expect.toBeNonNaNNumber(),
|
expect.toBeNonNaNNumber(),
|
||||||
|
expect.toBeNonNaNNumber(),
|
||||||
|
]),
|
||||||
|
mode: "orbit",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
@ -4677,12 +4724,21 @@ describe("history", () => {
|
|||||||
expect(h.elements).toEqual([
|
expect(h.elements).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: rect1.id,
|
id: rect1.id,
|
||||||
boundElements: [],
|
boundElements: [
|
||||||
|
expect.objectContaining({
|
||||||
|
id: arrowId,
|
||||||
|
type: "arrow",
|
||||||
|
}),
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({ id: rect2.id, boundElements: [] }),
|
expect.objectContaining({ id: rect2.id, boundElements: [] }),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: arrowId,
|
id: arrowId,
|
||||||
startBinding: null,
|
startBinding: expect.objectContaining({
|
||||||
|
elementId: rect1.id,
|
||||||
|
fixedPoint: [1, 0.5001],
|
||||||
|
mode: "inside",
|
||||||
|
}),
|
||||||
endBinding: null,
|
endBinding: null,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
@ -4702,9 +4758,8 @@ describe("history", () => {
|
|||||||
newElementWith(h.elements[2] as ExcalidrawElbowArrowElement, {
|
newElementWith(h.elements[2] as ExcalidrawElbowArrowElement, {
|
||||||
endBinding: {
|
endBinding: {
|
||||||
elementId: remoteContainer.id,
|
elementId: remoteContainer.id,
|
||||||
gap: 1,
|
|
||||||
focus: 0,
|
|
||||||
fixedPoint: [0.5, 1],
|
fixedPoint: [0.5, 1],
|
||||||
|
mode: "orbit",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
remoteContainer,
|
remoteContainer,
|
||||||
@ -4731,14 +4786,14 @@ describe("history", () => {
|
|||||||
id: arrowId,
|
id: arrowId,
|
||||||
startBinding: expect.objectContaining({
|
startBinding: expect.objectContaining({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: [1, 0.6],
|
||||||
gap: expect.toBeNonNaNNumber(),
|
mode: "orbit",
|
||||||
}),
|
}),
|
||||||
// rebound with previous rectangle
|
// rebound with previous rectangle
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: [0, 0.6],
|
||||||
gap: expect.toBeNonNaNNumber(),
|
mode: "orbit",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -4756,7 +4811,12 @@ describe("history", () => {
|
|||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: rect1.id,
|
id: rect1.id,
|
||||||
boundElements: [],
|
boundElements: [
|
||||||
|
expect.objectContaining({
|
||||||
|
id: arrowId,
|
||||||
|
type: "arrow",
|
||||||
|
}),
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: rect2.id,
|
id: rect2.id,
|
||||||
@ -4764,16 +4824,16 @@ describe("history", () => {
|
|||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: arrowId,
|
id: arrowId,
|
||||||
startBinding: null,
|
startBinding: expect.objectContaining({
|
||||||
|
elementId: rect1.id,
|
||||||
|
fixedPoint: [1, 0.5001],
|
||||||
|
mode: "inside",
|
||||||
|
}),
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
// now we are back in the previous state!
|
// now we are back in the previous state!
|
||||||
elementId: remoteContainer.id,
|
elementId: remoteContainer.id,
|
||||||
fixedPoint: [
|
fixedPoint: [0.5, 1],
|
||||||
expect.toBeNonNaNNumber(),
|
mode: "orbit",
|
||||||
expect.toBeNonNaNNumber(),
|
|
||||||
],
|
|
||||||
focus: expect.toBeNonNaNNumber(),
|
|
||||||
gap: expect.toBeNonNaNNumber(),
|
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -4791,15 +4851,13 @@ describe("history", () => {
|
|||||||
type: "arrow",
|
type: "arrow",
|
||||||
startBinding: {
|
startBinding: {
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
gap: 1,
|
|
||||||
focus: 0,
|
|
||||||
fixedPoint: [1, 0.5],
|
fixedPoint: [1, 0.5],
|
||||||
|
mode: "orbit",
|
||||||
},
|
},
|
||||||
endBinding: {
|
endBinding: {
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
gap: 1,
|
|
||||||
focus: 0,
|
|
||||||
fixedPoint: [0.5, 1],
|
fixedPoint: [0.5, 1],
|
||||||
|
mode: "orbit",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -4853,8 +4911,7 @@ describe("history", () => {
|
|||||||
expect.toBeNonNaNNumber(),
|
expect.toBeNonNaNNumber(),
|
||||||
expect.toBeNonNaNNumber(),
|
expect.toBeNonNaNNumber(),
|
||||||
],
|
],
|
||||||
focus: expect.toBeNonNaNNumber(),
|
mode: "orbit",
|
||||||
gap: expect.toBeNonNaNNumber(),
|
|
||||||
}),
|
}),
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
// now we are back in the previous state!
|
// now we are back in the previous state!
|
||||||
@ -4863,8 +4920,7 @@ describe("history", () => {
|
|||||||
expect.toBeNonNaNNumber(),
|
expect.toBeNonNaNNumber(),
|
||||||
expect.toBeNonNaNNumber(),
|
expect.toBeNonNaNNumber(),
|
||||||
],
|
],
|
||||||
focus: expect.toBeNonNaNNumber(),
|
mode: "orbit",
|
||||||
gap: expect.toBeNonNaNNumber(),
|
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -4900,15 +4956,13 @@ describe("history", () => {
|
|||||||
newElementWith(h.elements[0] as ExcalidrawElbowArrowElement, {
|
newElementWith(h.elements[0] as ExcalidrawElbowArrowElement, {
|
||||||
startBinding: {
|
startBinding: {
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
gap: 1,
|
|
||||||
focus: 0,
|
|
||||||
fixedPoint: [0.5, 1],
|
fixedPoint: [0.5, 1],
|
||||||
|
mode: "orbit",
|
||||||
},
|
},
|
||||||
endBinding: {
|
endBinding: {
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
gap: 1,
|
|
||||||
focus: 0,
|
|
||||||
fixedPoint: [1, 0.5],
|
fixedPoint: [1, 0.5],
|
||||||
|
mode: "orbit",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
newElementWith(rect1, {
|
newElementWith(rect1, {
|
||||||
@ -4935,8 +4989,7 @@ describe("history", () => {
|
|||||||
expect.toBeNonNaNNumber(),
|
expect.toBeNonNaNNumber(),
|
||||||
expect.toBeNonNaNNumber(),
|
expect.toBeNonNaNNumber(),
|
||||||
],
|
],
|
||||||
focus: expect.toBeNonNaNNumber(),
|
mode: "orbit",
|
||||||
gap: expect.toBeNonNaNNumber(),
|
|
||||||
}),
|
}),
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
@ -4944,8 +4997,7 @@ describe("history", () => {
|
|||||||
expect.toBeNonNaNNumber(),
|
expect.toBeNonNaNNumber(),
|
||||||
expect.toBeNonNaNNumber(),
|
expect.toBeNonNaNNumber(),
|
||||||
],
|
],
|
||||||
focus: expect.toBeNonNaNNumber(),
|
mode: "orbit",
|
||||||
gap: expect.toBeNonNaNNumber(),
|
|
||||||
}),
|
}),
|
||||||
isDeleted: true,
|
isDeleted: true,
|
||||||
}),
|
}),
|
||||||
@ -4975,8 +5027,7 @@ describe("history", () => {
|
|||||||
expect.toBeNonNaNNumber(),
|
expect.toBeNonNaNNumber(),
|
||||||
expect.toBeNonNaNNumber(),
|
expect.toBeNonNaNNumber(),
|
||||||
],
|
],
|
||||||
focus: expect.toBeNonNaNNumber(),
|
mode: "orbit",
|
||||||
gap: expect.toBeNonNaNNumber(),
|
|
||||||
},
|
},
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
@ -4984,8 +5035,7 @@ describe("history", () => {
|
|||||||
expect.toBeNonNaNNumber(),
|
expect.toBeNonNaNNumber(),
|
||||||
expect.toBeNonNaNNumber(),
|
expect.toBeNonNaNNumber(),
|
||||||
],
|
],
|
||||||
focus: expect.toBeNonNaNNumber(),
|
mode: "orbit",
|
||||||
gap: expect.toBeNonNaNNumber(),
|
|
||||||
}),
|
}),
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
}),
|
}),
|
||||||
@ -5028,13 +5078,11 @@ describe("history", () => {
|
|||||||
id: arrowId,
|
id: arrowId,
|
||||||
startBinding: expect.objectContaining({
|
startBinding: expect.objectContaining({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
focus: 0,
|
fixedPoint: expect.arrayContaining([1, 0.5001]),
|
||||||
gap: 1,
|
|
||||||
}),
|
}),
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
focus: -0,
|
fixedPoint: expect.arrayContaining([0, 0.5001]),
|
||||||
gap: 1,
|
|
||||||
}),
|
}),
|
||||||
isDeleted: true,
|
isDeleted: true,
|
||||||
}),
|
}),
|
||||||
@ -5076,13 +5124,19 @@ describe("history", () => {
|
|||||||
id: arrowId,
|
id: arrowId,
|
||||||
startBinding: expect.objectContaining({
|
startBinding: expect.objectContaining({
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([
|
||||||
gap: expect.toBeNonNaNNumber(),
|
expect.toBeNonNaNNumber(),
|
||||||
|
expect.toBeNonNaNNumber(),
|
||||||
|
]),
|
||||||
|
mode: "orbit",
|
||||||
}),
|
}),
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
elementId: rect2.id,
|
elementId: rect2.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
fixedPoint: expect.arrayContaining([
|
||||||
gap: expect.toBeNonNaNNumber(),
|
expect.toBeNonNaNNumber(),
|
||||||
|
expect.toBeNonNaNNumber(),
|
||||||
|
]),
|
||||||
|
mode: "orbit",
|
||||||
}),
|
}),
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -210,7 +210,6 @@ describe("Basic lasso selection tests", () => {
|
|||||||
[0, 0],
|
[0, 0],
|
||||||
[168.4765625, -153.38671875],
|
[168.4765625, -153.38671875],
|
||||||
],
|
],
|
||||||
lastCommittedPoint: null,
|
|
||||||
startBinding: null,
|
startBinding: null,
|
||||||
endBinding: null,
|
endBinding: null,
|
||||||
startArrowhead: null,
|
startArrowhead: null,
|
||||||
@ -250,7 +249,6 @@ describe("Basic lasso selection tests", () => {
|
|||||||
[0, 0],
|
[0, 0],
|
||||||
[206.12890625, 35.4140625],
|
[206.12890625, 35.4140625],
|
||||||
],
|
],
|
||||||
lastCommittedPoint: null,
|
|
||||||
startBinding: null,
|
startBinding: null,
|
||||||
endBinding: null,
|
endBinding: null,
|
||||||
startArrowhead: null,
|
startArrowhead: null,
|
||||||
@ -354,7 +352,6 @@ describe("Basic lasso selection tests", () => {
|
|||||||
],
|
],
|
||||||
pressures: [],
|
pressures: [],
|
||||||
simulatePressure: true,
|
simulatePressure: true,
|
||||||
lastCommittedPoint: null,
|
|
||||||
},
|
},
|
||||||
].map(
|
].map(
|
||||||
(e) =>
|
(e) =>
|
||||||
@ -1229,7 +1226,6 @@ describe("Special cases", () => {
|
|||||||
locked: false,
|
locked: false,
|
||||||
startBinding: null,
|
startBinding: null,
|
||||||
endBinding: null,
|
endBinding: null,
|
||||||
lastCommittedPoint: null,
|
|
||||||
startArrowhead: null,
|
startArrowhead: null,
|
||||||
endArrowhead: null,
|
endArrowhead: null,
|
||||||
points: [
|
points: [
|
||||||
@ -1271,7 +1267,6 @@ describe("Special cases", () => {
|
|||||||
locked: false,
|
locked: false,
|
||||||
startBinding: null,
|
startBinding: null,
|
||||||
endBinding: null,
|
endBinding: null,
|
||||||
lastCommittedPoint: null,
|
|
||||||
startArrowhead: null,
|
startArrowhead: null,
|
||||||
endArrowhead: null,
|
endArrowhead: null,
|
||||||
points: [
|
points: [
|
||||||
@ -1312,7 +1307,6 @@ describe("Special cases", () => {
|
|||||||
locked: false,
|
locked: false,
|
||||||
startBinding: null,
|
startBinding: null,
|
||||||
endBinding: null,
|
endBinding: null,
|
||||||
lastCommittedPoint: null,
|
|
||||||
startArrowhead: null,
|
startArrowhead: null,
|
||||||
endArrowhead: null,
|
endArrowhead: null,
|
||||||
points: [
|
points: [
|
||||||
@ -1353,7 +1347,6 @@ describe("Special cases", () => {
|
|||||||
locked: false,
|
locked: false,
|
||||||
startBinding: null,
|
startBinding: null,
|
||||||
endBinding: null,
|
endBinding: null,
|
||||||
lastCommittedPoint: null,
|
|
||||||
startArrowhead: null,
|
startArrowhead: null,
|
||||||
endArrowhead: null,
|
endArrowhead: null,
|
||||||
points: [
|
points: [
|
||||||
@ -1692,7 +1685,6 @@ describe("Special cases", () => {
|
|||||||
locked: false,
|
locked: false,
|
||||||
startBinding: null,
|
startBinding: null,
|
||||||
endBinding: null,
|
endBinding: null,
|
||||||
lastCommittedPoint: null,
|
|
||||||
startArrowhead: null,
|
startArrowhead: null,
|
||||||
endArrowhead: null,
|
endArrowhead: null,
|
||||||
points: [
|
points: [
|
||||||
@ -1744,7 +1736,6 @@ describe("Special cases", () => {
|
|||||||
locked: false,
|
locked: false,
|
||||||
startBinding: null,
|
startBinding: null,
|
||||||
endBinding: null,
|
endBinding: null,
|
||||||
lastCommittedPoint: null,
|
|
||||||
startArrowhead: null,
|
startArrowhead: null,
|
||||||
endArrowhead: null,
|
endArrowhead: null,
|
||||||
points: [
|
points: [
|
||||||
|
|||||||
@ -111,9 +111,8 @@ describe("library", () => {
|
|||||||
type: "arrow",
|
type: "arrow",
|
||||||
endBinding: {
|
endBinding: {
|
||||||
elementId: "rectangle1",
|
elementId: "rectangle1",
|
||||||
focus: -1,
|
|
||||||
gap: 0,
|
|
||||||
fixedPoint: [0.5, 1],
|
fixedPoint: [0.5, 1],
|
||||||
|
mode: "orbit",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,16 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
|
|
||||||
import { bindOrUnbindLinearElement } from "@excalidraw/element";
|
|
||||||
|
|
||||||
import { KEYS, reseed } from "@excalidraw/common";
|
import { KEYS, reseed } from "@excalidraw/common";
|
||||||
|
import { bindBindingElement } from "@excalidraw/element";
|
||||||
import "@excalidraw/utils/test-utils";
|
import "@excalidraw/utils/test-utils";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawArrowElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
ExcalidrawRectangleElement,
|
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { Excalidraw } from "../index";
|
import { Excalidraw } from "../index";
|
||||||
@ -83,12 +79,21 @@ describe("move element", () => {
|
|||||||
const rectA = UI.createElement("rectangle", { size: 100 });
|
const rectA = UI.createElement("rectangle", { size: 100 });
|
||||||
const rectB = UI.createElement("rectangle", { x: 200, y: 0, size: 300 });
|
const rectB = UI.createElement("rectangle", { x: 200, y: 0, size: 300 });
|
||||||
const arrow = UI.createElement("arrow", { x: 110, y: 50, size: 80 });
|
const arrow = UI.createElement("arrow", { x: 110, y: 50, size: 80 });
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
// bind line to two rectangles
|
// bind line to two rectangles
|
||||||
bindOrUnbindLinearElement(
|
bindBindingElement(
|
||||||
arrow.get() as NonDeleted<ExcalidrawLinearElement>,
|
arrow.get() as NonDeleted<ExcalidrawArrowElement>,
|
||||||
rectA.get() as ExcalidrawRectangleElement,
|
rectA.get(),
|
||||||
rectB.get() as ExcalidrawRectangleElement,
|
"orbit",
|
||||||
|
"start",
|
||||||
|
h.app.scene,
|
||||||
|
);
|
||||||
|
bindBindingElement(
|
||||||
|
arrow.get() as NonDeleted<ExcalidrawArrowElement>,
|
||||||
|
rectB.get(),
|
||||||
|
"orbit",
|
||||||
|
"end",
|
||||||
h.app.scene,
|
h.app.scene,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -97,16 +102,16 @@ describe("move element", () => {
|
|||||||
new Pointer("mouse").clickOn(rectB);
|
new Pointer("mouse").clickOn(rectB);
|
||||||
|
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`17`,
|
`15`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`13`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`14`);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(3);
|
expect(h.elements.length).toEqual(3);
|
||||||
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
||||||
expect([rectA.x, rectA.y]).toEqual([0, 0]);
|
expect([rectA.x, rectA.y]).toEqual([0, 0]);
|
||||||
expect([rectB.x, rectB.y]).toEqual([200, 0]);
|
expect([rectB.x, rectB.y]).toEqual([200, 0]);
|
||||||
expect([arrow.x, arrow.y]).toEqual([110, 50]);
|
expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[110, 50]], 0);
|
||||||
expect([arrow.width, arrow.height]).toEqual([80, 80]);
|
expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([[80, 80]], 0);
|
||||||
|
|
||||||
renderInteractiveScene.mockClear();
|
renderInteractiveScene.mockClear();
|
||||||
renderStaticScene.mockClear();
|
renderStaticScene.mockClear();
|
||||||
@ -124,8 +129,11 @@ describe("move element", () => {
|
|||||||
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
||||||
expect([rectA.x, rectA.y]).toEqual([0, 0]);
|
expect([rectA.x, rectA.y]).toEqual([0, 0]);
|
||||||
expect([rectB.x, rectB.y]).toEqual([201, 2]);
|
expect([rectB.x, rectB.y]).toEqual([201, 2]);
|
||||||
expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[110, 50]]);
|
expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[106, 46]], 0);
|
||||||
expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([[81, 81.4]]);
|
expect([[arrow.width, arrow.height]]).toCloselyEqualPoints(
|
||||||
|
[[89, 90.033]],
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
||||||
});
|
});
|
||||||
|
|||||||
@ -118,8 +118,10 @@ describe("multi point mode in linear elements", () => {
|
|||||||
key: KEYS.ENTER,
|
key: KEYS.ENTER,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
`11`,
|
||||||
|
);
|
||||||
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
|
|
||||||
const element = h.elements[0] as ExcalidrawLinearElement;
|
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||||
@ -161,8 +163,10 @@ describe("multi point mode in linear elements", () => {
|
|||||||
fireEvent.keyDown(document, {
|
fireEvent.keyDown(document, {
|
||||||
key: KEYS.ENTER,
|
key: KEYS.ENTER,
|
||||||
});
|
});
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
`11`,
|
||||||
|
);
|
||||||
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
|
|
||||||
const element = h.elements[0] as ExcalidrawLinearElement;
|
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||||
|
|||||||
@ -363,7 +363,6 @@ describe("regression tests", () => {
|
|||||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
Keyboard.keyPress(KEYS.Z);
|
Keyboard.keyPress(KEYS.Z);
|
||||||
Keyboard.keyPress(KEYS.Z);
|
Keyboard.keyPress(KEYS.Z);
|
||||||
Keyboard.keyPress(KEYS.Z);
|
|
||||||
});
|
});
|
||||||
expect(h.elements.filter((element) => !element.isDeleted).length).toBe(2);
|
expect(h.elements.filter((element) => !element.isDeleted).length).toBe(2);
|
||||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
|
|||||||
@ -24,7 +24,7 @@ test("unselected bound arrow updates when rotating its target element", async ()
|
|||||||
const arrow = UI.createElement("arrow", {
|
const arrow = UI.createElement("arrow", {
|
||||||
x: -80,
|
x: -80,
|
||||||
y: 50,
|
y: 50,
|
||||||
width: 70,
|
width: 85,
|
||||||
height: 0,
|
height: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -35,8 +35,8 @@ test("unselected bound arrow updates when rotating its target element", async ()
|
|||||||
expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
|
expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
|
||||||
expect(arrow.x).toBeCloseTo(-80);
|
expect(arrow.x).toBeCloseTo(-80);
|
||||||
expect(arrow.y).toBeCloseTo(50);
|
expect(arrow.y).toBeCloseTo(50);
|
||||||
expect(arrow.width).toBeCloseTo(110.7, 1);
|
expect(arrow.width).toBeCloseTo(84.9, 1);
|
||||||
expect(arrow.height).toBeCloseTo(0);
|
expect(arrow.height).toBeCloseTo(52.717, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("unselected bound arrows update when rotating their target elements", async () => {
|
test("unselected bound arrows update when rotating their target elements", async () => {
|
||||||
@ -48,9 +48,10 @@ test("unselected bound arrows update when rotating their target elements", async
|
|||||||
height: 120,
|
height: 120,
|
||||||
});
|
});
|
||||||
const ellipseArrow = UI.createElement("arrow", {
|
const ellipseArrow = UI.createElement("arrow", {
|
||||||
position: 0,
|
x: -10,
|
||||||
width: 40,
|
y: 80,
|
||||||
height: 80,
|
width: 50,
|
||||||
|
height: 60,
|
||||||
});
|
});
|
||||||
const text = UI.createElement("text", {
|
const text = UI.createElement("text", {
|
||||||
position: 220,
|
position: 220,
|
||||||
@ -59,8 +60,8 @@ test("unselected bound arrows update when rotating their target elements", async
|
|||||||
const textArrow = UI.createElement("arrow", {
|
const textArrow = UI.createElement("arrow", {
|
||||||
x: 360,
|
x: 360,
|
||||||
y: 300,
|
y: 300,
|
||||||
width: -100,
|
width: -140,
|
||||||
height: -40,
|
height: -60,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(ellipseArrow.endBinding?.elementId).toEqual(ellipse.id);
|
expect(ellipseArrow.endBinding?.elementId).toEqual(ellipse.id);
|
||||||
@ -69,16 +70,16 @@ test("unselected bound arrows update when rotating their target elements", async
|
|||||||
UI.rotate([ellipse, text], [-82, 23], { shift: true });
|
UI.rotate([ellipse, text], [-82, 23], { shift: true });
|
||||||
|
|
||||||
expect(ellipseArrow.endBinding?.elementId).toEqual(ellipse.id);
|
expect(ellipseArrow.endBinding?.elementId).toEqual(ellipse.id);
|
||||||
expect(ellipseArrow.x).toEqual(0);
|
expect(ellipseArrow.x).toEqual(-10);
|
||||||
expect(ellipseArrow.y).toEqual(0);
|
expect(ellipseArrow.y).toEqual(80);
|
||||||
expect(ellipseArrow.points[0]).toEqual([0, 0]);
|
expect(ellipseArrow.points[0]).toEqual([0, 0]);
|
||||||
expect(ellipseArrow.points[1][0]).toBeCloseTo(48.98, 1);
|
expect(ellipseArrow.points[1][0]).toBeCloseTo(42.318, 1);
|
||||||
expect(ellipseArrow.points[1][1]).toBeCloseTo(125.79, 1);
|
expect(ellipseArrow.points[1][1]).toBeCloseTo(92.133, 1);
|
||||||
|
|
||||||
expect(textArrow.endBinding?.elementId).toEqual(text.id);
|
expect(textArrow.endBinding?.elementId).toEqual(text.id);
|
||||||
expect(textArrow.x).toEqual(360);
|
expect(textArrow.x).toEqual(360);
|
||||||
expect(textArrow.y).toEqual(300);
|
expect(textArrow.y).toEqual(300);
|
||||||
expect(textArrow.points[0]).toEqual([0, 0]);
|
expect(textArrow.points[0]).toEqual([0, 0]);
|
||||||
expect(textArrow.points[1][0]).toBeCloseTo(-94, 0);
|
expect(textArrow.points[1][0]).toBeCloseTo(-98.86, 0);
|
||||||
expect(textArrow.points[1][1]).toBeCloseTo(-116.1, 0);
|
expect(textArrow.points[1][1]).toBeCloseTo(-123.65, 0);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -425,8 +425,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(8);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||||
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();
|
||||||
@ -469,8 +469,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(8);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||||
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();
|
||||||
@ -487,7 +487,12 @@ describe("tool locking & selection", () => {
|
|||||||
expect(h.state.activeTool.locked).toBe(true);
|
expect(h.state.activeTool.locked).toBe(true);
|
||||||
|
|
||||||
for (const { value } of Object.values(SHAPES)) {
|
for (const { value } of Object.values(SHAPES)) {
|
||||||
if (value !== "image" && value !== "selection" && value !== "eraser") {
|
if (
|
||||||
|
value !== "image" &&
|
||||||
|
value !== "selection" &&
|
||||||
|
value !== "eraser" &&
|
||||||
|
value !== "arrow"
|
||||||
|
) {
|
||||||
const element = UI.createElement(value);
|
const element = UI.createElement(value);
|
||||||
expect(h.state.selectedElementIds[element.id]).not.toBe(true);
|
expect(h.state.selectedElementIds[element.id]).not.toBe(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,8 +5,6 @@ import type {
|
|||||||
MIME_TYPES,
|
MIME_TYPES,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import type { SuggestedBinding } from "@excalidraw/element";
|
|
||||||
|
|
||||||
import type { LinearElementEditor } from "@excalidraw/element";
|
import type { LinearElementEditor } from "@excalidraw/element";
|
||||||
|
|
||||||
import type { MaybeTransformHandleType } from "@excalidraw/element";
|
import type { MaybeTransformHandleType } from "@excalidraw/element";
|
||||||
@ -33,6 +31,7 @@ import type {
|
|||||||
ExcalidrawIframeLikeElement,
|
ExcalidrawIframeLikeElement,
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
ExcalidrawNonSelectionElement,
|
ExcalidrawNonSelectionElement,
|
||||||
|
BindMode,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@ -204,6 +203,7 @@ export type StaticCanvasAppState = Readonly<
|
|||||||
frameRendering: AppState["frameRendering"];
|
frameRendering: AppState["frameRendering"];
|
||||||
currentHoveredFontFamily: AppState["currentHoveredFontFamily"];
|
currentHoveredFontFamily: AppState["currentHoveredFontFamily"];
|
||||||
hoveredElementIds: AppState["hoveredElementIds"];
|
hoveredElementIds: AppState["hoveredElementIds"];
|
||||||
|
suggestedBinding: AppState["suggestedBinding"];
|
||||||
// Cropping
|
// Cropping
|
||||||
croppingElementId: AppState["croppingElementId"];
|
croppingElementId: AppState["croppingElementId"];
|
||||||
}
|
}
|
||||||
@ -217,8 +217,9 @@ export type InteractiveCanvasAppState = Readonly<
|
|||||||
selectedGroupIds: AppState["selectedGroupIds"];
|
selectedGroupIds: AppState["selectedGroupIds"];
|
||||||
selectedLinearElement: AppState["selectedLinearElement"];
|
selectedLinearElement: AppState["selectedLinearElement"];
|
||||||
multiElement: AppState["multiElement"];
|
multiElement: AppState["multiElement"];
|
||||||
|
newElement: AppState["newElement"];
|
||||||
isBindingEnabled: AppState["isBindingEnabled"];
|
isBindingEnabled: AppState["isBindingEnabled"];
|
||||||
suggestedBindings: AppState["suggestedBindings"];
|
suggestedBinding: AppState["suggestedBinding"];
|
||||||
isRotating: AppState["isRotating"];
|
isRotating: AppState["isRotating"];
|
||||||
elementsToHighlight: AppState["elementsToHighlight"];
|
elementsToHighlight: AppState["elementsToHighlight"];
|
||||||
// Collaborators
|
// Collaborators
|
||||||
@ -233,6 +234,11 @@ export type InteractiveCanvasAppState = Readonly<
|
|||||||
// Search matches
|
// Search matches
|
||||||
searchMatches: AppState["searchMatches"];
|
searchMatches: AppState["searchMatches"];
|
||||||
activeLockedId: AppState["activeLockedId"];
|
activeLockedId: AppState["activeLockedId"];
|
||||||
|
// Non-used but needed in binding highlight arrow overdraw
|
||||||
|
hoveredElementIds: AppState["hoveredElementIds"];
|
||||||
|
frameRendering: AppState["frameRendering"];
|
||||||
|
shouldCacheIgnoreZoom: AppState["shouldCacheIgnoreZoom"];
|
||||||
|
exportScale: AppState["exportScale"];
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@ -292,7 +298,7 @@ export interface AppState {
|
|||||||
selectionElement: NonDeletedExcalidrawElement | null;
|
selectionElement: NonDeletedExcalidrawElement | null;
|
||||||
isBindingEnabled: boolean;
|
isBindingEnabled: boolean;
|
||||||
startBoundElement: NonDeleted<ExcalidrawBindableElement> | null;
|
startBoundElement: NonDeleted<ExcalidrawBindableElement> | null;
|
||||||
suggestedBindings: SuggestedBinding[];
|
suggestedBinding: NonDeleted<ExcalidrawBindableElement> | null;
|
||||||
frameToHighlight: NonDeleted<ExcalidrawFrameLikeElement> | null;
|
frameToHighlight: NonDeleted<ExcalidrawFrameLikeElement> | null;
|
||||||
frameRendering: {
|
frameRendering: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -446,6 +452,7 @@ export interface AppState {
|
|||||||
// as elements are unlocked, we remove the groupId from the elements
|
// as elements are unlocked, we remove the groupId from the elements
|
||||||
// and also remove groupId from this map
|
// and also remove groupId from this map
|
||||||
lockedMultiSelections: { [groupId: string]: true };
|
lockedMultiSelections: { [groupId: string]: true };
|
||||||
|
bindMode: BindMode;
|
||||||
|
|
||||||
/** properties sidebar mode - determines whether to show compact or complete sidebar */
|
/** properties sidebar mode - determines whether to show compact or complete sidebar */
|
||||||
stylesPanelMode: "compact" | "full";
|
stylesPanelMode: "compact" | "full";
|
||||||
@ -465,7 +472,7 @@ export type SearchMatch = {
|
|||||||
|
|
||||||
export type UIAppState = Omit<
|
export type UIAppState = Omit<
|
||||||
AppState,
|
AppState,
|
||||||
| "suggestedBindings"
|
| "suggestedBinding"
|
||||||
| "startBoundElement"
|
| "startBoundElement"
|
||||||
| "cursorButton"
|
| "cursorButton"
|
||||||
| "scrollX"
|
| "scrollX"
|
||||||
@ -740,6 +747,8 @@ export type AppClassProperties = {
|
|||||||
updateEditorAtom: App["updateEditorAtom"];
|
updateEditorAtom: App["updateEditorAtom"];
|
||||||
|
|
||||||
defaultSelectionTool: "selection" | "lasso";
|
defaultSelectionTool: "selection" | "lasso";
|
||||||
|
|
||||||
|
bindModeHandler: App["bindModeHandler"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PointerDownState = Readonly<{
|
export type PointerDownState = Readonly<{
|
||||||
|
|||||||
@ -21,20 +21,9 @@ export function curve<Point extends GlobalPoint | LocalPoint>(
|
|||||||
return [a, b, c, d] as Curve<Point>;
|
return [a, b, c, d] as Curve<Point>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function gradient(
|
function solveWithAnalyticalJacobian<Point extends GlobalPoint | LocalPoint>(
|
||||||
f: (t: number, s: number) => number,
|
curve: Curve<Point>,
|
||||||
t0: number,
|
lineSegment: LineSegment<Point>,
|
||||||
s0: number,
|
|
||||||
delta: number = 1e-6,
|
|
||||||
): number[] {
|
|
||||||
return [
|
|
||||||
(f(t0 + delta, s0) - f(t0 - delta, s0)) / (2 * delta),
|
|
||||||
(f(t0, s0 + delta) - f(t0, s0 - delta)) / (2 * delta),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function solve(
|
|
||||||
f: (t: number, s: number) => [number, number],
|
|
||||||
t0: number,
|
t0: number,
|
||||||
s0: number,
|
s0: number,
|
||||||
tolerance: number = 1e-3,
|
tolerance: number = 1e-3,
|
||||||
@ -48,33 +37,75 @@ function solve(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const y0 = f(t0, s0);
|
// Compute bezier point at parameter t0
|
||||||
const jacobian = [
|
const bt = 1 - t0;
|
||||||
gradient((t, s) => f(t, s)[0], t0, s0),
|
const bt2 = bt * bt;
|
||||||
gradient((t, s) => f(t, s)[1], t0, s0),
|
const bt3 = bt2 * bt;
|
||||||
];
|
const t0_2 = t0 * t0;
|
||||||
const b = [[-y0[0]], [-y0[1]]];
|
const t0_3 = t0_2 * t0;
|
||||||
const det =
|
|
||||||
jacobian[0][0] * jacobian[1][1] - jacobian[0][1] * jacobian[1][0];
|
|
||||||
|
|
||||||
if (det === 0) {
|
const bezierX =
|
||||||
|
bt3 * curve[0][0] +
|
||||||
|
3 * bt2 * t0 * curve[1][0] +
|
||||||
|
3 * bt * t0_2 * curve[2][0] +
|
||||||
|
t0_3 * curve[3][0];
|
||||||
|
const bezierY =
|
||||||
|
bt3 * curve[0][1] +
|
||||||
|
3 * bt2 * t0 * curve[1][1] +
|
||||||
|
3 * bt * t0_2 * curve[2][1] +
|
||||||
|
t0_3 * curve[3][1];
|
||||||
|
|
||||||
|
// Compute line point at parameter s0
|
||||||
|
const lineX =
|
||||||
|
lineSegment[0][0] + s0 * (lineSegment[1][0] - lineSegment[0][0]);
|
||||||
|
const lineY =
|
||||||
|
lineSegment[0][1] + s0 * (lineSegment[1][1] - lineSegment[0][1]);
|
||||||
|
|
||||||
|
// Function values
|
||||||
|
const fx = bezierX - lineX;
|
||||||
|
const fy = bezierY - lineY;
|
||||||
|
|
||||||
|
error = Math.abs(fx) + Math.abs(fy);
|
||||||
|
|
||||||
|
if (error < tolerance) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analytical derivatives
|
||||||
|
const dfx_dt =
|
||||||
|
-3 * bt2 * curve[0][0] +
|
||||||
|
3 * bt2 * curve[1][0] -
|
||||||
|
6 * bt * t0 * curve[1][0] -
|
||||||
|
3 * t0_2 * curve[2][0] +
|
||||||
|
6 * bt * t0 * curve[2][0] +
|
||||||
|
3 * t0_2 * curve[3][0];
|
||||||
|
|
||||||
|
const dfy_dt =
|
||||||
|
-3 * bt2 * curve[0][1] +
|
||||||
|
3 * bt2 * curve[1][1] -
|
||||||
|
6 * bt * t0 * curve[1][1] -
|
||||||
|
3 * t0_2 * curve[2][1] +
|
||||||
|
6 * bt * t0 * curve[2][1] +
|
||||||
|
3 * t0_2 * curve[3][1];
|
||||||
|
|
||||||
|
// Line derivatives
|
||||||
|
const dfx_ds = -(lineSegment[1][0] - lineSegment[0][0]);
|
||||||
|
const dfy_ds = -(lineSegment[1][1] - lineSegment[0][1]);
|
||||||
|
|
||||||
|
// Jacobian determinant
|
||||||
|
const det = dfx_dt * dfy_ds - dfx_ds * dfy_dt;
|
||||||
|
|
||||||
|
if (Math.abs(det) < 1e-12) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const iJ = [
|
// Newton step
|
||||||
[jacobian[1][1] / det, -jacobian[0][1] / det],
|
const invDet = 1 / det;
|
||||||
[-jacobian[1][0] / det, jacobian[0][0] / det],
|
const dt = invDet * (dfy_ds * -fx - dfx_ds * -fy);
|
||||||
];
|
const ds = invDet * (-dfy_dt * -fx + dfx_dt * -fy);
|
||||||
const h = [
|
|
||||||
[iJ[0][0] * b[0][0] + iJ[0][1] * b[1][0]],
|
|
||||||
[iJ[1][0] * b[0][0] + iJ[1][1] * b[1][0]],
|
|
||||||
];
|
|
||||||
|
|
||||||
t0 = t0 + h[0][0];
|
t0 += dt;
|
||||||
s0 = s0 + h[1][0];
|
s0 += ds;
|
||||||
|
|
||||||
const [tErr, sErr] = f(t0, s0);
|
|
||||||
error = Math.max(Math.abs(tErr), Math.abs(sErr));
|
|
||||||
iter += 1;
|
iter += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,38 +127,18 @@ export const bezierEquation = <Point extends GlobalPoint | LocalPoint>(
|
|||||||
t ** 3 * c[3][1],
|
t ** 3 * c[3][1],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Computes the intersection between a cubic spline and a line segment.
|
|
||||||
*/
|
|
||||||
export function curveIntersectLineSegment<
|
|
||||||
Point extends GlobalPoint | LocalPoint,
|
|
||||||
>(c: Curve<Point>, l: LineSegment<Point>): Point[] {
|
|
||||||
const line = (s: number) =>
|
|
||||||
pointFrom<Point>(
|
|
||||||
l[0][0] + s * (l[1][0] - l[0][0]),
|
|
||||||
l[0][1] + s * (l[1][1] - l[0][1]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const initial_guesses: [number, number][] = [
|
const initial_guesses: [number, number][] = [
|
||||||
[0.5, 0],
|
[0.5, 0],
|
||||||
[0.2, 0],
|
[0.2, 0],
|
||||||
[0.8, 0],
|
[0.8, 0],
|
||||||
];
|
];
|
||||||
|
|
||||||
const calculate = ([t0, s0]: [number, number]) => {
|
const calculate = <Point extends GlobalPoint | LocalPoint>(
|
||||||
const solution = solve(
|
[t0, s0]: [number, number],
|
||||||
(t: number, s: number) => {
|
l: LineSegment<Point>,
|
||||||
const bezier_point = bezierEquation(c, t);
|
c: Curve<Point>,
|
||||||
const line_point = line(s);
|
) => {
|
||||||
|
const solution = solveWithAnalyticalJacobian(c, l, t0, s0, 1e-2, 3);
|
||||||
return [
|
|
||||||
bezier_point[0] - line_point[0],
|
|
||||||
bezier_point[1] - line_point[1],
|
|
||||||
];
|
|
||||||
},
|
|
||||||
t0,
|
|
||||||
s0,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!solution) {
|
if (!solution) {
|
||||||
return null;
|
return null;
|
||||||
@ -142,17 +153,23 @@ export function curveIntersectLineSegment<
|
|||||||
return bezierEquation(c, t);
|
return bezierEquation(c, t);
|
||||||
};
|
};
|
||||||
|
|
||||||
let solution = calculate(initial_guesses[0]);
|
/**
|
||||||
|
* Computes the intersection between a cubic spline and a line segment.
|
||||||
|
*/
|
||||||
|
export function curveIntersectLineSegment<
|
||||||
|
Point extends GlobalPoint | LocalPoint,
|
||||||
|
>(c: Curve<Point>, l: LineSegment<Point>): Point[] {
|
||||||
|
let solution = calculate(initial_guesses[0], l, c);
|
||||||
if (solution) {
|
if (solution) {
|
||||||
return [solution];
|
return [solution];
|
||||||
}
|
}
|
||||||
|
|
||||||
solution = calculate(initial_guesses[1]);
|
solution = calculate(initial_guesses[1], l, c);
|
||||||
if (solution) {
|
if (solution) {
|
||||||
return [solution];
|
return [solution];
|
||||||
}
|
}
|
||||||
|
|
||||||
solution = calculate(initial_guesses[2]);
|
solution = calculate(initial_guesses[2], l, c);
|
||||||
if (solution) {
|
if (solution) {
|
||||||
return [solution];
|
return [solution];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,9 +46,11 @@ describe("Math curve", () => {
|
|||||||
pointFrom(10, 50),
|
pointFrom(10, 50),
|
||||||
pointFrom(50, 50),
|
pointFrom(50, 50),
|
||||||
);
|
);
|
||||||
const l = lineSegment(pointFrom(0, 112.5), pointFrom(90, 0));
|
const l = lineSegment(pointFrom(10, -60), pointFrom(10, 60));
|
||||||
|
|
||||||
expect(curveIntersectLineSegment(c, l)).toCloselyEqualPoints([[50, 50]]);
|
expect(curveIntersectLineSegment(c, l)).toCloselyEqualPoints([
|
||||||
|
[9.99, 5.05],
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can be detected where the determinant is overly precise", () => {
|
it("can be detected where the determinant is overly precise", () => {
|
||||||
|
|||||||
@ -6,11 +6,11 @@ expect.extend({
|
|||||||
throw new Error("expected and received are not point arrays");
|
throw new Error("expected and received are not point arrays");
|
||||||
}
|
}
|
||||||
|
|
||||||
const COMPARE = 1 / Math.pow(10, precision || 2);
|
const COMPARE = 1 / precision === 0 ? 1 : Math.pow(10, precision ?? 2);
|
||||||
const pass = expected.every(
|
const pass = expected.every(
|
||||||
(point, idx) =>
|
(point, idx) =>
|
||||||
Math.abs(received[idx]?.[0] - point[0]) < COMPARE &&
|
Math.abs(received[idx][0] - point[0]) < COMPARE &&
|
||||||
Math.abs(received[idx]?.[1] - point[1]) < COMPARE,
|
Math.abs(received[idx][1] - point[1]) < COMPARE,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!pass) {
|
if (!pass) {
|
||||||
|
|||||||
@ -11,6 +11,7 @@ exports[`exportToSvg > with default arguments 1`] = `
|
|||||||
"locked": false,
|
"locked": false,
|
||||||
"type": "selection",
|
"type": "selection",
|
||||||
},
|
},
|
||||||
|
"bindMode": "orbit",
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"croppingElementId": null,
|
"croppingElementId": null,
|
||||||
@ -101,7 +102,7 @@ exports[`exportToSvg > with default arguments 1`] = `
|
|||||||
"panels": 3,
|
"panels": 3,
|
||||||
},
|
},
|
||||||
"stylesPanelMode": "full",
|
"stylesPanelMode": "full",
|
||||||
"suggestedBindings": [],
|
"suggestedBinding": null,
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
"toast": null,
|
"toast": null,
|
||||||
"userToFollow": null,
|
"userToFollow": null,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user