Refactor ExcalidrawElement (#874)

* Get rid of isSelected, canvas, canvasZoom, canvasOffsetX and canvasOffsetY on ExcalidrawElement.

* Fix most unit tests. Fix cmd a. Fix alt drag

* Focus on paste

* shift select should include previously selected items

* Fix last test

* Move this.shape out of ExcalidrawElement and into a WeakMap
This commit is contained in:
Pete Hunt 2020-03-08 10:20:55 -07:00 committed by GitHub
parent 8ecb4201db
commit ccbbdb75a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 416 additions and 306 deletions

View file

@ -2,6 +2,7 @@ import { ExcalidrawElement } from "./types";
import { rotate } from "../math";
import { Drawable } from "roughjs/bin/core";
import { Point } from "roughjs/bin/geometry";
import { getShapeForElement } from "../renderer/renderElement";
// If the element is created from right to left, the width is going to be negative
// This set of functions retrieves the absolute position of the 4 points.
@ -33,7 +34,7 @@ export function getDiamondPoints(element: ExcalidrawElement) {
}
export function getLinearElementAbsoluteBounds(element: ExcalidrawElement) {
if (element.points.length < 2 || !element.shape) {
if (element.points.length < 2 || !getShapeForElement(element)) {
const { minX, minY, maxX, maxY } = element.points.reduce(
(limits, [x, y]) => {
limits.minY = Math.min(limits.minY, y);
@ -54,7 +55,7 @@ export function getLinearElementAbsoluteBounds(element: ExcalidrawElement) {
];
}
const shape = element.shape as Drawable[];
const shape = getShapeForElement(element) as Drawable[];
// first element is always the curve
const ops = shape[0].sets[0].ops;
@ -118,8 +119,7 @@ export function getLinearElementAbsoluteBounds(element: ExcalidrawElement) {
];
}
export function getArrowPoints(element: ExcalidrawElement) {
const shape = element.shape as Drawable[];
export function getArrowPoints(element: ExcalidrawElement, shape: Drawable[]) {
const ops = shape[0].sets[0].ops;
const data = ops[ops.length - 1].data;

View file

@ -9,13 +9,22 @@ import {
} from "./bounds";
import { Point } from "roughjs/bin/geometry";
import { Drawable, OpSet } from "roughjs/bin/core";
import { AppState } from "../types";
import { getShapeForElement } from "../renderer/renderElement";
function isElementDraggableFromInside(element: ExcalidrawElement): boolean {
return element.backgroundColor !== "transparent" || element.isSelected;
function isElementDraggableFromInside(
element: ExcalidrawElement,
appState: AppState,
): boolean {
return (
element.backgroundColor !== "transparent" ||
appState.selectedElementIds[element.id]
);
}
export function hitTest(
element: ExcalidrawElement,
appState: AppState,
x: number,
y: number,
zoom: number,
@ -58,7 +67,7 @@ export function hitTest(
ty /= t;
});
if (isElementDraggableFromInside(element)) {
if (isElementDraggableFromInside(element, appState)) {
return (
a * tx - (px - lineThreshold) >= 0 && b * ty - (py - lineThreshold) >= 0
);
@ -67,7 +76,7 @@ export function hitTest(
} else if (element.type === "rectangle") {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
if (isElementDraggableFromInside(element)) {
if (isElementDraggableFromInside(element, appState)) {
return (
x > x1 - lineThreshold &&
x < x2 + lineThreshold &&
@ -99,7 +108,7 @@ export function hitTest(
leftY,
] = getDiamondPoints(element);
if (isElementDraggableFromInside(element)) {
if (isElementDraggableFromInside(element, appState)) {
// TODO: remove this when we normalize coordinates globally
if (topY > bottomY) {
[bottomY, topY] = [topY, bottomY];
@ -150,10 +159,10 @@ export function hitTest(
lineThreshold
);
} else if (element.type === "arrow" || element.type === "line") {
if (!element.shape) {
if (!getShapeForElement(element)) {
return false;
}
const shape = element.shape as Drawable[];
const shape = getShapeForElement(element) as Drawable[];
const [x1, y1, x2, y2] = getLinearElementAbsoluteBounds(element);
if (x < x1 || y < y1 - 10 || x > x2 || y > y2 + 10) {

View file

@ -54,8 +54,6 @@ it("clones arrow element", () => {
...element,
id: copy.id,
seed: copy.seed,
shape: undefined,
canvas: undefined,
});
});

View file

@ -1,6 +1,5 @@
import { randomSeed } from "roughjs/bin/math";
import nanoid from "nanoid";
import { Drawable } from "roughjs/bin/core";
import { Point } from "roughjs/bin/geometry";
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
@ -32,14 +31,8 @@ export function newElement(
strokeWidth,
roughness,
opacity,
isSelected: false,
seed: randomSeed(),
shape: null as Drawable | Drawable[] | null,
points: [] as Point[],
canvas: null as HTMLCanvasElement | null,
canvasZoom: 1, // The zoom level used to render the cached canvas
canvasOffsetX: 0,
canvasOffsetY: 0,
};
return element;
}
@ -52,7 +45,6 @@ export function newTextElement(
const metrics = measureText(text, font);
const textElement: ExcalidrawTextElement = {
...element,
shape: null,
type: "text",
text: text,
font: font,

View file

@ -1,17 +1,19 @@
import { ExcalidrawElement, PointerType } from "./types";
import { handlerRectangles } from "./handlerRectangles";
import { AppState } from "../types";
type HandlerRectanglesRet = keyof ReturnType<typeof handlerRectangles>;
export function resizeTest(
element: ExcalidrawElement,
appState: AppState,
x: number,
y: number,
zoom: number,
pointerType: PointerType,
): HandlerRectanglesRet | false {
if (!element.isSelected || element.type === "text") {
if (!appState.selectedElementIds[element.id] || element.type === "text") {
return false;
}
@ -40,6 +42,7 @@ export function resizeTest(
export function getElementWithResizeHandler(
elements: readonly ExcalidrawElement[],
appState: AppState,
{ x, y }: { x: number; y: number },
zoom: number,
pointerType: PointerType,
@ -48,7 +51,7 @@ export function getElementWithResizeHandler(
if (result) {
return result;
}
const resizeHandle = resizeTest(element, x, y, zoom, pointerType);
const resizeHandle = resizeTest(element, appState, x, y, zoom, pointerType);
return resizeHandle ? { element, resizeHandle } : null;
}, null as { element: ExcalidrawElement; resizeHandle: ReturnType<typeof resizeTest> } | null);
}

View file

@ -8,6 +8,6 @@ export const showSelectedShapeActions = (
) =>
Boolean(
appState.editingElement ||
getSelectedElements(elements).length ||
getSelectedElements(elements, appState).length ||
appState.elementType !== "selection",
);

View file

@ -1,4 +1,5 @@
import { ExcalidrawElement } from "./types";
import { invalidateShapeForElement } from "../renderer/renderElement";
export function isInvisiblySmallElement(element: ExcalidrawElement): boolean {
if (element.type === "arrow" || element.type === "line") {
@ -86,7 +87,7 @@ export function normalizeDimensions(
element.y -= element.height;
}
element.shape = null;
invalidateShapeForElement(element);
return true;
}

View file

@ -1,5 +1,10 @@
import { newElement } from "./newElement";
/**
* ExcalidrawElement should be JSON serializable and (eventually) contain
* no computed data. The list of all ExcalidrawElements should be shareable
* between peers and contain no state local to the peer.
*/
export type ExcalidrawElement = ReturnType<typeof newElement>;
export type ExcalidrawTextElement = ExcalidrawElement & {
type: "text";