feat: Call actionFinalize at the end of arrow creation and drag (#9453)
* First iter Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * Restore binding Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * More actionFinalize Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * Additional fixes Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * New elbow arrow is removed if too small Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * Remove very small arrows * Still allow loops * Restore tests Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * Update history snapshot * More history snapshot updates * keep invisible 2-point lines/freedraw elements --------- Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
parent
cc571c4681
commit
4dc205537c
@ -3,13 +3,21 @@ import {
|
|||||||
viewportCoordsToSceneCoords,
|
viewportCoordsToSceneCoords,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
|
import { pointsEqual } from "@excalidraw/math";
|
||||||
|
|
||||||
import type { AppState, Offsets, Zoom } from "@excalidraw/excalidraw/types";
|
import type { AppState, Offsets, Zoom } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import { getCommonBounds, getElementBounds } from "./bounds";
|
import { getCommonBounds, getElementBounds } from "./bounds";
|
||||||
import { isFreeDrawElement, isLinearElement } from "./typeChecks";
|
import {
|
||||||
|
isArrowElement,
|
||||||
|
isFreeDrawElement,
|
||||||
|
isLinearElement,
|
||||||
|
} from "./typeChecks";
|
||||||
|
|
||||||
import type { ElementsMap, ExcalidrawElement } from "./types";
|
import type { ElementsMap, ExcalidrawElement } from "./types";
|
||||||
|
|
||||||
|
export const INVISIBLY_SMALL_ELEMENT_SIZE = 0.1;
|
||||||
|
|
||||||
// TODO: remove invisible elements consistently actions, so that invisible elements are not recorded by the store, exported, broadcasted or persisted
|
// TODO: remove invisible elements consistently actions, so that invisible elements are not recorded by the store, exported, broadcasted or persisted
|
||||||
// - perhaps could be as part of a standalone 'cleanup' action, in addition to 'finalize'
|
// - perhaps could be as part of a standalone 'cleanup' action, in addition to 'finalize'
|
||||||
// - could also be part of `_clearElements`
|
// - could also be part of `_clearElements`
|
||||||
@ -17,8 +25,18 @@ export const isInvisiblySmallElement = (
|
|||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
||||||
return element.points.length < 2;
|
return (
|
||||||
|
element.points.length < 2 ||
|
||||||
|
(element.points.length === 2 &&
|
||||||
|
isArrowElement(element) &&
|
||||||
|
pointsEqual(
|
||||||
|
element.points[0],
|
||||||
|
element.points[element.points.length - 1],
|
||||||
|
INVISIBLY_SMALL_ELEMENT_SIZE,
|
||||||
|
))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return element.width === 0 && element.height === 0;
|
return element.width === 0 && element.height === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -292,7 +292,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`12`,
|
`12`,
|
||||||
);
|
);
|
||||||
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(`
|
||||||
@ -333,7 +333,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`9`,
|
`9`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||||
|
|
||||||
const midPointsWithRoundEdge = LinearElementEditor.getEditorMidPoints(
|
const midPointsWithRoundEdge = LinearElementEditor.getEditorMidPoints(
|
||||||
h.elements[0] as ExcalidrawLinearElement,
|
h.elements[0] as ExcalidrawLinearElement,
|
||||||
@ -394,7 +394,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`12`,
|
`12`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
||||||
|
|
||||||
expect([line.x, line.y]).toEqual([
|
expect([line.x, line.y]).toEqual([
|
||||||
points[0][0] + deltaX,
|
points[0][0] + deltaX,
|
||||||
@ -462,7 +462,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`16`,
|
`16`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
||||||
|
|
||||||
expect(line.points.length).toEqual(5);
|
expect(line.points.length).toEqual(5);
|
||||||
|
|
||||||
@ -513,7 +513,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`12`,
|
`12`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||||
|
|
||||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
||||||
line,
|
line,
|
||||||
@ -554,7 +554,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`12`,
|
`12`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||||
|
|
||||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
||||||
line,
|
line,
|
||||||
@ -602,7 +602,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`18`,
|
`18`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
||||||
|
|
||||||
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
||||||
line,
|
line,
|
||||||
@ -660,7 +660,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`16`,
|
`16`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
||||||
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)
|
||||||
@ -758,7 +758,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`12`,
|
`12`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||||
|
|
||||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
||||||
line,
|
line,
|
||||||
|
@ -3,8 +3,9 @@ import { pointFrom } from "@excalidraw/math";
|
|||||||
import {
|
import {
|
||||||
maybeBindLinearElement,
|
maybeBindLinearElement,
|
||||||
bindOrUnbindLinearElement,
|
bindOrUnbindLinearElement,
|
||||||
} from "@excalidraw/element";
|
isBindingEnabled,
|
||||||
import { LinearElementEditor } from "@excalidraw/element";
|
} from "@excalidraw/element/binding";
|
||||||
|
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||||
|
|
||||||
import { isBindingElement, isLinearElement } from "@excalidraw/element";
|
import { isBindingElement, isLinearElement } from "@excalidraw/element";
|
||||||
|
|
||||||
@ -15,6 +16,12 @@ import { isInvisiblySmallElement } from "@excalidraw/element";
|
|||||||
|
|
||||||
import { CaptureUpdateAction } from "@excalidraw/element";
|
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
|
NonDeleted,
|
||||||
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { resetCursor } from "../cursor";
|
import { resetCursor } from "../cursor";
|
||||||
import { done } from "../components/icons";
|
import { done } from "../components/icons";
|
||||||
@ -28,11 +35,50 @@ export const actionFinalize = register({
|
|||||||
name: "finalize",
|
name: "finalize",
|
||||||
label: "",
|
label: "",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, data, app) => {
|
||||||
const { interactiveCanvas, focusContainer, scene } = app;
|
const { interactiveCanvas, focusContainer, scene } = app;
|
||||||
|
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
|
|
||||||
|
if (data?.event && appState.selectedLinearElement) {
|
||||||
|
const linearElementEditor = LinearElementEditor.handlePointerUp(
|
||||||
|
data.event,
|
||||||
|
appState.selectedLinearElement,
|
||||||
|
appState,
|
||||||
|
app.scene,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { startBindingElement, endBindingElement } = linearElementEditor;
|
||||||
|
const element = app.scene.getElement(linearElementEditor.elementId);
|
||||||
|
if (isBindingElement(element)) {
|
||||||
|
bindOrUnbindLinearElement(
|
||||||
|
element,
|
||||||
|
startBindingElement,
|
||||||
|
endBindingElement,
|
||||||
|
app.scene,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (linearElementEditor !== appState.selectedLinearElement) {
|
||||||
|
let newElements = elements;
|
||||||
|
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
|
||||||
|
newElements = newElements.filter((el) => el.id !== element!.id);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
elements: newElements,
|
||||||
|
appState: {
|
||||||
|
selectedLinearElement: {
|
||||||
|
...linearElementEditor,
|
||||||
|
selectedPointsIndices: null,
|
||||||
|
},
|
||||||
|
suggestedBindings: [],
|
||||||
|
},
|
||||||
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (appState.editingLinearElement) {
|
if (appState.editingLinearElement) {
|
||||||
const { elementId, startBindingElement, endBindingElement } =
|
const { elementId, startBindingElement, endBindingElement } =
|
||||||
appState.editingLinearElement;
|
appState.editingLinearElement;
|
||||||
@ -80,48 +126,56 @@ export const actionFinalize = register({
|
|||||||
focusContainer();
|
focusContainer();
|
||||||
}
|
}
|
||||||
|
|
||||||
const multiPointElement = appState.multiElement
|
let element: NonDeleted<ExcalidrawElement> | null = null;
|
||||||
? appState.multiElement
|
if (appState.multiElement) {
|
||||||
: appState.newElement?.type === "freedraw"
|
element = appState.multiElement;
|
||||||
? appState.newElement
|
} else if (
|
||||||
: null;
|
appState.newElement?.type === "freedraw" ||
|
||||||
|
isBindingElement(appState.newElement)
|
||||||
|
) {
|
||||||
|
element = appState.newElement;
|
||||||
|
} else if (Object.keys(appState.selectedElementIds).length === 1) {
|
||||||
|
const candidate = elementsMap.get(
|
||||||
|
Object.keys(appState.selectedElementIds)[0],
|
||||||
|
) as NonDeleted<ExcalidrawLinearElement> | undefined;
|
||||||
|
if (candidate) {
|
||||||
|
element = candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (multiPointElement) {
|
if (element) {
|
||||||
// pen and mouse have hover
|
// pen and mouse have hover
|
||||||
if (
|
if (
|
||||||
multiPointElement.type !== "freedraw" &&
|
appState.multiElement &&
|
||||||
|
element.type !== "freedraw" &&
|
||||||
appState.lastPointerDownWith !== "touch"
|
appState.lastPointerDownWith !== "touch"
|
||||||
) {
|
) {
|
||||||
const { points, lastCommittedPoint } = multiPointElement;
|
const { points, lastCommittedPoint } = element;
|
||||||
if (
|
if (
|
||||||
!lastCommittedPoint ||
|
!lastCommittedPoint ||
|
||||||
points[points.length - 1] !== lastCommittedPoint
|
points[points.length - 1] !== lastCommittedPoint
|
||||||
) {
|
) {
|
||||||
scene.mutateElement(multiPointElement, {
|
scene.mutateElement(element, {
|
||||||
points: multiPointElement.points.slice(0, -1),
|
points: element.points.slice(0, -1),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInvisiblySmallElement(multiPointElement)) {
|
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.filter(
|
newElements = newElements.filter((el) => el.id !== element!.id);
|
||||||
(el) => el.id !== multiPointElement.id,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isLinearElement(element) || element.type === "freedraw") {
|
||||||
// If the multi point line closes the loop,
|
// If the multi point line closes the loop,
|
||||||
// set the last point to first point.
|
// set the last point to first point.
|
||||||
// This ensures that loop remains closed at different scales.
|
// This ensures that loop remains closed at different scales.
|
||||||
const isLoop = isPathALoop(multiPointElement.points, appState.zoom.value);
|
const isLoop = isPathALoop(element.points, appState.zoom.value);
|
||||||
if (
|
if (element.type === "line" || element.type === "freedraw") {
|
||||||
multiPointElement.type === "line" ||
|
|
||||||
multiPointElement.type === "freedraw"
|
|
||||||
) {
|
|
||||||
if (isLoop) {
|
if (isLoop) {
|
||||||
const linePoints = multiPointElement.points;
|
const linePoints = element.points;
|
||||||
const firstPoint = linePoints[0];
|
const firstPoint = linePoints[0];
|
||||||
scene.mutateElement(multiPointElement, {
|
scene.mutateElement(element, {
|
||||||
points: linePoints.map((p, index) =>
|
points: linePoints.map((p, index) =>
|
||||||
index === linePoints.length - 1
|
index === linePoints.length - 1
|
||||||
? pointFrom(firstPoint[0], firstPoint[1])
|
? pointFrom(firstPoint[0], firstPoint[1])
|
||||||
@ -132,23 +186,25 @@ export const actionFinalize = register({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isBindingElement(multiPointElement) &&
|
isBindingElement(element) &&
|
||||||
!isLoop &&
|
!isLoop &&
|
||||||
multiPointElement.points.length > 1
|
element.points.length > 1 &&
|
||||||
|
isBindingEnabled(appState)
|
||||||
) {
|
) {
|
||||||
const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||||
multiPointElement,
|
element,
|
||||||
-1,
|
-1,
|
||||||
arrayToMap(elements),
|
arrayToMap(elements),
|
||||||
);
|
);
|
||||||
maybeBindLinearElement(multiPointElement, appState, { x, y }, scene);
|
maybeBindLinearElement(element, appState, { x, y }, scene);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(!appState.activeTool.locked &&
|
(!appState.activeTool.locked &&
|
||||||
appState.activeTool.type !== "freedraw") ||
|
appState.activeTool.type !== "freedraw") ||
|
||||||
!multiPointElement
|
!element
|
||||||
) {
|
) {
|
||||||
resetCursor(interactiveCanvas);
|
resetCursor(interactiveCanvas);
|
||||||
}
|
}
|
||||||
@ -175,7 +231,7 @@ export const actionFinalize = register({
|
|||||||
activeTool:
|
activeTool:
|
||||||
(appState.activeTool.locked ||
|
(appState.activeTool.locked ||
|
||||||
appState.activeTool.type === "freedraw") &&
|
appState.activeTool.type === "freedraw") &&
|
||||||
multiPointElement
|
element
|
||||||
? appState.activeTool
|
? appState.activeTool
|
||||||
: activeTool,
|
: activeTool,
|
||||||
activeEmbeddable: null,
|
activeEmbeddable: null,
|
||||||
@ -186,21 +242,18 @@ export const actionFinalize = register({
|
|||||||
startBoundElement: null,
|
startBoundElement: null,
|
||||||
suggestedBindings: [],
|
suggestedBindings: [],
|
||||||
selectedElementIds:
|
selectedElementIds:
|
||||||
multiPointElement &&
|
element &&
|
||||||
!appState.activeTool.locked &&
|
!appState.activeTool.locked &&
|
||||||
appState.activeTool.type !== "freedraw"
|
appState.activeTool.type !== "freedraw"
|
||||||
? {
|
? {
|
||||||
...appState.selectedElementIds,
|
...appState.selectedElementIds,
|
||||||
[multiPointElement.id]: true,
|
[element.id]: true,
|
||||||
}
|
}
|
||||||
: appState.selectedElementIds,
|
: appState.selectedElementIds,
|
||||||
// To select the linear element when user has finished mutipoint editing
|
// To select the linear element when user has finished mutipoint editing
|
||||||
selectedLinearElement:
|
selectedLinearElement:
|
||||||
multiPointElement && isLinearElement(multiPointElement)
|
element && isLinearElement(element)
|
||||||
? new LinearElementEditor(
|
? new LinearElementEditor(element, arrayToMap(newElements))
|
||||||
multiPointElement,
|
|
||||||
arrayToMap(newElements),
|
|
||||||
)
|
|
||||||
: appState.selectedLinearElement,
|
: appState.selectedLinearElement,
|
||||||
pendingImageElementId: null,
|
pendingImageElementId: null,
|
||||||
},
|
},
|
||||||
|
@ -107,13 +107,11 @@ import {
|
|||||||
import { getCommonBounds, getElementAbsoluteCoords } from "@excalidraw/element";
|
import { getCommonBounds, getElementAbsoluteCoords } from "@excalidraw/element";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
bindOrUnbindLinearElement,
|
|
||||||
bindOrUnbindLinearElements,
|
bindOrUnbindLinearElements,
|
||||||
fixBindingsAfterDeletion,
|
fixBindingsAfterDeletion,
|
||||||
getHoveredElementForBinding,
|
getHoveredElementForBinding,
|
||||||
isBindingEnabled,
|
isBindingEnabled,
|
||||||
isLinearElementSimpleAndAlreadyBound,
|
isLinearElementSimpleAndAlreadyBound,
|
||||||
maybeBindLinearElement,
|
|
||||||
shouldEnableBindingForPointerEvent,
|
shouldEnableBindingForPointerEvent,
|
||||||
updateBoundElements,
|
updateBoundElements,
|
||||||
getSuggestedBindingsForArrows,
|
getSuggestedBindingsForArrows,
|
||||||
@ -2797,7 +2795,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.updateEmbeddables();
|
this.updateEmbeddables();
|
||||||
const elements = this.scene.getElementsIncludingDeleted();
|
const elements = this.scene.getElementsIncludingDeleted();
|
||||||
const elementsMap = this.scene.getElementsMapIncludingDeleted();
|
const elementsMap = this.scene.getElementsMapIncludingDeleted();
|
||||||
const nonDeletedElementsMap = this.scene.getNonDeletedElementsMap();
|
|
||||||
|
|
||||||
if (!this.state.showWelcomeScreen && !elements.length) {
|
if (!this.state.showWelcomeScreen && !elements.length) {
|
||||||
this.setState({ showWelcomeScreen: true });
|
this.setState({ showWelcomeScreen: true });
|
||||||
@ -2944,27 +2941,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState({ selectedLinearElement: null });
|
this.setState({ selectedLinearElement: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { multiElement } = prevState;
|
|
||||||
if (
|
|
||||||
prevState.activeTool !== this.state.activeTool &&
|
|
||||||
multiElement != null &&
|
|
||||||
isBindingEnabled(this.state) &&
|
|
||||||
isBindingElement(multiElement, false)
|
|
||||||
) {
|
|
||||||
maybeBindLinearElement(
|
|
||||||
multiElement,
|
|
||||||
this.state,
|
|
||||||
tupleToCoors(
|
|
||||||
LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
|
||||||
multiElement,
|
|
||||||
-1,
|
|
||||||
nonDeletedElementsMap,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
this.scene,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.store.commit(elementsMap, this.state);
|
this.store.commit(elementsMap, this.state);
|
||||||
|
|
||||||
// Do not notify consumers if we're still loading the scene. Among other
|
// Do not notify consumers if we're still loading the scene. Among other
|
||||||
@ -9143,36 +9119,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState({ selectedLinearElement: null });
|
this.setState({ selectedLinearElement: null });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const linearElementEditor = LinearElementEditor.handlePointerUp(
|
this.actionManager.executeAction(actionFinalize, "ui", {
|
||||||
childEvent,
|
event: childEvent,
|
||||||
this.state.selectedLinearElement,
|
|
||||||
this.state,
|
|
||||||
this.scene,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { startBindingElement, endBindingElement } =
|
|
||||||
linearElementEditor;
|
|
||||||
const element = this.scene.getElement(linearElementEditor.elementId);
|
|
||||||
if (isBindingElement(element)) {
|
|
||||||
bindOrUnbindLinearElement(
|
|
||||||
element,
|
|
||||||
startBindingElement,
|
|
||||||
endBindingElement,
|
|
||||||
this.scene,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (linearElementEditor !== this.state.selectedLinearElement) {
|
|
||||||
this.setState({
|
|
||||||
selectedLinearElement: {
|
|
||||||
...linearElementEditor,
|
|
||||||
selectedPointsIndices: null,
|
|
||||||
},
|
|
||||||
suggestedBindings: [],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
this.missingPointerEventCleanupEmitter.clear();
|
this.missingPointerEventCleanupEmitter.clear();
|
||||||
|
|
||||||
@ -9294,12 +9245,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
isBindingEnabled(this.state) &&
|
isBindingEnabled(this.state) &&
|
||||||
isBindingElement(newElement, false)
|
isBindingElement(newElement, false)
|
||||||
) {
|
) {
|
||||||
maybeBindLinearElement(
|
this.actionManager.executeAction(actionFinalize);
|
||||||
newElement,
|
|
||||||
this.state,
|
|
||||||
pointerCoords,
|
|
||||||
this.scene,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
this.setState({ suggestedBindings: [], startBoundElement: null });
|
this.setState({ suggestedBindings: [], startBoundElement: null });
|
||||||
if (!activeTool.locked) {
|
if (!activeTool.locked) {
|
||||||
|
@ -173,7 +173,7 @@ exports[`move element > rectangles with binding arrow 6`] = `
|
|||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 7,
|
"version": 7,
|
||||||
"versionNonce": 745419401,
|
"versionNonce": 1051383431,
|
||||||
"width": 300,
|
"width": 300,
|
||||||
"x": 201,
|
"x": 201,
|
||||||
"y": 2,
|
"y": 2,
|
||||||
@ -231,7 +231,7 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 11,
|
"version": 11,
|
||||||
"versionNonce": 1051383431,
|
"versionNonce": 1996028265,
|
||||||
"width": "86.85786",
|
"width": "86.85786",
|
||||||
"x": "107.07107",
|
"x": "107.07107",
|
||||||
"y": "47.07107",
|
"y": "47.07107",
|
||||||
|
@ -6,7 +6,10 @@ import { DEFAULT_SIDEBAR, FONT_FAMILY, ROUNDNESS } from "@excalidraw/common";
|
|||||||
import { newElementWith } from "@excalidraw/element";
|
import { newElementWith } from "@excalidraw/element";
|
||||||
import * as sizeHelpers from "@excalidraw/element";
|
import * as sizeHelpers from "@excalidraw/element";
|
||||||
|
|
||||||
|
import type { LocalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
ExcalidrawArrowElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawFreeDrawElement,
|
ExcalidrawFreeDrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
@ -163,6 +166,109 @@ describe("restoreElements", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should remove imperceptibly small elements", () => {
|
||||||
|
const arrowElement = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
points: [
|
||||||
|
[0, 0],
|
||||||
|
[0.02, 0.05],
|
||||||
|
] as LocalPoint[],
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const restoredElements = restore.restoreElements([arrowElement], null);
|
||||||
|
|
||||||
|
const restoredArrow = restoredElements[0] as
|
||||||
|
| ExcalidrawArrowElement
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
expect(restoredArrow).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should keep 'imperceptibly' small freedraw/line elements", () => {
|
||||||
|
const freedrawElement = API.createElement({
|
||||||
|
type: "freedraw",
|
||||||
|
points: [
|
||||||
|
[0, 0],
|
||||||
|
[0.0001, 0.0001],
|
||||||
|
] as LocalPoint[],
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
const lineElement = API.createElement({
|
||||||
|
type: "line",
|
||||||
|
points: [
|
||||||
|
[0, 0],
|
||||||
|
[0.0001, 0.0001],
|
||||||
|
] as LocalPoint[],
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const restoredElements = restore.restoreElements(
|
||||||
|
[freedrawElement, lineElement],
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(restoredElements).toEqual([
|
||||||
|
expect.objectContaining({ id: freedrawElement.id }),
|
||||||
|
expect.objectContaining({ id: lineElement.id }),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should restore loop linears correctly", () => {
|
||||||
|
const linearElement = API.createElement({
|
||||||
|
type: "line",
|
||||||
|
points: [
|
||||||
|
[0, 0],
|
||||||
|
[100, 100],
|
||||||
|
[100, 200],
|
||||||
|
[0, 0],
|
||||||
|
] as LocalPoint[],
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
const arrowElement = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
points: [
|
||||||
|
[0, 0],
|
||||||
|
[100, 100],
|
||||||
|
[100, 200],
|
||||||
|
[0, 0],
|
||||||
|
] as LocalPoint[],
|
||||||
|
x: 500,
|
||||||
|
y: 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
const restoredElements = restore.restoreElements(
|
||||||
|
[linearElement, arrowElement],
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const restoredLinear = restoredElements[0] as
|
||||||
|
| ExcalidrawLinearElement
|
||||||
|
| undefined;
|
||||||
|
const restoredArrow = restoredElements[1] as
|
||||||
|
| ExcalidrawArrowElement
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
expect(restoredLinear?.type).toBe("line");
|
||||||
|
expect(restoredLinear?.points).toEqual([
|
||||||
|
[0, 0],
|
||||||
|
[100, 100],
|
||||||
|
[100, 200],
|
||||||
|
[0, 0],
|
||||||
|
] as LocalPoint[]);
|
||||||
|
expect(restoredArrow?.type).toBe("arrow");
|
||||||
|
expect(restoredArrow?.points).toEqual([
|
||||||
|
[0, 0],
|
||||||
|
[100, 100],
|
||||||
|
[100, 200],
|
||||||
|
[0, 0],
|
||||||
|
] as LocalPoint[]);
|
||||||
|
});
|
||||||
|
|
||||||
it('should set arrow element endArrowHead as "arrow" when arrow element endArrowHead is null', () => {
|
it('should set arrow element endArrowHead as "arrow" when arrow element endArrowHead is null', () => {
|
||||||
const arrowElement = API.createElement({ type: "arrow" });
|
const arrowElement = API.createElement({ type: "arrow" });
|
||||||
const restoredElements = restore.restoreElements([arrowElement], null);
|
const restoredElements = restore.restoreElements([arrowElement], null);
|
||||||
|
@ -426,7 +426,7 @@ describe("select single element on the scene", () => {
|
|||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(8);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(8);
|
||||||
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();
|
||||||
@ -470,7 +470,7 @@ describe("select single element on the scene", () => {
|
|||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(8);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(8);
|
||||||
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();
|
||||||
|
@ -91,9 +91,10 @@ export function isPoint(p: unknown): p is LocalPoint | GlobalPoint {
|
|||||||
export function pointsEqual<Point extends GlobalPoint | LocalPoint>(
|
export function pointsEqual<Point extends GlobalPoint | LocalPoint>(
|
||||||
a: Point,
|
a: Point,
|
||||||
b: Point,
|
b: Point,
|
||||||
|
tolerance: number = PRECISION,
|
||||||
): boolean {
|
): boolean {
|
||||||
const abs = Math.abs;
|
const abs = Math.abs;
|
||||||
return abs(a[0] - b[0]) < PRECISION && abs(a[1] - b[1]) < PRECISION;
|
return abs(a[0] - b[0]) < tolerance && abs(a[1] - b[1]) < tolerance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
x
Reference in New Issue
Block a user