fix: binding text to non-bindable containers and not always preferring selection (#4655)

This commit is contained in:
David Luzar 2022-03-02 17:04:09 +01:00 committed by GitHub
parent 8e26d5b500
commit 6d0716eb6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 336 additions and 85 deletions

View File

@ -127,6 +127,7 @@ import {
isInitializedImageElement, isInitializedImageElement,
isLinearElement, isLinearElement,
isLinearElementType, isLinearElementType,
isTextBindableContainer,
} from "../element/typeChecks"; } from "../element/typeChecks";
import { import {
ExcalidrawBindableElement, ExcalidrawBindableElement,
@ -140,6 +141,7 @@ import {
ExcalidrawImageElement, ExcalidrawImageElement,
FileId, FileId,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
ExcalidrawTextContainer,
} from "../element/types"; } from "../element/types";
import { getCenter, getDistance } from "../gesture"; import { getCenter, getDistance } from "../gesture";
import { import {
@ -167,7 +169,7 @@ import { renderScene } from "../renderer";
import { invalidateShapeForElement } from "../renderer/renderElement"; import { invalidateShapeForElement } from "../renderer/renderElement";
import { import {
calculateScrollCenter, calculateScrollCenter,
getElementContainingPosition, getTextBindableContainerAtPosition,
getElementsAtPosition, getElementsAtPosition,
getElementsWithinSelection, getElementsWithinSelection,
getNormalizedZoom, getNormalizedZoom,
@ -238,7 +240,7 @@ import {
bindTextToShapeAfterDuplication, bindTextToShapeAfterDuplication,
getApproxMinLineHeight, getApproxMinLineHeight,
getApproxMinLineWidth, getApproxMinLineWidth,
getBoundTextElementId, getBoundTextElement,
} from "../element/textElement"; } from "../element/textElement";
import { isHittingElementNotConsideringBoundingBox } from "../element/collision"; import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
import { import {
@ -2157,28 +2159,40 @@ class App extends React.Component<AppProps, AppState> {
window.devicePixelRatio, window.devicePixelRatio,
); );
let existingTextElement: NonDeleted<ExcalidrawTextElement> | null = null;
let container: ExcalidrawTextContainer | null = null;
const selectedElements = getSelectedElements(
this.scene.getElements(),
this.state,
);
if (selectedElements.length === 1) {
if (isTextElement(selectedElements[0])) {
existingTextElement = selectedElements[0];
} else if (isTextBindableContainer(selectedElements[0])) {
container = selectedElements[0];
existingTextElement = getBoundTextElement(container);
}
}
existingTextElement =
existingTextElement ?? this.getTextElementAtPosition(sceneX, sceneY);
// bind to container when shouldBind is true or // bind to container when shouldBind is true or
// clicked on center of container // clicked on center of container
const container = if (
shouldBind || parentCenterPosition !container &&
? getElementContainingPosition( !existingTextElement &&
(shouldBind || parentCenterPosition)
) {
container = getTextBindableContainerAtPosition(
this.scene.getElements().filter((ele) => !isTextElement(ele)), this.scene.getElements().filter((ele) => !isTextElement(ele)),
sceneX, sceneX,
sceneY, sceneY,
) );
: null;
let existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
// consider bounded text element if container present
if (container) {
const boundTextElementId = getBoundTextElementId(container);
if (boundTextElementId) {
existingTextElement = this.scene.getElement(
boundTextElementId,
) as ExcalidrawTextElement;
}
} }
if (!existingTextElement && container) { if (!existingTextElement && container) {
const fontString = { const fontString = {
fontSize: this.state.currentItemFontSize, fontSize: this.state.currentItemFontSize,
@ -5432,7 +5446,7 @@ class App extends React.Component<AppProps, AppState> {
canvas: HTMLCanvasElement | null, canvas: HTMLCanvasElement | null,
scale: number, scale: number,
) { ) {
const elementClickedInside = getElementContainingPosition( const elementClickedInside = getTextBindableContainerAtPosition(
this.scene this.scene
.getElementsIncludingDeleted() .getElementsIncludingDeleted()
.filter((element) => !isTextElement(element)), .filter((element) => !isTextElement(element)),

View File

@ -3,10 +3,9 @@ import { updateBoundElements } from "./binding";
import { getCommonBounds } from "./bounds"; import { getCommonBounds } from "./bounds";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { getPerfectElementSize } from "./sizeHelpers"; import { getPerfectElementSize } from "./sizeHelpers";
import Scene from "../scene/Scene";
import { NonDeletedExcalidrawElement } from "./types"; import { NonDeletedExcalidrawElement } from "./types";
import { AppState, PointerDownState } from "../types"; import { AppState, PointerDownState } from "../types";
import { getBoundTextElementId } from "./textElement"; import { getBoundTextElement } from "./textElement";
import { isSelectedViaGroup } from "../groups"; import { isSelectedViaGroup } from "../groups";
export const dragSelectedElements = ( export const dragSelectedElements = (
@ -39,16 +38,14 @@ export const dragSelectedElements = (
// container is part of a group, but we're dragging the container directly // container is part of a group, but we're dragging the container directly
(appState.editingGroupId && !isSelectedViaGroup(appState, element)) (appState.editingGroupId && !isSelectedViaGroup(appState, element))
) { ) {
const boundTextElementId = getBoundTextElementId(element); const textElement = getBoundTextElement(element);
if (boundTextElementId) { if (textElement) {
const textElement =
Scene.getScene(element)!.getElement(boundTextElementId);
updateElementCoords( updateElementCoords(
lockDirection, lockDirection,
distanceX, distanceX,
distanceY, distanceY,
pointerDownState, pointerDownState,
textElement!, textElement,
offset, offset,
); );
} }

View File

@ -22,7 +22,6 @@ import { getElementAbsoluteCoords } from ".";
import { adjustXYWithRotation } from "../math"; import { adjustXYWithRotation } from "../math";
import { getResizedElementAbsoluteCoords } from "./bounds"; import { getResizedElementAbsoluteCoords } from "./bounds";
import { getContainerElement, measureText, wrapText } from "./textElement"; import { getContainerElement, measureText, wrapText } from "./textElement";
import { isBoundToContainer } from "./typeChecks";
import { BOUND_TEXT_PADDING, VERTICAL_ALIGN } from "../constants"; import { BOUND_TEXT_PADDING, VERTICAL_ALIGN } from "../constants";
type ElementConstructorOpts = MarkOptional< type ElementConstructorOpts = MarkOptional<
@ -221,8 +220,7 @@ const getAdjustedDimensions = (
// make sure container dimensions are set properly when // make sure container dimensions are set properly when
// text editor overflows beyond viewport dimensions // text editor overflows beyond viewport dimensions
if (isBoundToContainer(element)) { if (container) {
const container = getContainerElement(element)!;
let height = container.height; let height = container.height;
let width = container.width; let width = container.width;
if (nextHeight > height - BOUND_TEXT_PADDING * 2) { if (nextHeight > height - BOUND_TEXT_PADDING * 2) {

View File

@ -1,6 +1,5 @@
import { getFontString, arrayToMap, isTestEnv } from "../utils"; import { getFontString, arrayToMap, isTestEnv } from "../utils";
import { import {
ExcalidrawBindableElement,
ExcalidrawElement, ExcalidrawElement,
ExcalidrawTextElement, ExcalidrawTextElement,
ExcalidrawTextElementWithContainer, ExcalidrawTextElementWithContainer,
@ -12,6 +11,7 @@ import { BOUND_TEXT_PADDING, VERTICAL_ALIGN } from "../constants";
import { MaybeTransformHandleType } from "./transformHandles"; import { MaybeTransformHandleType } from "./transformHandles";
import Scene from "../scene/Scene"; import Scene from "../scene/Scene";
import { AppState } from "../types"; import { AppState } from "../types";
import { isTextElement } from ".";
export const redrawTextBoundingBox = ( export const redrawTextBoundingBox = (
element: ExcalidrawTextElement, element: ExcalidrawTextElement,
@ -79,22 +79,24 @@ export const bindTextToShapeAfterDuplication = (
const boundTextElementId = getBoundTextElementId(element); const boundTextElementId = getBoundTextElementId(element);
if (boundTextElementId) { if (boundTextElementId) {
const newTextElementId = oldIdToDuplicatedId.get(boundTextElementId)!; const newTextElementId = oldIdToDuplicatedId.get(boundTextElementId);
mutateElement( if (newTextElementId) {
sceneElementMap.get(newElementId) as ExcalidrawBindableElement, const newContainer = sceneElementMap.get(newElementId);
{ if (newContainer) {
mutateElement(newContainer, {
boundElements: element.boundElements?.concat({ boundElements: element.boundElements?.concat({
type: "text", type: "text",
id: newTextElementId, id: newTextElementId,
}), }),
}, });
); }
mutateElement( const newTextElement = sceneElementMap.get(newTextElementId);
sceneElementMap.get(newTextElementId) as ExcalidrawTextElement, if (newTextElement && isTextElement(newTextElement)) {
{ mutateElement(newTextElement, {
containerId: newElementId, containerId: newContainer ? newElementId : null,
}, });
); }
}
} }
}); });
}; };
@ -440,7 +442,10 @@ export const getApproxCharsToFitInWidth = (font: FontString, width: number) => {
}; };
export const getBoundTextElementId = (container: ExcalidrawElement | null) => { export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
return container?.boundElements?.filter((ele) => ele.type === "text")[0]?.id; return container?.boundElements?.length
? container?.boundElements?.filter((ele) => ele.type === "text")[0]?.id ||
null
: null;
}; };
export const getBoundTextElement = (element: ExcalidrawElement | null) => { export const getBoundTextElement = (element: ExcalidrawElement | null) => {

View File

@ -12,6 +12,8 @@ import {
ExcalidrawTextElementWithContainer, ExcalidrawTextElementWithContainer,
} from "./types"; } from "./types";
import * as textElementUtils from "./textElement"; import * as textElementUtils from "./textElement";
import { API } from "../tests/helpers/api";
import { mutateElement } from "./mutateElement";
// Unmount ReactDOM from root // Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!); ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
@ -19,7 +21,201 @@ const tab = " ";
const mouse = new Pointer("mouse"); const mouse = new Pointer("mouse");
describe("textWysiwyg", () => { describe("textWysiwyg", () => {
describe("Test unbounded text", () => { describe("start text editing", () => {
const { h } = window;
beforeEach(async () => {
await render(<ExcalidrawApp />);
h.elements = [];
});
it("should prefer editing selected text element (non-bindable container present)", async () => {
const line = API.createElement({
type: "line",
width: 100,
height: 0,
points: [
[0, 0],
[100, 0],
],
});
const textSize = 20;
const text = API.createElement({
type: "text",
text: "ola",
x: line.width / 2 - textSize / 2,
y: -textSize / 2,
width: textSize,
height: textSize,
});
h.elements = [text, line];
API.setSelectedElements([text]);
Keyboard.keyPress(KEYS.ENTER);
expect(h.state.editingElement?.id).toBe(text.id);
expect(
(h.state.editingElement as ExcalidrawTextElement).containerId,
).toBe(null);
});
it("should prefer editing selected text element (bindable container present)", async () => {
const container = API.createElement({
type: "rectangle",
width: 100,
boundElements: [],
});
const textSize = 20;
const boundText = API.createElement({
type: "text",
text: "ola",
x: container.width / 2 - textSize / 2,
y: container.height / 2 - textSize / 2,
width: textSize,
height: textSize,
containerId: container.id,
});
const boundText2 = API.createElement({
type: "text",
text: "ola",
x: container.width / 2 - textSize / 2,
y: container.height / 2 - textSize / 2,
width: textSize,
height: textSize,
containerId: container.id,
});
h.elements = [container, boundText, boundText2];
mutateElement(container, {
boundElements: [{ type: "text", id: boundText.id }],
});
API.setSelectedElements([boundText2]);
Keyboard.keyPress(KEYS.ENTER);
expect(h.state.editingElement?.id).toBe(boundText2.id);
});
it("should not create bound text on ENTER if text exists at container center", () => {
const container = API.createElement({
type: "rectangle",
width: 100,
});
const textSize = 20;
const text = API.createElement({
type: "text",
text: "ola",
x: container.width / 2 - textSize / 2,
y: container.height / 2 - textSize / 2,
width: textSize,
height: textSize,
containerId: container.id,
});
h.elements = [container, text];
API.setSelectedElements([container]);
Keyboard.keyPress(KEYS.ENTER);
expect(h.state.editingElement?.id).toBe(text.id);
});
it("should edit existing bound text on ENTER even if higher z-index unbound text exists at container center", () => {
const container = API.createElement({
type: "rectangle",
width: 100,
boundElements: [],
});
const textSize = 20;
const boundText = API.createElement({
type: "text",
text: "ola",
x: container.width / 2 - textSize / 2,
y: container.height / 2 - textSize / 2,
width: textSize,
height: textSize,
containerId: container.id,
});
const boundText2 = API.createElement({
type: "text",
text: "ola",
x: container.width / 2 - textSize / 2,
y: container.height / 2 - textSize / 2,
width: textSize,
height: textSize,
containerId: container.id,
});
h.elements = [container, boundText, boundText2];
mutateElement(container, {
boundElements: [{ type: "text", id: boundText.id }],
});
API.setSelectedElements([container]);
Keyboard.keyPress(KEYS.ENTER);
expect(h.state.editingElement?.id).toBe(boundText.id);
});
it("should edit text under cursor when clicked with text tool", () => {
const text = API.createElement({
type: "text",
text: "ola",
x: 60,
y: 0,
width: 100,
height: 100,
});
h.elements = [text];
UI.clickTool("text");
mouse.clickAt(text.x + 50, text.y + 50);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
expect(editor).not.toBe(null);
expect(h.state.editingElement?.id).toBe(text.id);
expect(h.elements.length).toBe(1);
});
it("should edit text under cursor when double-clicked with selection tool", () => {
const text = API.createElement({
type: "text",
text: "ola",
x: 60,
y: 0,
width: 100,
height: 100,
});
h.elements = [text];
UI.clickTool("selection");
mouse.doubleClickAt(text.x + 50, text.y + 50);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
expect(editor).not.toBe(null);
expect(h.state.editingElement?.id).toBe(text.id);
expect(h.elements.length).toBe(1);
});
});
describe("Test container-unbound text", () => {
const { h } = window; const { h } = window;
let textarea: HTMLTextAreaElement; let textarea: HTMLTextAreaElement;
@ -235,7 +431,7 @@ describe("textWysiwyg", () => {
}); });
}); });
describe("Test bounded text", () => { describe("Test container-bound text", () => {
let rectangle: any; let rectangle: any;
const { h } = window; const { h } = window;
@ -315,6 +511,39 @@ describe("textWysiwyg", () => {
]); ]);
}); });
it("shouldn't bind to non-text-bindable containers", async () => {
const line = API.createElement({
type: "line",
width: 100,
height: 0,
points: [
[0, 0],
[100, 0],
],
});
h.elements = [line];
UI.clickTool("text");
mouse.clickAt(line.x + line.width / 2, line.y + line.height / 2);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
fireEvent.change(editor, {
target: {
value: "Hello World!",
},
});
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
editor.dispatchEvent(new Event("input"));
expect(line.boundElements).toBe(null);
expect(h.elements[1].type).toBe("text");
expect((h.elements[1] as ExcalidrawTextElement).containerId).toBe(null);
});
it("should update font family correctly on undo/redo by selecting bounded text when font family was updated", async () => { it("should update font family correctly on undo/redo by selecting bounded text when font family was updated", async () => {
expect(h.elements.length).toBe(1); expect(h.elements.length).toBe(1);

View File

@ -8,6 +8,7 @@ import {
InitializedExcalidrawImageElement, InitializedExcalidrawImageElement,
ExcalidrawImageElement, ExcalidrawImageElement,
ExcalidrawTextElementWithContainer, ExcalidrawTextElementWithContainer,
ExcalidrawTextContainer,
} from "./types"; } from "./types";
export const isGenericElement = ( export const isGenericElement = (
@ -91,7 +92,9 @@ export const isBindableElement = (
); );
}; };
export const isTextBindableContainer = (element: ExcalidrawElement | null) => { export const isTextBindableContainer = (
element: ExcalidrawElement | null,
): element is ExcalidrawTextContainer => {
return ( return (
element != null && element != null &&
(element.type === "rectangle" || (element.type === "rectangle" ||

View File

@ -135,8 +135,14 @@ export type ExcalidrawBindableElement =
| ExcalidrawTextElement | ExcalidrawTextElement
| ExcalidrawImageElement; | ExcalidrawImageElement;
export type ExcalidrawTextContainer =
| ExcalidrawRectangleElement
| ExcalidrawDiamondElement
| ExcalidrawEllipseElement
| ExcalidrawImageElement;
export type ExcalidrawTextElementWithContainer = { export type ExcalidrawTextElementWithContainer = {
containerId: ExcalidrawGenericElement["id"]; containerId: ExcalidrawTextContainer["id"];
} & ExcalidrawTextElement; } & ExcalidrawTextElement;
export type PointBinding = { export type PointBinding = {

View File

@ -1,13 +1,7 @@
import { import { GroupId, ExcalidrawElement, NonDeleted } from "./element/types";
GroupId,
ExcalidrawElement,
NonDeleted,
ExcalidrawTextElementWithContainer,
} from "./element/types";
import { AppState } from "./types"; import { AppState } from "./types";
import { getSelectedElements } from "./scene"; import { getSelectedElements } from "./scene";
import { getBoundTextElementId } from "./element/textElement"; import { getBoundTextElement } from "./element/textElement";
import Scene from "./scene/Scene";
export const selectGroup = ( export const selectGroup = (
groupId: GroupId, groupId: GroupId,
@ -182,13 +176,10 @@ export const getMaximumGroups = (
const currentGroupMembers = groups.get(groupId) || []; const currentGroupMembers = groups.get(groupId) || [];
// Include bounded text if present when grouping // Include bound text if present when grouping
const boundTextElementId = getBoundTextElementId(element); const boundTextElement = getBoundTextElement(element);
if (boundTextElementId) { if (boundTextElement) {
const textElement = Scene.getScene(element)!.getElement( currentGroupMembers.push(boundTextElement);
boundTextElementId,
) as ExcalidrawTextElementWithContainer;
currentGroupMembers.push(textElement);
} }
groups.set(groupId, [...currentGroupMembers, element]); groups.set(groupId, [...currentGroupMembers, element]);
}); });

View File

@ -1,9 +1,11 @@
import { import {
ExcalidrawElement, ExcalidrawElement,
ExcalidrawTextContainer,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
} from "../element/types"; } from "../element/types";
import { getElementAbsoluteCoords } from "../element"; import { getElementAbsoluteCoords } from "../element";
import { isTextBindableContainer } from "../element/typeChecks";
export const hasBackground = (type: string) => export const hasBackground = (type: string) =>
type === "rectangle" || type === "rectangle" ||
@ -72,11 +74,11 @@ export const getElementsAtPosition = (
); );
}; };
export const getElementContainingPosition = ( export const getTextBindableContainerAtPosition = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
x: number, x: number,
y: number, y: number,
) => { ): ExcalidrawTextContainer | null => {
let hitElement = null; let hitElement = null;
// We need to to hit testing from front (end of the array) to back (beginning of the array) // We need to to hit testing from front (end of the array) to back (beginning of the array)
for (let index = elements.length - 1; index >= 0; --index) { for (let index = elements.length - 1; index >= 0; --index) {
@ -89,5 +91,5 @@ export const getElementContainingPosition = (
break; break;
} }
} }
return hitElement; return isTextBindableContainer(hitElement) ? hitElement : null;
}; };

View File

@ -14,7 +14,7 @@ export {
canHaveArrowheads, canHaveArrowheads,
canChangeSharpness, canChangeSharpness,
getElementAtPosition, getElementAtPosition,
getElementContainingPosition, getTextBindableContainerAtPosition,
hasText, hasText,
getElementsAtPosition, getElementsAtPosition,
} from "./comparisons"; } from "./comparisons";

View File

@ -152,6 +152,7 @@ describe("element binding", () => {
UI.clickTool("text"); UI.clickTool("text");
mouse.clickAt(text.x + 50, text.y + 50); mouse.clickAt(text.x + 50, text.y + 50);
const editor = document.querySelector( const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea", ".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement; ) as HTMLTextAreaElement;

View File

@ -9,7 +9,7 @@ Object {
"endBinding": null, "endBinding": null,
"fillStyle": "hachure", "fillStyle": "hachure",
"groupIds": Array [], "groupIds": Array [],
"height": 0, "height": 100,
"id": "id-arrow01", "id": "id-arrow01",
"isDeleted": false, "isDeleted": false,
"lastCommittedPoint": null, "lastCommittedPoint": null,
@ -21,8 +21,8 @@ Object {
0, 0,
], ],
Array [ Array [
0, 100,
0, 100,
], ],
], ],
"roughness": 1, "roughness": 1,
@ -37,7 +37,7 @@ Object {
"updated": 1, "updated": 1,
"version": 1, "version": 1,
"versionNonce": 0, "versionNonce": 0,
"width": 0, "width": 100,
"x": 0, "x": 0,
"y": 0, "y": 0,
} }
@ -180,7 +180,7 @@ Object {
"endBinding": null, "endBinding": null,
"fillStyle": "hachure", "fillStyle": "hachure",
"groupIds": Array [], "groupIds": Array [],
"height": 0, "height": 100,
"id": "id-line01", "id": "id-line01",
"isDeleted": false, "isDeleted": false,
"lastCommittedPoint": null, "lastCommittedPoint": null,
@ -192,8 +192,8 @@ Object {
0, 0,
], ],
Array [ Array [
0, 100,
0, 100,
], ],
], ],
"roughness": 1, "roughness": 1,
@ -208,7 +208,7 @@ Object {
"updated": 1, "updated": 1,
"version": 1, "version": 1,
"versionNonce": 0, "versionNonce": 0,
"width": 0, "width": 100,
"x": 0, "x": 0,
"y": 0, "y": 0,
} }
@ -223,7 +223,7 @@ Object {
"endBinding": null, "endBinding": null,
"fillStyle": "hachure", "fillStyle": "hachure",
"groupIds": Array [], "groupIds": Array [],
"height": 0, "height": 100,
"id": "id-draw01", "id": "id-draw01",
"isDeleted": false, "isDeleted": false,
"lastCommittedPoint": null, "lastCommittedPoint": null,
@ -235,8 +235,8 @@ Object {
0, 0,
], ],
Array [ Array [
0, 100,
0, 100,
], ],
], ],
"roughness": 1, "roughness": 1,
@ -251,7 +251,7 @@ Object {
"updated": 1, "updated": 1,
"version": 1, "version": 1,
"versionNonce": 0, "versionNonce": 0,
"width": 0, "width": 100,
"x": 0, "x": 0,
"y": 0, "y": 0,
} }

View File

@ -14,6 +14,7 @@ import util from "util";
import path from "path"; import path from "path";
import { getMimeType } from "../../data/blob"; import { getMimeType } from "../../data/blob";
import { newFreeDrawElement } from "../../element/newElement"; import { newFreeDrawElement } from "../../element/newElement";
import { Point } from "../../types";
const readFile = util.promisify(fs.readFile); const readFile = util.promisify(fs.readFile);
@ -98,6 +99,7 @@ export class API {
containerId?: T extends "text" containerId?: T extends "text"
? ExcalidrawTextElement["containerId"] ? ExcalidrawTextElement["containerId"]
: never; : never;
points?: T extends "arrow" | "line" ? readonly Point[] : never;
}): T extends "arrow" | "line" }): T extends "arrow" | "line"
? ExcalidrawLinearElement ? ExcalidrawLinearElement
: T extends "freedraw" : T extends "freedraw"
@ -158,10 +160,13 @@ export class API {
case "arrow": case "arrow":
case "line": case "line":
element = newLinearElement({ element = newLinearElement({
...base,
width,
height,
type: type as "arrow" | "line", type: type as "arrow" | "line",
startArrowhead: null, startArrowhead: null,
endArrowhead: null, endArrowhead: null,
...base, points: rest.points ?? [],
}); });
break; break;
} }