mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
improve & granularize ExcalidrawElement types (#991)
* improve & granularize ExcalidrawElement types * fix incorrectly passing type * fix tests * fix more tests * fix unnecessary spreads & refactor * add comments
This commit is contained in:
parent
1c545c1d47
commit
373d16abe6
22 changed files with 430 additions and 272 deletions
|
@ -3,7 +3,7 @@ import { ExcalidrawElement } from "./types";
|
|||
|
||||
const _ce = ({ x, y, w, h }: { x: number; y: number; w: number; h: number }) =>
|
||||
({
|
||||
type: "test",
|
||||
type: "rectangle",
|
||||
strokeColor: "#000",
|
||||
backgroundColor: "#000",
|
||||
fillStyle: "solid",
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import { ExcalidrawElement } from "./types";
|
||||
import { ExcalidrawElement, ExcalidrawLinearElement } from "./types";
|
||||
import { rotate } from "../math";
|
||||
import { Drawable } from "roughjs/bin/core";
|
||||
import { Point } from "../types";
|
||||
import { getShapeForElement } from "../renderer/renderElement";
|
||||
import { isLinearElement } from "./typeChecks";
|
||||
|
||||
// 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.
|
||||
export function getElementAbsoluteCoords(element: ExcalidrawElement) {
|
||||
if (element.type === "arrow" || element.type === "line") {
|
||||
if (isLinearElement(element)) {
|
||||
return getLinearElementAbsoluteBounds(element);
|
||||
}
|
||||
return [
|
||||
|
@ -33,7 +34,9 @@ export function getDiamondPoints(element: ExcalidrawElement) {
|
|||
return [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY];
|
||||
}
|
||||
|
||||
export function getLinearElementAbsoluteBounds(element: ExcalidrawElement) {
|
||||
export function getLinearElementAbsoluteBounds(
|
||||
element: ExcalidrawLinearElement,
|
||||
) {
|
||||
if (element.points.length < 2 || !getShapeForElement(element)) {
|
||||
const { minX, minY, maxX, maxY } = element.points.reduce(
|
||||
(limits, [x, y]) => {
|
||||
|
@ -119,7 +122,10 @@ export function getLinearElementAbsoluteBounds(element: ExcalidrawElement) {
|
|||
];
|
||||
}
|
||||
|
||||
export function getArrowPoints(element: ExcalidrawElement, shape: Drawable[]) {
|
||||
export function getArrowPoints(
|
||||
element: ExcalidrawLinearElement,
|
||||
shape: Drawable[],
|
||||
) {
|
||||
const ops = shape[0].sets[0].ops;
|
||||
|
||||
const data = ops[ops.length - 1].data;
|
||||
|
|
|
@ -11,6 +11,7 @@ import { Point } from "../types";
|
|||
import { Drawable, OpSet } from "roughjs/bin/core";
|
||||
import { AppState } from "../types";
|
||||
import { getShapeForElement } from "../renderer/renderElement";
|
||||
import { isLinearElement } from "./typeChecks";
|
||||
|
||||
function isElementDraggableFromInside(
|
||||
element: ExcalidrawElement,
|
||||
|
@ -158,7 +159,7 @@ export function hitTest(
|
|||
distanceBetweenPointAndSegment(x, y, leftX, leftY, topX, topY) <
|
||||
lineThreshold
|
||||
);
|
||||
} else if (element.type === "arrow" || element.type === "line") {
|
||||
} else if (isLinearElement(element)) {
|
||||
if (!getShapeForElement(element)) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import { ExcalidrawElement } from "./types";
|
||||
import { isInvisiblySmallElement } from "./sizeHelpers";
|
||||
|
||||
export { newElement, newTextElement, duplicateElement } from "./newElement";
|
||||
export {
|
||||
newElement,
|
||||
newTextElement,
|
||||
newLinearElement,
|
||||
duplicateElement,
|
||||
} from "./newElement";
|
||||
export {
|
||||
getElementAbsoluteCoords,
|
||||
getCommonBounds,
|
||||
|
|
|
@ -13,33 +13,36 @@ type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
|||
// The version is used to compare updates when more than one user is working in
|
||||
// the same drawing. Note: this will trigger the component to update. Make sure you
|
||||
// are calling it either from a React event handler or within unstable_batchedUpdates().
|
||||
export function mutateElement<TElement extends ExcalidrawElement>(
|
||||
export function mutateElement<TElement extends Mutable<ExcalidrawElement>>(
|
||||
element: TElement,
|
||||
updates: ElementUpdate<TElement>,
|
||||
) {
|
||||
const mutableElement = element as any;
|
||||
// casting to any because can't use `in` operator
|
||||
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
||||
const { points } = updates as any;
|
||||
|
||||
if (typeof updates.points !== "undefined") {
|
||||
updates = { ...getSizeFromPoints(updates.points!), ...updates };
|
||||
if (typeof points !== "undefined") {
|
||||
updates = { ...getSizeFromPoints(points), ...updates };
|
||||
}
|
||||
|
||||
for (const key in updates) {
|
||||
const value = (updates as any)[key];
|
||||
if (typeof value !== "undefined") {
|
||||
mutableElement[key] = value;
|
||||
// @ts-ignore
|
||||
element[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
typeof updates.height !== "undefined" ||
|
||||
typeof updates.width !== "undefined" ||
|
||||
typeof updates.points !== "undefined"
|
||||
typeof points !== "undefined"
|
||||
) {
|
||||
invalidateShapeForElement(element);
|
||||
}
|
||||
|
||||
mutableElement.version++;
|
||||
mutableElement.versionNonce = randomSeed();
|
||||
element.version++;
|
||||
element.versionNonce = randomSeed();
|
||||
|
||||
globalSceneState.informMutation();
|
||||
}
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import { newElement, newTextElement, duplicateElement } from "./newElement";
|
||||
import {
|
||||
newTextElement,
|
||||
duplicateElement,
|
||||
newLinearElement,
|
||||
} from "./newElement";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
|
||||
function isPrimitive(val: any) {
|
||||
const type = typeof val;
|
||||
|
@ -17,25 +22,27 @@ function assertCloneObjects(source: any, clone: any) {
|
|||
}
|
||||
|
||||
it("clones arrow element", () => {
|
||||
const element = newElement(
|
||||
"arrow",
|
||||
0,
|
||||
0,
|
||||
"#000000",
|
||||
"transparent",
|
||||
"hachure",
|
||||
1,
|
||||
1,
|
||||
100,
|
||||
);
|
||||
const element = newLinearElement({
|
||||
type: "arrow",
|
||||
x: 0,
|
||||
y: 0,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "transparent",
|
||||
fillStyle: "hachure",
|
||||
strokeWidth: 1,
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
element.__proto__ = { hello: "world" };
|
||||
|
||||
element.points = [
|
||||
[1, 2],
|
||||
[3, 4],
|
||||
];
|
||||
mutateElement(element, {
|
||||
points: [
|
||||
[1, 2],
|
||||
[3, 4],
|
||||
],
|
||||
});
|
||||
|
||||
const copy = duplicateElement(element);
|
||||
|
||||
|
@ -59,17 +66,24 @@ it("clones arrow element", () => {
|
|||
});
|
||||
|
||||
it("clones text element", () => {
|
||||
const element = newTextElement(
|
||||
newElement("text", 0, 0, "#000000", "transparent", "hachure", 1, 1, 100),
|
||||
"hello",
|
||||
"Arial 20px",
|
||||
);
|
||||
const element = newTextElement({
|
||||
x: 0,
|
||||
y: 0,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "transparent",
|
||||
fillStyle: "hachure",
|
||||
strokeWidth: 1,
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
text: "hello",
|
||||
font: "Arial 20px",
|
||||
});
|
||||
|
||||
const copy = duplicateElement(element);
|
||||
|
||||
assertCloneObjects(element, copy);
|
||||
|
||||
expect(copy.points).not.toBe(element.points);
|
||||
expect(copy).not.toHaveProperty("points");
|
||||
expect(copy).not.toHaveProperty("shape");
|
||||
expect(copy.id).not.toBe(element.id);
|
||||
expect(typeof copy.id).toBe("string");
|
||||
|
|
|
@ -1,25 +1,45 @@
|
|||
import { randomSeed } from "roughjs/bin/math";
|
||||
import nanoid from "nanoid";
|
||||
import { Point } from "../types";
|
||||
|
||||
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawTextElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawGenericElement,
|
||||
} from "../element/types";
|
||||
import { measureText } from "../utils";
|
||||
|
||||
export function newElement(
|
||||
type: string,
|
||||
x: number,
|
||||
y: number,
|
||||
strokeColor: string,
|
||||
backgroundColor: string,
|
||||
fillStyle: string,
|
||||
strokeWidth: number,
|
||||
roughness: number,
|
||||
opacity: number,
|
||||
width = 0,
|
||||
height = 0,
|
||||
type ElementConstructorOpts = {
|
||||
x: ExcalidrawGenericElement["x"];
|
||||
y: ExcalidrawGenericElement["y"];
|
||||
strokeColor: ExcalidrawGenericElement["strokeColor"];
|
||||
backgroundColor: ExcalidrawGenericElement["backgroundColor"];
|
||||
fillStyle: ExcalidrawGenericElement["fillStyle"];
|
||||
strokeWidth: ExcalidrawGenericElement["strokeWidth"];
|
||||
roughness: ExcalidrawGenericElement["roughness"];
|
||||
opacity: ExcalidrawGenericElement["opacity"];
|
||||
width?: ExcalidrawGenericElement["width"];
|
||||
height?: ExcalidrawGenericElement["height"];
|
||||
};
|
||||
|
||||
function _newElementBase<T extends ExcalidrawElement>(
|
||||
type: T["type"],
|
||||
{
|
||||
x,
|
||||
y,
|
||||
strokeColor,
|
||||
backgroundColor,
|
||||
fillStyle,
|
||||
strokeWidth,
|
||||
roughness,
|
||||
opacity,
|
||||
width = 0,
|
||||
height = 0,
|
||||
...rest
|
||||
}: ElementConstructorOpts & Partial<ExcalidrawGenericElement>,
|
||||
) {
|
||||
const element = {
|
||||
id: nanoid(),
|
||||
return {
|
||||
id: rest.id || nanoid(),
|
||||
type,
|
||||
x,
|
||||
y,
|
||||
|
@ -31,29 +51,36 @@ export function newElement(
|
|||
strokeWidth,
|
||||
roughness,
|
||||
opacity,
|
||||
seed: randomSeed(),
|
||||
points: [] as readonly Point[],
|
||||
version: 1,
|
||||
versionNonce: 0,
|
||||
isDeleted: false,
|
||||
seed: rest.seed ?? randomSeed(),
|
||||
version: rest.version || 1,
|
||||
versionNonce: rest.versionNonce ?? 0,
|
||||
isDeleted: rest.isDeleted ?? false,
|
||||
};
|
||||
return element;
|
||||
}
|
||||
|
||||
export function newElement(
|
||||
opts: {
|
||||
type: ExcalidrawGenericElement["type"];
|
||||
} & ElementConstructorOpts,
|
||||
): ExcalidrawGenericElement {
|
||||
return _newElementBase<ExcalidrawGenericElement>(opts.type, opts);
|
||||
}
|
||||
|
||||
export function newTextElement(
|
||||
element: ExcalidrawElement,
|
||||
text: string,
|
||||
font: string,
|
||||
) {
|
||||
opts: {
|
||||
text: string;
|
||||
font: string;
|
||||
} & ElementConstructorOpts,
|
||||
): ExcalidrawTextElement {
|
||||
const { text, font } = opts;
|
||||
const metrics = measureText(text, font);
|
||||
const textElement: ExcalidrawTextElement = {
|
||||
...element,
|
||||
type: "text",
|
||||
const textElement = {
|
||||
..._newElementBase<ExcalidrawTextElement>("text", opts),
|
||||
text: text,
|
||||
font: font,
|
||||
// Center the text
|
||||
x: element.x - metrics.width / 2,
|
||||
y: element.y - metrics.height / 2,
|
||||
x: opts.x - metrics.width / 2,
|
||||
y: opts.y - metrics.height / 2,
|
||||
width: metrics.width,
|
||||
height: metrics.height,
|
||||
baseline: metrics.baseline,
|
||||
|
@ -62,6 +89,17 @@ export function newTextElement(
|
|||
return textElement;
|
||||
}
|
||||
|
||||
export function newLinearElement(
|
||||
opts: {
|
||||
type: "arrow" | "line";
|
||||
} & ElementConstructorOpts,
|
||||
): ExcalidrawLinearElement {
|
||||
return {
|
||||
..._newElementBase<ExcalidrawLinearElement>(opts.type, opts),
|
||||
points: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Simplified deep clone for the purpose of cloning ExcalidrawElement only
|
||||
// (doesn't clone Date, RegExp, Map, Set, Typed arrays etc.)
|
||||
//
|
||||
|
@ -100,11 +138,15 @@ function _duplicateElement(val: any, depth: number = 0) {
|
|||
return val;
|
||||
}
|
||||
|
||||
export function duplicateElement(
|
||||
element: ReturnType<typeof newElement>,
|
||||
): ReturnType<typeof newElement> {
|
||||
const copy = _duplicateElement(element);
|
||||
export function duplicateElement<TElement extends Mutable<ExcalidrawElement>>(
|
||||
element: TElement,
|
||||
overrides?: Partial<TElement>,
|
||||
): TElement {
|
||||
let copy: TElement = _duplicateElement(element);
|
||||
copy.id = nanoid();
|
||||
copy.seed = randomSeed();
|
||||
if (overrides) {
|
||||
copy = Object.assign(copy, overrides);
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { ExcalidrawElement, PointerType } from "./types";
|
|||
|
||||
import { handlerRectangles } from "./handlerRectangles";
|
||||
import { AppState } from "../types";
|
||||
import { isLinearElement } from "./typeChecks";
|
||||
|
||||
type HandlerRectanglesRet = keyof ReturnType<typeof handlerRectangles>;
|
||||
|
||||
|
@ -102,11 +103,7 @@ export function normalizeResizeHandle(
|
|||
element: ExcalidrawElement,
|
||||
resizeHandle: HandlerRectanglesRet,
|
||||
): HandlerRectanglesRet {
|
||||
if (
|
||||
(element.width >= 0 && element.height >= 0) ||
|
||||
element.type === "line" ||
|
||||
element.type === "arrow"
|
||||
) {
|
||||
if ((element.width >= 0 && element.height >= 0) || isLinearElement(element)) {
|
||||
return resizeHandle;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { ExcalidrawElement } from "./types";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { isLinearElement } from "./typeChecks";
|
||||
|
||||
export function isInvisiblySmallElement(element: ExcalidrawElement): boolean {
|
||||
if (element.type === "arrow" || element.type === "line") {
|
||||
if (isLinearElement(element)) {
|
||||
return element.points.length < 2;
|
||||
}
|
||||
return element.width === 0 && element.height === 0;
|
||||
|
@ -78,8 +79,7 @@ export function normalizeDimensions(
|
|||
if (
|
||||
!element ||
|
||||
(element.width >= 0 && element.height >= 0) ||
|
||||
element.type === "line" ||
|
||||
element.type === "arrow"
|
||||
isLinearElement(element)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
import { ExcalidrawElement, ExcalidrawTextElement } from "./types";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawTextElement,
|
||||
ExcalidrawLinearElement,
|
||||
} from "./types";
|
||||
|
||||
export function isTextElement(
|
||||
element: ExcalidrawElement,
|
||||
|
@ -6,6 +10,14 @@ export function isTextElement(
|
|||
return element.type === "text";
|
||||
}
|
||||
|
||||
export function isLinearElement(
|
||||
element?: ExcalidrawElement | null,
|
||||
): element is ExcalidrawLinearElement {
|
||||
return (
|
||||
element != null && (element.type === "arrow" || element.type === "line")
|
||||
);
|
||||
}
|
||||
|
||||
export function isExcalidrawElement(element: any): boolean {
|
||||
return (
|
||||
element?.type === "text" ||
|
||||
|
|
|
@ -1,20 +1,49 @@
|
|||
import { newElement } from "./newElement";
|
||||
import { Point } from "../types";
|
||||
|
||||
type _ExcalidrawElementBase = Readonly<{
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
strokeColor: string;
|
||||
backgroundColor: string;
|
||||
fillStyle: string;
|
||||
strokeWidth: number;
|
||||
roughness: number;
|
||||
opacity: number;
|
||||
width: number;
|
||||
height: number;
|
||||
seed: number;
|
||||
version: number;
|
||||
versionNonce: number;
|
||||
isDeleted: boolean;
|
||||
}>;
|
||||
|
||||
export type ExcalidrawGenericElement = _ExcalidrawElementBase & {
|
||||
type: "selection" | "rectangle" | "diamond" | "ellipse";
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 = Readonly<ReturnType<typeof newElement>>;
|
||||
export type ExcalidrawElement =
|
||||
| ExcalidrawGenericElement
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawLinearElement;
|
||||
|
||||
export type ExcalidrawTextElement = ExcalidrawElement &
|
||||
export type ExcalidrawTextElement = _ExcalidrawElementBase &
|
||||
Readonly<{
|
||||
type: "text";
|
||||
font: string;
|
||||
text: string;
|
||||
// for backward compatibility
|
||||
actualBoundingBoxAscent?: number;
|
||||
baseline: number;
|
||||
}>;
|
||||
|
||||
export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
||||
Readonly<{
|
||||
type: "arrow" | "line";
|
||||
points: Point[];
|
||||
}>;
|
||||
|
||||
export type PointerType = "mouse" | "pen" | "touch";
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue