mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: flip arrowheads if only arrow(s) selected (#8525)
Co-authored-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
parent
f3f0ab7c83
commit
8ca4cf3260
3 changed files with 222 additions and 68 deletions
|
@ -3,87 +3,209 @@ import { Excalidraw } from "../index";
|
||||||
import { render } from "../tests/test-utils";
|
import { render } from "../tests/test-utils";
|
||||||
import { API } from "../tests/helpers/api";
|
import { API } from "../tests/helpers/api";
|
||||||
import { point } from "../../math";
|
import { point } from "../../math";
|
||||||
import { actionFlipHorizontal } from "./actionFlip";
|
import { actionFlipHorizontal, actionFlipVertical } from "./actionFlip";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
const testElements = [
|
describe("flipping re-centers selection", () => {
|
||||||
API.createElement({
|
it("elbow arrow touches group selection side yet it remains in place after multiple moves", async () => {
|
||||||
type: "rectangle",
|
const elements = [
|
||||||
id: "rec1",
|
API.createElement({
|
||||||
x: 1046,
|
type: "rectangle",
|
||||||
y: 541,
|
id: "rec1",
|
||||||
width: 100,
|
x: 100,
|
||||||
height: 100,
|
y: 100,
|
||||||
boundElements: [
|
width: 100,
|
||||||
{
|
height: 100,
|
||||||
id: "arr",
|
boundElements: [{ id: "arr", type: "arrow" }],
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "rec2",
|
||||||
|
x: 220,
|
||||||
|
y: 250,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
boundElements: [{ id: "arr", type: "arrow" }],
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
type: "arrow",
|
type: "arrow",
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
API.createElement({
|
|
||||||
type: "rectangle",
|
|
||||||
id: "rec2",
|
|
||||||
x: 1169,
|
|
||||||
y: 777,
|
|
||||||
width: 102,
|
|
||||||
height: 115,
|
|
||||||
boundElements: [
|
|
||||||
{
|
|
||||||
id: "arr",
|
id: "arr",
|
||||||
type: "arrow",
|
x: 149.9,
|
||||||
},
|
y: 95,
|
||||||
],
|
width: 156,
|
||||||
}),
|
height: 239.9,
|
||||||
API.createElement({
|
startBinding: {
|
||||||
type: "arrow",
|
elementId: "rec1",
|
||||||
id: "arrow",
|
focus: 0,
|
||||||
x: 1103.0717787616313,
|
gap: 5,
|
||||||
y: 536.8531862198708,
|
fixedPoint: [0.49, -0.05],
|
||||||
width: 159.68539325842903,
|
},
|
||||||
height: 333.0396003698186,
|
endBinding: {
|
||||||
startBinding: {
|
elementId: "rec2",
|
||||||
elementId: "rec1",
|
focus: 0,
|
||||||
focus: 0.1366906474820229,
|
gap: 5,
|
||||||
gap: 5.000000000000057,
|
fixedPoint: [-0.05, 0.49],
|
||||||
fixedPoint: [0.5683453237410123, -0.05014327585315258],
|
},
|
||||||
},
|
startArrowhead: null,
|
||||||
endBinding: {
|
endArrowhead: "arrow",
|
||||||
elementId: "rec2",
|
points: [
|
||||||
focus: 0.0014925373134265828,
|
point(0, 0),
|
||||||
gap: 5,
|
point(0, -35),
|
||||||
fixedPoint: [-0.04862325174825108, 0.4992537313432874],
|
point(-90.9, -35),
|
||||||
},
|
point(-90.9, 204.9),
|
||||||
points: [
|
point(65.1, 204.9),
|
||||||
point(0, 0),
|
],
|
||||||
point(0, -35),
|
elbowed: true,
|
||||||
point(-97.80898876404626, -35),
|
}),
|
||||||
point(-97.80898876404626, 298.0396003698186),
|
];
|
||||||
point(61.87640449438277, 298.0396003698186),
|
await render(<Excalidraw initialData={{ elements }} />);
|
||||||
],
|
|
||||||
elbowed: true,
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
describe("flipping action", () => {
|
API.setSelectedElements(elements);
|
||||||
it("flip re-centers the selection even after multiple flip actions", async () => {
|
|
||||||
await render(<Excalidraw initialData={{ elements: testElements }} />);
|
|
||||||
|
|
||||||
API.setSelectedElements(testElements);
|
|
||||||
|
|
||||||
expect(Object.keys(h.state.selectedElementIds).length).toBe(3);
|
expect(Object.keys(h.state.selectedElementIds).length).toBe(3);
|
||||||
|
|
||||||
API.executeAction(actionFlipHorizontal);
|
API.executeAction(actionFlipHorizontal);
|
||||||
API.executeAction(actionFlipHorizontal);
|
API.executeAction(actionFlipHorizontal);
|
||||||
API.executeAction(actionFlipHorizontal);
|
API.executeAction(actionFlipHorizontal);
|
||||||
|
API.executeAction(actionFlipHorizontal);
|
||||||
|
|
||||||
const rec1 = h.elements.find((el) => el.id === "rec1");
|
const rec1 = h.elements.find((el) => el.id === "rec1");
|
||||||
expect(rec1?.x).toBeCloseTo(1113.78, 0);
|
expect(rec1?.x).toBeCloseTo(100);
|
||||||
expect(rec1?.y).toBeCloseTo(541, 0);
|
expect(rec1?.y).toBeCloseTo(100);
|
||||||
|
|
||||||
const rec2 = h.elements.find((el) => el.id === "rec2");
|
const rec2 = h.elements.find((el) => el.id === "rec2");
|
||||||
expect(rec2?.x).toBeCloseTo(988.72, 0);
|
expect(rec2?.x).toBeCloseTo(220);
|
||||||
expect(rec2?.y).toBeCloseTo(777, 0);
|
expect(rec2?.y).toBeCloseTo(250);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("flipping arrowheads", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await render(<Excalidraw />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flipping bound arrow should flip arrowheads only", () => {
|
||||||
|
const rect = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
boundElements: [{ type: "arrow", id: "arrow1" }],
|
||||||
|
});
|
||||||
|
const arrow = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
id: "arrow1",
|
||||||
|
startArrowhead: "arrow",
|
||||||
|
endArrowhead: null,
|
||||||
|
endBinding: {
|
||||||
|
elementId: rect.id,
|
||||||
|
focus: 0.5,
|
||||||
|
gap: 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rect, arrow]);
|
||||||
|
API.setSelectedElements([arrow]);
|
||||||
|
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe("arrow");
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe(null);
|
||||||
|
|
||||||
|
API.executeAction(actionFlipHorizontal);
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe(null);
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe("arrow");
|
||||||
|
|
||||||
|
API.executeAction(actionFlipHorizontal);
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe("arrow");
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe(null);
|
||||||
|
|
||||||
|
API.executeAction(actionFlipVertical);
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe(null);
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe("arrow");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flipping bound arrow should flip arrowheads only 2", () => {
|
||||||
|
const rect = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
boundElements: [{ type: "arrow", id: "arrow1" }],
|
||||||
|
});
|
||||||
|
const rect2 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
boundElements: [{ type: "arrow", id: "arrow1" }],
|
||||||
|
});
|
||||||
|
const arrow = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
id: "arrow1",
|
||||||
|
startArrowhead: "arrow",
|
||||||
|
endArrowhead: "circle",
|
||||||
|
startBinding: {
|
||||||
|
elementId: rect.id,
|
||||||
|
focus: 0.5,
|
||||||
|
gap: 5,
|
||||||
|
},
|
||||||
|
endBinding: {
|
||||||
|
elementId: rect2.id,
|
||||||
|
focus: 0.5,
|
||||||
|
gap: 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rect, rect2, arrow]);
|
||||||
|
API.setSelectedElements([arrow]);
|
||||||
|
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe("arrow");
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe("circle");
|
||||||
|
|
||||||
|
API.executeAction(actionFlipHorizontal);
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe("circle");
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe("arrow");
|
||||||
|
|
||||||
|
API.executeAction(actionFlipVertical);
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe("arrow");
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe("circle");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flipping unbound arrow shouldn't flip arrowheads", () => {
|
||||||
|
const arrow = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
id: "arrow1",
|
||||||
|
startArrowhead: "arrow",
|
||||||
|
endArrowhead: "circle",
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([arrow]);
|
||||||
|
API.setSelectedElements([arrow]);
|
||||||
|
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe("arrow");
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe("circle");
|
||||||
|
|
||||||
|
API.executeAction(actionFlipHorizontal);
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe("arrow");
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe("circle");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flipping bound arrow shouldn't flip arrowheads if selected alongside non-arrow eleemnt", () => {
|
||||||
|
const rect = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
boundElements: [{ type: "arrow", id: "arrow1" }],
|
||||||
|
});
|
||||||
|
const arrow = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
id: "arrow1",
|
||||||
|
startArrowhead: "arrow",
|
||||||
|
endArrowhead: null,
|
||||||
|
endBinding: {
|
||||||
|
elementId: rect.id,
|
||||||
|
focus: 0.5,
|
||||||
|
gap: 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rect, arrow]);
|
||||||
|
API.setSelectedElements([rect, arrow]);
|
||||||
|
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe("arrow");
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe(null);
|
||||||
|
|
||||||
|
API.executeAction(actionFlipHorizontal);
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe("arrow");
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { register } from "./register";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import type {
|
import type {
|
||||||
|
ExcalidrawArrowElement,
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
|
@ -19,9 +20,13 @@ import {
|
||||||
import { updateFrameMembershipOfSelectedElements } from "../frame";
|
import { updateFrameMembershipOfSelectedElements } from "../frame";
|
||||||
import { flipHorizontal, flipVertical } from "../components/icons";
|
import { flipHorizontal, flipVertical } from "../components/icons";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
import { isElbowArrow, isLinearElement } from "../element/typeChecks";
|
import {
|
||||||
|
isArrowElement,
|
||||||
|
isElbowArrow,
|
||||||
|
isLinearElement,
|
||||||
|
} from "../element/typeChecks";
|
||||||
import { mutateElbowArrow } from "../element/routing";
|
import { mutateElbowArrow } from "../element/routing";
|
||||||
import { mutateElement } from "../element/mutateElement";
|
import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||||
|
|
||||||
export const actionFlipHorizontal = register({
|
export const actionFlipHorizontal = register({
|
||||||
name: "flipHorizontal",
|
name: "flipHorizontal",
|
||||||
|
@ -112,6 +117,21 @@ const flipElements = (
|
||||||
flipDirection: "horizontal" | "vertical",
|
flipDirection: "horizontal" | "vertical",
|
||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
): ExcalidrawElement[] => {
|
): ExcalidrawElement[] => {
|
||||||
|
if (
|
||||||
|
selectedElements.every(
|
||||||
|
(element) =>
|
||||||
|
isArrowElement(element) && (element.startBinding || element.endBinding),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return selectedElements.map((element) => {
|
||||||
|
const _element = element as ExcalidrawArrowElement;
|
||||||
|
return newElementWith(_element, {
|
||||||
|
startArrowhead: _element.endArrowhead,
|
||||||
|
endArrowhead: _element.startArrowhead,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const { minX, minY, maxX, maxY, midX, midY } =
|
const { minX, minY, maxX, maxY, midX, midY } =
|
||||||
getCommonBoundingBox(selectedElements);
|
getCommonBoundingBox(selectedElements);
|
||||||
|
|
||||||
|
|
|
@ -129,6 +129,10 @@ export class API {
|
||||||
expect(API.getSelectedElements().length).toBe(0);
|
expect(API.getSelectedElements().length).toBe(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static getElement = <T extends ExcalidrawElement>(element: T): T => {
|
||||||
|
return h.app.scene.getElementsMapIncludingDeleted().get(element.id) as T || element;
|
||||||
|
}
|
||||||
|
|
||||||
static createElement = <
|
static createElement = <
|
||||||
T extends Exclude<ExcalidrawElementType, "selection"> = "rectangle",
|
T extends Exclude<ExcalidrawElementType, "selection"> = "rectangle",
|
||||||
>({
|
>({
|
||||||
|
@ -186,6 +190,12 @@ export class API {
|
||||||
endBinding?: T extends "arrow"
|
endBinding?: T extends "arrow"
|
||||||
? ExcalidrawArrowElement["endBinding"] | ExcalidrawElbowArrowElement["endBinding"]
|
? ExcalidrawArrowElement["endBinding"] | ExcalidrawElbowArrowElement["endBinding"]
|
||||||
: never;
|
: never;
|
||||||
|
startArrowhead?: T extends "arrow"
|
||||||
|
? ExcalidrawArrowElement["startArrowhead"] | ExcalidrawElbowArrowElement["startArrowhead"]
|
||||||
|
: never;
|
||||||
|
endArrowhead?: T extends "arrow"
|
||||||
|
? ExcalidrawArrowElement["endArrowhead"] | ExcalidrawElbowArrowElement["endArrowhead"]
|
||||||
|
: never;
|
||||||
elbowed?: boolean;
|
elbowed?: boolean;
|
||||||
}): T extends "arrow" | "line"
|
}): T extends "arrow" | "line"
|
||||||
? ExcalidrawLinearElement
|
? ExcalidrawLinearElement
|
||||||
|
@ -342,6 +352,8 @@ export class API {
|
||||||
if (element.type === "arrow") {
|
if (element.type === "arrow") {
|
||||||
element.startBinding = rest.startBinding ?? null;
|
element.startBinding = rest.startBinding ?? null;
|
||||||
element.endBinding = rest.endBinding ?? null;
|
element.endBinding = rest.endBinding ?? null;
|
||||||
|
element.startArrowhead = rest.startArrowhead ?? null;
|
||||||
|
element.endArrowhead = rest.endArrowhead ?? null;
|
||||||
}
|
}
|
||||||
if (id) {
|
if (id) {
|
||||||
element.id = id;
|
element.id = id;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue